Archived
2
0

Compare commits

..

1 Commits

Author SHA1 Message Date
Miroslav Šedivý
3e67e44c61 initial h265 implementation. 2021-12-08 20:01:47 +01:00
233 changed files with 3439 additions and 33132 deletions

View File

@ -2,7 +2,7 @@
If you want to contribute, but do not want to install anything on your host system, we got you covered. You only need docker. Technically, it could be done using vs code development in container, but this is more fun:).
You need to copy `.env.default` to `.env` and customize values.
You need to copy `.env.development` to `.env` and customize values.
## Step 1: Building server

View File

@ -1,7 +1,7 @@
#
# STAGE 1: SERVER
#
FROM golang:1.20-bullseye as server
FROM golang:1.17-bullseye as server
WORKDIR /src
#
@ -13,7 +13,7 @@ RUN set -eux; apt-get update; \
# install libclipboard
set -eux; \
cd /tmp; \
git clone --depth=1 https://github.com/jtanx/libclipboard; \
git clone https://github.com/jtanx/libclipboard; \
cd libclipboard; \
cmake .; \
make -j4; \
@ -27,12 +27,12 @@ RUN set -eux; apt-get update; \
#
# build server
COPY server/ .
RUN ./build
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
FROM node:14-bullseye-slim as client
WORKDIR /src
#
@ -60,10 +60,9 @@ ARG USERNAME=neko
ARG USER_UID=1000
ARG USER_GID=$USER_UID
RUN set -eux; \
apt-get update; \
#
# install dependencies
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends wget ca-certificates supervisor; \
apt-get install -y --no-install-recommends pulseaudio dbus-x11 xserver-xorg-video-dummy; \
apt-get install -y --no-install-recommends libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx6; \
@ -72,16 +71,8 @@ RUN set -eux; \
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; \
#
# 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; \
# fonts
apt-get install -y --no-install-recommends fonts-takao-mincho; \
#
# create a non-root user
groupadd --gid $USER_GID $USERNAME; \
@ -90,18 +81,19 @@ 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 \
/tmp/runtime-$USERNAME \
/home/$USERNAME/.config/pulse \
/home/$USERNAME/.local/share/xorg; \
mkdir -p /etc/neko /var/www /var/log/neko; \
chmod 1777 /var/log/neko; \
chown $USERNAME /var/log/neko/ /tmp/runtime-$USERNAME; \
chown $USERNAME /var/log/neko/; \
chown -R $USERNAME:$USERNAME /home/$USERNAME; \
#
# clean up
@ -119,8 +111,6 @@ 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

View File

@ -1,7 +1,7 @@
#
# STAGE 1: SERVER
#
FROM golang:1.20-bullseye as server
FROM arm32v7/golang:1.17-buster as server
WORKDIR /src
#
@ -13,7 +13,7 @@ RUN set -eux; apt-get update; \
# install libclipboard
set -eux; \
cd /tmp; \
git clone --depth=1 https://github.com/jtanx/libclipboard; \
git clone https://github.com/jtanx/libclipboard; \
cd libclipboard; \
cmake .; \
make -j4; \
@ -27,36 +27,33 @@ RUN set -eux; apt-get update; \
#
# build server
COPY server/ .
RUN ./build
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
# install dependencies
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends python2 build-essential
WORKDIR /src
#
# Because client builds fail in Github Actions, therefor we build it outside of Docker.
#
# FROM node:18-bullseye-slim as client
#
# # install dependencies
# RUN set -eux; apt-get update; \
# apt-get install -y --no-install-recommends python2 build-essential
#
# WORKDIR /src
#
# #
# # install dependencies
# COPY client/package*.json ./
# RUN npm install
#
# #
# # build client
# COPY client/ .
# RUN npm run build
# install dependencies
COPY client/package*.json ./
RUN npm install
#
# build client
COPY client/ .
RUN npm run build
#
# STAGE 3: RUNTIME
#
FROM debian:bullseye-slim
FROM arm32v7/debian:buster-slim
#
# avoid warnings by switching to noninteractive
@ -68,29 +65,19 @@ ARG USERNAME=neko
ARG USER_UID=1000
ARG USER_GID=$USER_UID
RUN set -eux; \
apt-get update; \
#
# install dependencies
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends wget ca-certificates supervisor; \
apt-get install -y --no-install-recommends pulseaudio dbus-x11 xserver-xorg-video-dummy; \
apt-get install -y --no-install-recommends libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx6; \
apt-get install -y --no-install-recommends libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx5; \
#
# 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; \
#
# 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; \
# fonts
apt-get install -y --no-install-recommends fonts-takao-mincho; \
#
# create a non-root user
groupadd --gid $USER_GID $USERNAME; \
@ -99,18 +86,19 @@ 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 \
/tmp/runtime-$USERNAME \
/home/$USERNAME/.config/pulse \
/home/$USERNAME/.local/share/xorg; \
mkdir -p /etc/neko /var/www /var/log/neko; \
chmod 1777 /var/log/neko; \
chown $USERNAME /var/log/neko/ /tmp/runtime-$USERNAME; \
chown $USERNAME /var/log/neko/; \
chown -R $USERNAME:$USERNAME /home/$USERNAME; \
#
# clean up
@ -128,8 +116,6 @@ 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
@ -137,8 +123,7 @@ ENV NEKO_BIND=:8080
#
# copy static files from previous stages
COPY --from=server /src/bin/neko /usr/bin/neko
# COPY --from=client /src/dist/ /var/www
COPY client/dist/ /var/www
COPY --from=client /src/dist/ /var/www
HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
CMD wget -O - http://localhost:${NEKO_BIND#*:}/health || exit 1
@ -146,3 +131,4 @@ HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
#
# run neko
CMD ["/usr/bin/supervisord", "-c", "/etc/neko/supervisord.conf"]

View File

@ -1,150 +0,0 @@
#
# 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 ./build
#
# 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"]

View File

@ -1,294 +0,0 @@
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 ./build
#
# 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"]

View File

@ -1,8 +1,5 @@
#!/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

View File

@ -1,18 +0,0 @@
#!/bin/bash
# if no hwenc required, noop
[[ -z "$NEKO_HWENC" ]] && exit 0
if [[ -z "$RENDER_GID" ]]; then
RENDER_GID=$(stat -c "%g" /dev/dri/render* | tail -n 1)
# is /dev/dri passed to the container?
[[ -z "$RENDER_GID" ]] && exit 1
fi
# note that this could conceivably be a security risk...
cnt_group=$(getent group "$RENDER_GID" | cut -d: -f1)
if [[ -z "$cnt_group" ]]; then
groupadd -g "$RENDER_GID" nekorender
cnt_group=nekorender
fi
usermod -a -G "$cnt_group" "$USER"

View File

@ -1,69 +0,0 @@
[supervisord]
nodaemon=true
user=root
pidfile=/var/run/supervisord.pid
logfile=/dev/null
logfile_maxbytes=0
loglevel=debug
[include]
files=/etc/neko/supervisord/*.conf
[program:rendergroup-init]
environment=RENDER_GID="%(ENV_RENDER_GID)s",USER="%(ENV_USER)s"
command=/usr/bin/add-render-group.sh
startsecs=0
startretries=0
autorestart=false
priority=10
user=root
stdout_logfile=/var/log/neko/rendergroup.log
stdout_logfile_maxbytes=1MB
stdout_logfile_backups=10
redirect_stderr=true
[program:dbus]
environment=HOME="/root",USER="root"
command=/usr/bin/dbus
autorestart=true
priority=100
user=root
stdout_logfile=/var/log/neko/dbus.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true
[program:x-server]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s"
command=/usr/bin/X -config /etc/neko/xorg.conf %(ENV_DISPLAY)s
autorestart=true
priority=300
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/xorg.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true
[program:pulseaudio]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/pulseaudio --log-level=info --disallow-module-loading --disallow-exit --exit-idle-time=-1
autorestart=true
priority=300
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/pulseaudio.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true
[program:neko]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/neko serve --static "/var/www"
stopsignal=INT
stopwaitsecs=5
autorestart=true
priority=800
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/neko.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true

View File

@ -1,12 +0,0 @@
#!/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

View File

@ -33,7 +33,7 @@ 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
command=/usr/bin/pulseaudio --disallow-module-loading -vvvv --disallow-exit --exit-idle-time=-1
autorestart=true
priority=300
user=%(ENV_USER)s
@ -54,14 +54,3 @@ 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

View File

@ -1,23 +0,0 @@
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

View File

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

View File

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

View File

@ -1,17 +1,6 @@
[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

View File

@ -1,36 +0,0 @@
[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

View File

@ -1,5 +1,4 @@
#!/bin/sh
set -ex
cd "$(dirname "$0")"
BASE="${PWD}/../"
@ -77,36 +76,6 @@ 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;;
@ -114,12 +83,6 @@ 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

View File

@ -3,21 +3,14 @@ FROM $BASE_IMAGE
#
# install neko chromium
RUN set -eux; \
echo "deb http://ftp.de.debian.org/debian bookworm main" >> /etc/apt/sources.list; \
apt-get update; \
apt-get install -y --no-install-recommends unzip chromium chromium-common chromium-sandbox openbox; \
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends unzip chromium chromium-sandbox openbox; \
#
# install widevine module
CHROMIUM_DIR="/usr/lib/chromium"; \
WIDEVINE_VERSION=$(wget --quiet -O - https://dl.google.com/widevine-cdm/versions.txt | sort --version-sort | 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 '{}' \;; \
WIDEVINE_VERSION=$(wget --quiet -O - https://dl.google.com/widevine-cdm/versions.txt | tail -n 1); \
wget -O /tmp/widevine.zip "https://dl.google.com/widevine-cdm/$WIDEVINE_VERSION-linux-x64.zip"; \
unzip -p /tmp/widevine.zip libwidevinecdm.so > /usr/lib/chromium/libwidevinecdm.so; \
chmod 644 /usr/lib/chromium/libwidevinecdm.so; \
rm /tmp/widevine.zip; \
#
# clean up

View File

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

View File

@ -1,37 +0,0 @@
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 | sort --version-sort | 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

View File

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

View File

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

View File

@ -1,17 +1,6 @@
[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

View File

@ -1,36 +0,0 @@
[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

View File

@ -10,7 +10,7 @@ RUN set -eux; apt-get update; \
# install extensions
mkdir -p /usr/lib/firefox-esr/distribution/extensions; \
wget -O '/usr/lib/firefox-esr/distribution/extensions/uBlock0@raymondhill.net.xpi' https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi; \
wget -O '/usr/lib/firefox-esr/distribution/extensions/sponsorBlocker@ajay.app.xpi' https://addons.mozilla.org/firefox/downloads/latest/sponsorblock/latest.xpi; \
wget -O /usr/lib/firefox-esr/distribution/extensions/nordvpnproxy@nordvpn.com.xpi https://addons.mozilla.org/firefox/downloads/latest/nordvpn-proxy-extension/latest.xpi; \
#
# create a profile directory
mkdir -p /home/neko/.mozilla/firefox/profile.default/extensions; \

View File

@ -1,35 +0,0 @@
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

View File

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

View File

@ -56,8 +56,8 @@
"*": {
"installation_mode": "blocked"
},
"sponsorBlocker@ajay.app": {
"install_url": "https://addons.mozilla.org/firefox/downloads/latest/sponsorblock/latest.xpi",
"nordvpnproxy@nordvpn.com": {
"install_url": "https://addons.mozilla.org/firefox/downloads/latest/nordvpn-proxy-extension/latest.xpi",
"installation_mode": "force_installed"
},
"uBlock0@raymondhill.net": {
@ -108,7 +108,7 @@
"datareporting.policy.dataSubmissionPolicyBypassNotification": true,
"dom.disable_window_flip": true,
"dom.disable_window_move_resize": true,
"dom.event.contextmenu.enabled": true,
"dom.event.contextmenu.enabled": false,
"extensions.getAddons.showPane": false,
"places.history.enabled": false,
"privacy.file_unique_origin": true,

View File

@ -1,28 +0,0 @@
[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

View File

@ -1,23 +0,0 @@
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

View File

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

View File

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

View File

@ -1,17 +1,6 @@
[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

View File

@ -1,36 +0,0 @@
[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

View File

@ -1,26 +0,0 @@
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

View File

@ -1,23 +0,0 @@
[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

View File

@ -1,24 +0,0 @@
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' | sort --version-sort | tail -1)"; \
wget -O /tmp/microsoft-edge.deb "${SRC_URL}"; \
apt-get install -y --no-install-recommends openbox /tmp/microsoft-edge.deb; \
#
# clean up
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# copy configuation files
COPY supervisord.conf /etc/neko/supervisord/microsoft-edge.conf
COPY --chown=neko preferences.json /home/neko/.config/microsoft-edge/Default/Preferences
COPY policies.json /etc/opt/edge/policies/managed/policies.json
COPY openbox.xml /etc/neko/openbox.xml

View File

@ -1,24 +0,0 @@
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' | sort --version-sort | 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

View File

@ -1,763 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Default openbox config but all window decorations are moved
thereby making it harder to accidentally close the virtual browser -->
<openbox_config xmlns="http://openbox.org/3.4/rc"
xmlns:xi="http://www.w3.org/2001/XInclude">
<resistance>
<strength>10</strength>
<screen_edge_strength>20</screen_edge_strength>
</resistance>
<applications>
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
<application class="Microsoft-edge*" name="microsoft-edge*" role="browser">
<decor>no</decor>
<maximized>true</maximized>
<focus>yes</focus>
<layer>normal</layer>
</application>
</applications>
<focus>
<focusNew>yes</focusNew>
<!-- always try to focus new windows when they appear. other rules do
apply -->
<followMouse>no</followMouse>
<!-- move focus to a window when you move the mouse into it -->
<focusLast>yes</focusLast>
<!-- focus the last used window when changing desktops, instead of the one
under the mouse pointer. when followMouse is enabled -->
<underMouse>no</underMouse>
<!-- move focus under the mouse, even when the mouse is not moving -->
<focusDelay>200</focusDelay>
<!-- when followMouse is enabled, the mouse must be inside the window for
this many milliseconds (1000 = 1 sec) before moving focus to it -->
<raiseOnFocus>no</raiseOnFocus>
<!-- when followMouse is enabled, and a window is given focus by moving the
mouse into it, also raise the window -->
</focus>
<placement>
<policy>Smart</policy>
<!-- 'Smart' or 'UnderMouse' -->
<center>yes</center>
<!-- whether to place windows in the center of the free area found or
the top left corner -->
<monitor>Primary</monitor>
<!-- with Smart placement on a multi-monitor system, try to place new windows
on: 'Any' - any monitor, 'Mouse' - where the mouse is, 'Active' - where
the active window is, 'Primary' - only on the primary monitor -->
<primaryMonitor>1</primaryMonitor>
<!-- The monitor where Openbox should place popup dialogs such as the
focus cycling popup, or the desktop switch popup. It can be an index
from 1, specifying a particular monitor. Or it can be one of the
following: 'Mouse' - where the mouse is, or
'Active' - where the active window is -->
</placement>
<theme>
<name>Clearlooks</name>
<titleLayout>NLIMC</titleLayout>
<!--
available characters are NDSLIMC, each can occur at most once.
N: window icon
L: window label (AKA title).
I: iconify
M: maximize
C: close
S: shade (roll up/down)
D: omnipresent (on all desktops).
-->
<keepBorder>yes</keepBorder>
<animateIconify>yes</animateIconify>
<font place="ActiveWindow">
<name>sans</name>
<size>8</size>
<!-- font size in points -->
<weight>bold</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="InactiveWindow">
<name>sans</name>
<size>8</size>
<!-- font size in points -->
<weight>bold</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="MenuHeader">
<name>sans</name>
<size>9</size>
<!-- font size in points -->
<weight>normal</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="MenuItem">
<name>sans</name>
<size>9</size>
<!-- font size in points -->
<weight>normal</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="ActiveOnScreenDisplay">
<name>sans</name>
<size>9</size>
<!-- font size in points -->
<weight>bold</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="InactiveOnScreenDisplay">
<name>sans</name>
<size>9</size>
<!-- font size in points -->
<weight>bold</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
</theme>
<desktops>
<!-- this stuff is only used at startup, pagers allow you to change them
during a session
these are default values to use when other ones are not already set
by other applications, or saved in your session
use obconf if you want to change these without having to log out
and back in -->
<number>1</number>
<firstdesk>1</firstdesk>
<names>
<!-- set names up here if you want to, like this:
<name>desktop 1</name>
<name>desktop 2</name>
-->
</names>
<popupTime>875</popupTime>
<!-- The number of milliseconds to show the popup for when switching
desktops. Set this to 0 to disable the popup. -->
</desktops>
<resize>
<drawContents>yes</drawContents>
<popupShow>Nonpixel</popupShow>
<!-- 'Always', 'Never', or 'Nonpixel' (xterms and such) -->
<popupPosition>Center</popupPosition>
<!-- 'Center', 'Top', or 'Fixed' -->
<popupFixedPosition>
<!-- these are used if popupPosition is set to 'Fixed' -->
<x>10</x>
<!-- positive number for distance from left edge, negative number for
distance from right edge, or 'Center' -->
<y>10</y>
<!-- positive number for distance from top edge, negative number for
distance from bottom edge, or 'Center' -->
</popupFixedPosition>
</resize>
<!-- You can reserve a portion of your screen where windows will not cover when
they are maximized, or when they are initially placed.
Many programs reserve space automatically, but you can use this in other
cases. -->
<margins>
<top>0</top>
<bottom>0</bottom>
<left>0</left>
<right>0</right>
</margins>
<dock>
<position>TopLeft</position>
<!-- (Top|Bottom)(Left|Right|)|Top|Bottom|Left|Right|Floating -->
<floatingX>0</floatingX>
<floatingY>0</floatingY>
<noStrut>no</noStrut>
<stacking>Above</stacking>
<!-- 'Above', 'Normal', or 'Below' -->
<direction>Vertical</direction>
<!-- 'Vertical' or 'Horizontal' -->
<autoHide>no</autoHide>
<hideDelay>300</hideDelay>
<!-- in milliseconds (1000 = 1 second) -->
<showDelay>300</showDelay>
<!-- in milliseconds (1000 = 1 second) -->
<moveButton>Middle</moveButton>
<!-- 'Left', 'Middle', 'Right' -->
</dock>
<keyboard>
<chainQuitKey>C-g</chainQuitKey>
<!-- Keybindings for desktop switching -->
<keybind key="C-A-Left">
<action name="GoToDesktop"><to>left</to><wrap>no</wrap></action>
</keybind>
<keybind key="C-A-Right">
<action name="GoToDesktop"><to>right</to><wrap>no</wrap></action>
</keybind>
<keybind key="C-A-Up">
<action name="GoToDesktop"><to>up</to><wrap>no</wrap></action>
</keybind>
<keybind key="C-A-Down">
<action name="GoToDesktop"><to>down</to><wrap>no</wrap></action>
</keybind>
<keybind key="S-A-Left">
<action name="SendToDesktop"><to>left</to><wrap>no</wrap></action>
</keybind>
<keybind key="S-A-Right">
<action name="SendToDesktop"><to>right</to><wrap>no</wrap></action>
</keybind>
<keybind key="S-A-Up">
<action name="SendToDesktop"><to>up</to><wrap>no</wrap></action>
</keybind>
<keybind key="S-A-Down">
<action name="SendToDesktop"><to>down</to><wrap>no</wrap></action>
</keybind>
<keybind key="W-F1">
<action name="GoToDesktop"><to>1</to></action>
</keybind>
<keybind key="W-F2">
<action name="GoToDesktop"><to>2</to></action>
</keybind>
<keybind key="W-F3">
<action name="GoToDesktop"><to>3</to></action>
</keybind>
<keybind key="W-F4">
<action name="GoToDesktop"><to>4</to></action>
</keybind>
<keybind key="W-d">
<action name="ToggleShowDesktop"/>
</keybind>
<!-- Keybindings for windows -->
<keybind key="A-F4">
<action name="Close"/>
</keybind>
<keybind key="A-Escape">
<action name="Lower"/>
<action name="FocusToBottom"/>
<action name="Unfocus"/>
</keybind>
<keybind key="A-space">
<!--action name="ShowMenu"><menu>client-menu</menu></action-->
</keybind>
<!-- Take a screenshot of the current window with scrot when Alt+Print are pressed -->
<keybind key="A-Print">
<action name="Execute"><command>scrot -s</command></action>
</keybind>
<!-- Keybindings for window switching -->
<keybind key="A-Tab">
<action name="NextWindow">
<finalactions>
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</finalactions>
</action>
</keybind>
<keybind key="A-S-Tab">
<action name="PreviousWindow">
<finalactions>
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</finalactions>
</action>
</keybind>
<keybind key="C-A-Tab">
<action name="NextWindow">
<panels>yes</panels><desktop>yes</desktop>
<finalactions>
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</finalactions>
</action>
</keybind>
<!-- Keybindings for window switching with the arrow keys -->
<keybind key="W-S-Right">
<action name="DirectionalCycleWindows">
<direction>right</direction>
</action>
</keybind>
<keybind key="W-S-Left">
<action name="DirectionalCycleWindows">
<direction>left</direction>
</action>
</keybind>
<keybind key="W-S-Up">
<action name="DirectionalCycleWindows">
<direction>up</direction>
</action>
</keybind>
<keybind key="W-S-Down">
<action name="DirectionalCycleWindows">
<direction>down</direction>
</action>
</keybind>
<!-- Keybindings for running applications -->
<keybind key="W-e">
<action name="Execute">
<startupnotify>
<enabled>true</enabled>
<name>Konqueror</name>
</startupnotify>
<command>kfmclient openProfile filemanagement</command>
</action>
</keybind>
<!-- Launch scrot when Print is pressed -->
<keybind key="Print">
<action name="Execute"><command>scrot</command></action>
</keybind>
</keyboard>
<mouse>
<dragThreshold>1</dragThreshold>
<!-- number of pixels the mouse must move before a drag begins -->
<doubleClickTime>500</doubleClickTime>
<!-- in milliseconds (1000 = 1 second) -->
<screenEdgeWarpTime>400</screenEdgeWarpTime>
<!-- Time before changing desktops when the pointer touches the edge of the
screen while moving a window, in milliseconds (1000 = 1 second).
Set this to 0 to disable warping -->
<screenEdgeWarpMouse>false</screenEdgeWarpMouse>
<!-- Set this to TRUE to move the mouse pointer across the desktop when
switching due to hitting the edge of the screen -->
<context name="Frame">
<mousebind button="A-Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="A-Left" action="Click">
<action name="Unshade"/>
</mousebind>
<mousebind button="A-Left" action="Drag">
<action name="Move"/>
</mousebind>
<mousebind button="A-Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="A-Right" action="Drag">
<action name="Resize"/>
</mousebind>
<mousebind button="A-Middle" action="Press">
<action name="Lower"/>
<action name="FocusToBottom"/>
<action name="Unfocus"/>
</mousebind>
<mousebind button="A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="C-A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="C-A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="A-S-Up" action="Click">
<action name="SendToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="A-S-Down" action="Click">
<action name="SendToDesktop"><to>next</to></action>
</mousebind>
</context>
<context name="Titlebar">
<mousebind button="Left" action="Drag">
<action name="Move"/>
</mousebind>
<mousebind button="Left" action="DoubleClick">
<action name="ToggleMaximize"/>
</mousebind>
<mousebind button="Up" action="Click">
<action name="if">
<shaded>no</shaded>
<then>
<action name="Shade"/>
<action name="FocusToBottom"/>
<action name="Unfocus"/>
<action name="Lower"/>
</then>
</action>
</mousebind>
<mousebind button="Down" action="Click">
<action name="if">
<shaded>yes</shaded>
<then>
<action name="Unshade"/>
<action name="Raise"/>
</then>
</action>
</mousebind>
</context>
<context name="Titlebar Top Right Bottom Left TLCorner TRCorner BRCorner BLCorner">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Middle" action="Press">
<action name="Lower"/>
<action name="FocusToBottom"/>
<action name="Unfocus"/>
</mousebind>
<!--mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="ShowMenu"><menu>client-menu</menu></action>
</mousebind-->
</context>
<context name="Top">
<mousebind button="Left" action="Drag">
<action name="Resize"><edge>top</edge></action>
</mousebind>
</context>
<context name="Left">
<mousebind button="Left" action="Drag">
<action name="Resize"><edge>left</edge></action>
</mousebind>
</context>
<context name="Right">
<mousebind button="Left" action="Drag">
<action name="Resize"><edge>right</edge></action>
</mousebind>
</context>
<context name="Bottom">
<mousebind button="Left" action="Drag">
<action name="Resize"><edge>bottom</edge></action>
</mousebind>
<!--mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="ShowMenu"><menu>client-menu</menu></action>
</mousebind-->
</context>
<context name="TRCorner BRCorner TLCorner BLCorner">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Left" action="Drag">
<action name="Resize"/>
</mousebind>
</context>
<context name="Client">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Middle" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
</context>
<context name="Icon">
<!--mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
<action name="ShowMenu"><menu>client-menu</menu></action>
</mousebind>
<mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="ShowMenu"><menu>client-menu</menu></action>
</mousebind-->
</context>
<context name="AllDesktops">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="ToggleOmnipresent"/>
</mousebind>
</context>
<context name="Shade">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="ToggleShade"/>
</mousebind>
</context>
<context name="Iconify">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="Iconify"/>
</mousebind>
</context>
<context name="Maximize">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Middle" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="ToggleMaximize"/>
</mousebind>
<mousebind button="Middle" action="Click">
<action name="ToggleMaximize"><direction>vertical</direction></action>
</mousebind>
<mousebind button="Right" action="Click">
<action name="ToggleMaximize"><direction>horizontal</direction></action>
</mousebind>
</context>
<context name="Close">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="Close"/>
</mousebind>
</context>
<context name="Desktop">
<mousebind button="Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="C-A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="C-A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
</context>
<context name="Root">
<!-- Menus -->
<!--mousebind button="Middle" action="Press">
<action name="ShowMenu"><menu>client-list-combined-menu</menu></action>
</mousebind>
<mousebind button="Right" action="Press">
<action name="ShowMenu"><menu>root-menu</menu></action>
</mousebind-->
</context>
<context name="MoveResize">
<mousebind button="Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
</context>
</mouse>
<menu>
<!-- You can specify more than one menu file in here and they are all loaded,
just don't make menu ids clash or, well, it'll be kind of pointless -->
<!-- default menu file (or custom one in $HOME/.config/openbox/) -->
<!-- system menu files on Debian systems -->
<!--file>/var/lib/openbox/debian-menu.xml</file-->
<file>menu.xml</file>
<hideDelay>200</hideDelay>
<!-- if a press-release lasts longer than this setting (in milliseconds), the
menu is hidden again -->
<middle>no</middle>
<!-- center submenus vertically about the parent entry -->
<submenuShowDelay>100</submenuShowDelay>
<!-- time to delay before showing a submenu after hovering over the parent
entry.
if this is a negative value, then the delay is infinite and the
submenu will not be shown until it is clicked on -->
<submenuHideDelay>400</submenuHideDelay>
<!-- time to delay before hiding a submenu when selecting another
entry in parent menu
if this is a negative value, then the delay is infinite and the
submenu will not be hidden until a different submenu is opened -->
<showIcons>yes</showIcons>
<!-- controls if icons appear in the client-list-(combined-)menu -->
<manageDesktops>yes</manageDesktops>
<!-- show the manage desktops section in the client-list-(combined-)menu -->
</menu>
<applications>
<!--
# this is an example with comments through out. use these to make your
# own rules, but without the comments of course.
# you may use one or more of the name/class/role/title/type rules to specify
# windows to match
<application name="the window's _OB_APP_NAME property (see obxprop)"
class="the window's _OB_APP_CLASS property (see obxprop)"
groupname="the window's _OB_APP_GROUP_NAME property (see obxprop)"
groupclass="the window's _OB_APP_GROUP_CLASS property (see obxprop)"
role="the window's _OB_APP_ROLE property (see obxprop)"
title="the window's _OB_APP_TITLE property (see obxprop)"
type="the window's _OB_APP_TYPE property (see obxprob)..
(if unspecified, then it is 'dialog' for child windows)">
# you may set only one of name/class/role/title/type, or you may use more
# than one together to restrict your matches.
# the name, class, role, and title use simple wildcard matching such as those
# used by a shell. you can use * to match any characters and ? to match
# any single character.
# the type is one of: normal, dialog, splash, utility, menu, toolbar, dock,
# or desktop
# when multiple rules match a window, they will all be applied, in the
# order that they appear in this list
# each rule element can be left out or set to 'default' to specify to not
# change that attribute of the window
<decor>yes</decor>
# enable or disable window decorations
<shade>no</shade>
# make the window shaded when it appears, or not
<position force="no">
# the position is only used if both an x and y coordinate are provided
# (and not set to 'default')
# when force is "yes", then the window will be placed here even if it
# says you want it placed elsewhere. this is to override buggy
# applications who refuse to behave
<x>center</x>
# a number like 50, or 'center' to center on screen. use a negative number
# to start from the right (or bottom for <y>), ie -50 is 50 pixels from
# the right edge (or bottom). use 'default' to specify using value
# provided by the application, or chosen by openbox, instead.
<y>200</y>
<monitor>1</monitor>
# specifies the monitor in a xinerama setup.
# 1 is the first head, or 'mouse' for wherever the mouse is
</position>
<size>
# the size to make the window.
<width>20</width>
# a number like 20, or 'default' to use the size given by the application.
# you can use fractions such as 1/2 or percentages such as 75% in which
# case the value is relative to the size of the monitor that the window
# appears on.
<height>30%</height>
</size>
<focus>yes</focus>
# if the window should try be given focus when it appears. if this is set
# to yes it doesn't guarantee the window will be given focus. some
# restrictions may apply, but Openbox will try to
<desktop>1</desktop>
# 1 is the first desktop, 'all' for all desktops
<layer>normal</layer>
# 'above', 'normal', or 'below'
<iconic>no</iconic>
# make the window iconified when it appears, or not
<skip_pager>no</skip_pager>
# asks to not be shown in pagers
<skip_taskbar>no</skip_taskbar>
# asks to not be shown in taskbars. window cycling actions will also
# skip past such windows
<fullscreen>yes</fullscreen>
# make the window in fullscreen mode when it appears
<maximized>true</maximized>
# 'Horizontal', 'Vertical' or boolean (yes/no)
</application>
# end of the example
-->
</applications>
</openbox_config>

View File

@ -1,39 +0,0 @@
{
"AutofillAddressEnabled": false,
"AutofillCreditCardEnabled": false,
"BrowserSignin": 0,
"DefaultNotificationsSetting": 2,
"DeveloperToolsAvailability": 2,
"EditBookmarksEnabled": false,
"FullscreenAllowed": true,
"IncognitoModeAvailability": 1,
"SyncDisabled": true,
"AutoplayAllowed": true,
"BrowserAddPersonEnabled": false,
"BrowserGuestModeEnabled": false,
"DefaultPopupsSetting": 2,
"DownloadRestrictions": 3,
"VideoCaptureAllowed": true,
"AllowFileSelectionDialogs": false,
"PromptForDownloadLocation": false,
"BookmarkBarEnabled": false,
"PasswordManagerEnabled": false,
"URLAllowlist": [
"file:///home/neko/Downloads"
],
"URLBlocklist": [
"file://*",
"edge://policy"
],
"ExtensionInstallForcelist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx",
"mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx"
],
"ExtensionInstallAllowlist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm",
"mnjggcdmjocbbbhaepdhchncahnbgone"
],
"ExtensionInstallBlocklist": [
"*"
]
}

View File

@ -1,110 +0,0 @@
{
"homepage": "http://www.google.com",
"homepage_is_newtabpage": false,
"first_run_tabs": [
"https://www.google.com/_/chrome/newtab?ie=UTF-8"
],
"custom_links": {
"initialized": true,
"list": [
{
"title": "YouTube",
"url": "https://www.youtube.com/"
},
{
"title": "Netflix",
"url": "https://netflix.com"
},
{
"title": "Hulu",
"url": "https://www.hulu.com/"
},
{
"title": "9Anime",
"url": "https://9anime.to/"
},
{
"title": "Crunchy Roll",
"url": "https://www.crunchyroll.com/"
},
{
"title": "Funimation",
"url": "https://www.funimation.com/"
},
{
"title": "Disney+",
"url": "https://www.disneyplus.com/"
},
{
"title": "HBO Now",
"url": "https://play.hbonow.com/"
},
{
"title": "Amazon Video",
"url": "https://www.amazon.com/Amazon-Video/b?node=2858778011"
},
{
"title": "VRV",
"url": "https://vrv.co/"
},
{
"title": "Twitch",
"url": "https://www.twitch.tv/"
},
{
"title": "Mixer",
"url": "https://mixer.com/"
}
]
},
"browser": {
"custom_chrome_frame": false,
"show_home_button": true,
"window_placement": {
"maximized": true
}
},
"bookmark_bar": {
"show_on_all_tabs": false
},
"sync_promo": {
"show_on_first_run_allowed": false
},
"distribution": {
"import_bookmarks_from_file": "bookmarks.html",
"import_bookmarks": true,
"import_history": true,
"import_home_page": true,
"import_search_engine": true,
"ping_delay": 60,
"do_not_create_desktop_shortcut": true,
"do_not_create_quick_launch_shortcut": true,
"do_not_create_taskbar_shortcut": true,
"do_not_launch_chrome": true,
"do_not_register_for_update_launch": true,
"make_chrome_default": true,
"make_chrome_default_for_user": true,
"system_level": false,
"verbose_logging": false
},
"profile": {
"avatar_index": 19,
"default_content_setting_values": {
"clipboard": 2,
"cookies": 4,
"geolocation": 2,
"media_stream_camera": 2,
"media_stream_mic": 2,
"midi_sysex": 2,
"payment_handler": 2,
"usb_guard": 2
},
"name": "neko",
"using_default_avatar": false,
"using_default_name": false,
"using_gaia_avatar": false
},
"signin": {
"allowed": false
}
}

View File

@ -1,33 +0,0 @@
[program:microsoft-edge]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/microsoft-edge
--window-position=0,0
--display=%(ENV_DISPLAY)s
--user-data-dir=/home/neko/.config/microsoft-edge
--no-first-run
--start-maximized
--bwsi
--force-dark-mode
--disable-file-system
--disable-gpu
--disable-software-rasterizer
--disable-dev-shm-usage
stopsignal=INT
autorestart=true
priority=800
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/microsoft-edge.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true
[program:openbox]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/openbox --config-file /etc/neko/openbox.xml
autorestart=true
priority=300
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/openbox.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true

View File

@ -1,36 +0,0 @@
[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

View File

@ -1,31 +0,0 @@
ARG BASE_IMAGE=m1k1o/neko:base
FROM $BASE_IMAGE
ARG API_URL="https://download5.operacdn.com/pub/opera/desktop/"
ARG LIBFFMPEG_API_URL="https://api.github.com/repos/nwjs-ffmpeg-prebuilt/nwjs-ffmpeg-prebuilt/releases/latest"
#
# install opera
RUN set -eux; apt-get update; \
#
# fetch latest release
VERSION="$(wget -O - "${API_URL}" 2>/dev/null | sed -n 's/.*href="\([^"/]*\).*/\1/p' | sort --version-sort | tail -1)"; \
wget -O /tmp/opera.deb "${API_URL}${VERSION}/linux/opera-stable_${VERSION}_amd64.deb"; \
apt-get install -y --no-install-recommends openbox jq unzip /tmp/opera.deb; \
#
# install libffmpeg
LIBFFMPEG_URL="$(wget -O - "${LIBFFMPEG_API_URL}" 2>/dev/null | jq -r "[.assets[] | select(.browser_download_url | contains(\"linux-x64.zip\"))][-1] | .browser_download_url")"; \
wget -O /tmp/libffmpeg.zip $LIBFFMPEG_URL; \
unzip -o /tmp/libffmpeg.zip libffmpeg.so -d /usr/lib/x86_64-linux-gnu/opera/lib_extra; \
echo '[]' > /usr/lib/x86_64-linux-gnu/opera/resources/ffmpeg_preload_config.json; \
#
# clean up
apt-get --purge autoremove -y unzip jq; \
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# copy configuation files
COPY supervisord.conf /etc/neko/supervisord/opera.conf
COPY openbox.xml /etc/neko/openbox.xml

View File

@ -17,9 +17,5 @@ fi
docker run --rm -it \
-v "${PWD}/../server:/src" \
-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
--entrypoint="go" \
neko_dev_server build -o "bin/neko" "cmd/neko/main.go"

View File

@ -1,20 +0,0 @@
ARG BASE_IMAGE=m1k1o/neko:base
FROM $BASE_IMAGE
# install remmina
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends \
remmina-plugin-rdp remmina-plugin-vnc \
# remmina-plugin-x2go # not in bullseye
remmina-plugin-spice remmina-plugin-nx; \
# clean up
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
# copy configuation files
COPY supervisord.conf /etc/neko/supervisord/remmina.conf
COPY --chown=neko remmina.pref /home/neko/.config/remmina/remmina.pref
COPY --chown=neko rdp.remmina spice.remmina vnc.remmina /home/neko/.local/share/remmina/
COPY run-remmina.sh /usr/bin/run-remmina.sh
ENV REMMINA_URL=
ENV REMMINA_PROFILE=

View File

@ -1,90 +0,0 @@
[remmina]
password=
gateway_username=
notes_text=
vc=
preferipv6=0
ssh_tunnel_loopback=0
serialname=
sound=local
printer_overrides=
name=rdpdefault
console=0
colordepth=99
security=
precommand=
disable_fastpath=0
left-handed=0
postcommand=
multitransport=0
group=
server=
ssh_tunnel_certfile=
glyph-cache=0
ssh_tunnel_enabled=0
disableclipboard=0
audio-output=
parallelpath=
monitorids=
cert_ignore=0
gateway_server=
serialpermissive=0
protocol=RDP
old-license=0
ssh_tunnel_password=
resolution_mode=2
pth=
loadbalanceinfo=
disableautoreconnect=0
clientname=
clientbuild=
resolution_width=0
drive=
relax-order-checks=0
username=
base-cred-for-gw=0
gateway_domain=
network=none
rdp2tcp=
gateway_password=
serialdriver=
domain=
profile-lock=0
rdp_reconnect_attempts=
restricted-admin=0
multimon=0
exec=
smartcardname=
serialpath=
enable-autostart=0
usb=
shareprinter=0
ssh_tunnel_passphrase=
shareparallel=0
disablepasswordstoring=0
quality=0
span=0
parallelname=
ssh_tunnel_auth=0
keymap=
ssh_tunnel_username=
execpath=
shareserial=0
resolution_height=0
timeout=
useproxyenv=0
sharesmartcard=0
freerdp_log_filters=
microphone=
dvc=
ssh_tunnel_privatekey=
gwtransp=http
ssh_tunnel_server=
ignore-tls-errors=1
disable-smooth-scrolling=0
gateway_usage=0
websockets=0
freerdp_log_level=INFO
window_maximize=1
scale=1
viewmode=4

View File

@ -1,103 +0,0 @@
[remmina_pref]
secret=Jsh7BJPwHqLCKi2vdkMAImSgdBVZGF6s8VbUPY3Q9WA=
datadir_path=
remmina_file_name=%G_%P_%N_%h
screenshot_path=
screenshot_name=
deny_screenshot_clipboard=true
save_view_mode=true
use_master_password=false
unlock_timeout=0
unlock_password=
trust_all=true
floating_toolbar_placement=0
toolbar_placement=3
prevent_snap_welcome_message=true
last_quickconnect_protocol=
fullscreen_on_auto=true
always_show_tab=false
hide_connection_toolbar=true
hide_searchbar=true
default_action=0
scale_quality=3
ssh_loglevel=1
ssh_parseconfig=true
hide_toolbar=true
small_toolbutton=true
view_file_mode=0
resolutions=640x480,800x600,1024x768,1152x864,1280x960,1400x1050
keystrokes=
main_width=600
main_height=400
main_maximize=true
main_sort_column_id=1
main_sort_order=0
expanded_group=
toolbar_pin_down=false
sshtunnel_port=4732
ssh_tcp_keepidle=20
ssh_tcp_keepintvl=10
ssh_tcp_keepcnt=3
ssh_tcp_usrtimeout=60000
applet_new_ontop=false
applet_hide_count=true
applet_enable_avahi=false
disable_tray_icon=true
dark_tray_icon=false
recent_maximum=10
default_mode=3
tab_mode=0
fullscreen_toolbar_visibility=2
auto_scroll_step=10
hostkey=65508
shortcutkey_fullscreen=
shortcutkey_autofit=
shortcutkey_nexttab=
shortcutkey_prevtab=
shortcutkey_scale=
shortcutkey_grab=65508
shortcutkey_viewonly=109
shortcutkey_screenshot=
shortcutkey_minimize=65478
shortcutkey_disconnect=65473
shortcutkey_toolbar=116
vte_font=
vte_allow_bold_text=true
vte_lines=512
[ssh_colors]
background=#d5ccba
cursor=#45373c
bold=#45373c
foreground=#45373c
color0=#20111b
color1=#be100e
color2=#858162
color3=#eaa549
color4=#426a79
color5=#97522c
color6=#989a9c
color7=#968c83
color8=#5e5252
color9=#be100e
color10=#858162
color11=#eaa549
color12=#426a79
color13=#97522c
color14=#989a9c
color15=#d5ccba
[usage_stats]
periodic_usage_stats_permitted=false
periodic_usage_stats_last_sent=0
periodic_usage_stats_uuid_prefix=
[remmina_news]
periodic_news_permitted=false
periodic_rmnews_last_get=0
periodic_rmnews_get_count=1
periodic_rmnews_uuid_prefix=
[remmina]
name=
ignore-tls-errors=1

View File

@ -1,38 +0,0 @@
#!/bin/bash
set -u
err() {
echo "ERROR: $*" >&2
exit 1
}
profile_dir="/home/neko/.local/share/remmina"
if [[ -n "$REMMINA_PROFILE" ]]; then
profile=${REMMINA_PROFILE%.remmina}.remmina
file=${profile##/*/}
[[ "$file" = "$profile" ]] && profile="$profile_dir"/"$file"
[[ -f "$profile" ]] || err "Connection profile $profile not found"
echo "Running remmina with connection profile $profile"
exec remmina -c "$profile"
fi
[[ -z "$REMMINA_URL" ]] && err "Neither 'REMMINA_PROFILE' nor 'REMMINA_URL' found in env vars"
readarray -t arr < <( echo -n "$REMMINA_URL" | perl -pe 's|^(\w+)\:\/\/(?:([^:]+)(?::([^@]+))?@)?(.*)$|\1\n\2\n\3\n\4|' )
proto="${arr[0]}"
user="${arr[1]}"
pw="${arr[2]}"
host="${arr[3]}"
echo "Parsed url in 'REMMINA_URL': proto:$proto username:$user host:$host"
[[ "$proto" != "vnc" && "$proto" != "rdp" && "$proto" != "spice" ]] && err "Unsupported protocol $proto in connection url 'REMMINA_URL'"
profile="$profile_dir"/"$proto".remmina
remmina --set-option username="$user" --update-profile "$profile"
remmina --set-option password="$pw" --update-profile "$profile"
remmina --set-option server="$host" --update-profile "$profile"
# remmina --set-option window_maximize=1 --update-profile "$profile"
# remmina --set-option scale=1 --update-profile "$profile"
exec remmina -c "$profile"

View File

@ -1,33 +0,0 @@
[remmina]
disablegstvideooverlay=0
disablepasswordstoring=0
sharesmartcard=0
videocodec=0
ssh_tunnel_password=
postcommand=
server=
name=spicedefault
ssh_tunnel_enabled=0
profile-lock=0
enable-autostart=0
imagecompression=0
password=
precommand=
disableclipboard=0
group=
ssh_tunnel_certfile=
protocol=SPICE
enableaudio=1
viewonly=0
ssh_tunnel_server=
ssh_tunnel_loopback=0
ssh_tunnel_auth=0
ignore-tls-errors=1
ssh_tunnel_username=
ssh_tunnel_passphrase=
ssh_tunnel_privatekey=
notes_text=
usetls=0
window_maximize=1
scale=1
viewmode=4

View File

@ -1,12 +0,0 @@
[program:remmina]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/run-remmina.sh
stopsignal=INT
autorestart=true
priority=500
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/remmina.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true

View File

@ -1,49 +0,0 @@
[remmina]
encodings=
videocodec=0
name=temp eos
ssh_tunnel_server=
ssh_tunnel_privatekey=
password=
quality=0
disablesmoothscrolling=0
enableaudio=1
precommand=
disablegstvideooverlay=0
ssh_tunnel_enabled=0
sharesmartcard=0
imagecompression=0
ssh_tunnel_passphrase=
ssh_tunnel_password=
usetls=0
viewonly=0
disableserverinput=0
depth_profile=0
postcommand=
tightencoding=0
disablepasswordstoring=0
lossy_encoding=0
ignore-tls-errors=1
gvncdebug=0
ssh_tunnel_username=
server=
disableclipboard=0
disableserverbell=0
profile-lock=0
enable-autostart=0
window_maximize=1
scale=1
disableencryption=0
ssh_tunnel_auth=0
group=
ssh_tunnel_loopback=0
showcursor=1
viewmode=4
notes_text=
keymap=
colordepth=32
proxy=
protocol=VNC
ssh_tunnel_certfile=
shared=0
username=

View File

@ -16,7 +16,7 @@ if [ ! -d "${PWD}/../client/node_modules" ] || [ "$1" == "-i" ]; then
-v "${PWD}/../client:/app" \
--workdir="/app" \
--entrypoint="npm" \
node:18-bullseye-slim install
node:14-buster-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:18-bullseye-slim run serve
node:14-buster-slim run serve

View File

@ -17,22 +17,8 @@ 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" \

View File

@ -11,7 +11,6 @@ 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; \
#

View File

@ -5,7 +5,6 @@ ARG API_URL="https://api.github.com/repos/macchrome/linchrome/releases/latest"
#
# install custom chromium build from woolyss with support for hevc/x265
SHELL ["/bin/bash", "-c"]
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends wget unzip libatk1.0-0 libatk-bridge2.0-0 libatomic1 \
libcups2 libgtk-3-0 libnss3 libpci3 libxcomposite1 libxss1 openbox xz-utils jq; \
@ -21,28 +20,12 @@ RUN set -eux; apt-get update; \
chmod 4755 /usr/lib/chromium/chrome-sandbox; \
#
# install widevine module
WIDEVINE_VERSION=$(wget --quiet -O - https://dl.google.com/widevine-cdm/versions.txt | sort --version-sort | tail -n 1); \
wget -O /tmp/widevine.zip "https://dl.google.com/widevine-cdm/${WIDEVINE_VERSION}-linux-x64.zip"; \
WIDEVINE_VERSION=$(wget --quiet -O - https://dl.google.com/widevine-cdm/versions.txt | tail -n 1); \
wget -O /tmp/widevine.zip "https://dl.google.com/widevine-cdm/$WIDEVINE_VERSION-linux-x64.zip"; \
unzip -p /tmp/widevine.zip libwidevinecdm.so > /usr/lib/chromium/libwidevinecdm.so; \
chmod 644 /usr/lib/chromium/libwidevinecdm.so; \
rm /tmp/widevine.zip; \
#
# install latest version of uBlock Origin and SponsorBlock for YouTube
CHROMIUM_VERSION="$(wget -O - "${API_URL}" 2>/dev/null | jq -r ".tag_name" | sed -e 's/v//' -e 's/-.*//')"; \
EXTENSIONS_DIR="/usr/share/chromium/extensions"; \
EXTENSIONS=( \
cjpalhdlnbpafiamejdnhcphjbkeiagm \
mnjggcdmjocbbbhaepdhchncahnbgone \
); \
mkdir -p "${EXTENSIONS_DIR}"; \
for EXT_ID in "${EXTENSIONS[@]}"; \
do \
EXT_URL="https://clients2.google.com/service/update2/crx?response=redirect&nacl_arch=x86-64&prodversion=${CHROMIUM_VERSION}&acceptformat=crx2,crx3&x=id%3D${EXT_ID}%26installsource%3Dondemand%26uc"; \
EXT_PATH="${EXTENSIONS_DIR}/${EXT_ID}.crx"; \
wget -O "${EXT_PATH}" "${EXT_URL}"; \
EXT_VERSION="$(unzip -p "${EXT_PATH}" manifest.json 2>/dev/null | jq -r ".version")"; \
echo -e "{\n \"external_crx\": \"${EXT_PATH}\",\n \"external_version\": \"${EXT_VERSION}\"\n}" > "${EXTENSIONS_DIR}"/"${EXT_ID}".json; \
done; \
# clean up
apt-get --purge autoremove -y xz-utils jq; \
apt-get clean -y; \
@ -54,3 +37,7 @@ COPY supervisord.conf /etc/neko/supervisord/ungoogled-chromium.conf
COPY preferences.json /usr/lib/chromium/master_preferences
COPY policies.json /etc/chromium/policies/managed/policies.json
COPY openbox.xml /etc/neko/openbox.xml
#
# copy extensions and policy files
COPY extensions /usr/share/chromium/extensions

View File

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

View File

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

View File

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

View File

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

View File

@ -1,42 +0,0 @@
ARG BASE_IMAGE=m1k1o/neko:base
FROM $BASE_IMAGE
ARG VIVALDI_VERSION="5.3.2679.34-1"
# TODO: Get chromium version from vivaldi
ARG CHROMIUM_VERSION="102.0.5005.72"
#
# install vivaldi
SHELL ["/bin/bash", "-c"]
RUN set -eux; apt-get update; \
wget -O /tmp/vivaldi.deb "https://downloads.vivaldi.com/stable/vivaldi-stable_${VIVALDI_VERSION}_amd64.deb"; \
apt-get install -y --no-install-recommends wget unzip xz-utils jq openbox /tmp/vivaldi.deb; \
/opt/vivaldi/update-ffmpeg; \
#
# install latest version of uBlock Origin and SponsorBlock for YouTube
EXTENSIONS_DIR="/usr/share/chromium/extensions"; \
EXTENSIONS=( \
cjpalhdlnbpafiamejdnhcphjbkeiagm \
mnjggcdmjocbbbhaepdhchncahnbgone \
); \
mkdir -p "${EXTENSIONS_DIR}"; \
for EXT_ID in "${EXTENSIONS[@]}"; \
do \
EXT_URL="https://clients2.google.com/service/update2/crx?response=redirect&nacl_arch=x86-64&prodversion=${CHROMIUM_VERSION}&acceptformat=crx2,crx3&x=id%3D${EXT_ID}%26installsource%3Dondemand%26uc"; \
EXT_PATH="${EXTENSIONS_DIR}/${EXT_ID}.crx"; \
wget -O "${EXT_PATH}" "${EXT_URL}"; \
EXT_VERSION="$(unzip -p "${EXT_PATH}" manifest.json 2>/dev/null | jq -r ".version")"; \
echo -e "{\n \"external_crx\": \"${EXT_PATH}\",\n \"external_version\": \"${EXT_VERSION}\"\n}" > "${EXTENSIONS_DIR}"/"${EXT_ID}".json; \
done; \
#
# clean up
apt-get --purge autoremove -y xz-utils jq; \
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# copy configuation files
COPY supervisord.conf /etc/neko/supervisord/vivaldi-browser.conf
COPY --chown=neko preferences.json /home/neko/.config/vivaldi/Default/Preferences
COPY policies.json /etc/opt/vivaldi/policies/managed/policies.json
COPY openbox.xml /etc/neko/openbox.xml

View File

@ -1,763 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Default openbox config but all window decorations are moved
thereby making it harder to accidentally close the virtual browser -->
<openbox_config xmlns="http://openbox.org/3.4/rc"
xmlns:xi="http://www.w3.org/2001/XInclude">
<resistance>
<strength>10</strength>
<screen_edge_strength>20</screen_edge_strength>
</resistance>
<applications>
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
<application class="Vivaldi-stable*" name="vivaldi-stable*" role="browser">
<decor>no</decor>
<maximized>true</maximized>
<focus>yes</focus>
<layer>normal</layer>
</application>
</applications>
<focus>
<focusNew>yes</focusNew>
<!-- always try to focus new windows when they appear. other rules do
apply -->
<followMouse>no</followMouse>
<!-- move focus to a window when you move the mouse into it -->
<focusLast>yes</focusLast>
<!-- focus the last used window when changing desktops, instead of the one
under the mouse pointer. when followMouse is enabled -->
<underMouse>no</underMouse>
<!-- move focus under the mouse, even when the mouse is not moving -->
<focusDelay>200</focusDelay>
<!-- when followMouse is enabled, the mouse must be inside the window for
this many milliseconds (1000 = 1 sec) before moving focus to it -->
<raiseOnFocus>no</raiseOnFocus>
<!-- when followMouse is enabled, and a window is given focus by moving the
mouse into it, also raise the window -->
</focus>
<placement>
<policy>Smart</policy>
<!-- 'Smart' or 'UnderMouse' -->
<center>yes</center>
<!-- whether to place windows in the center of the free area found or
the top left corner -->
<monitor>Primary</monitor>
<!-- with Smart placement on a multi-monitor system, try to place new windows
on: 'Any' - any monitor, 'Mouse' - where the mouse is, 'Active' - where
the active window is, 'Primary' - only on the primary monitor -->
<primaryMonitor>1</primaryMonitor>
<!-- The monitor where Openbox should place popup dialogs such as the
focus cycling popup, or the desktop switch popup. It can be an index
from 1, specifying a particular monitor. Or it can be one of the
following: 'Mouse' - where the mouse is, or
'Active' - where the active window is -->
</placement>
<theme>
<name>Clearlooks</name>
<titleLayout>NLIMC</titleLayout>
<!--
available characters are NDSLIMC, each can occur at most once.
N: window icon
L: window label (AKA title).
I: iconify
M: maximize
C: close
S: shade (roll up/down)
D: omnipresent (on all desktops).
-->
<keepBorder>yes</keepBorder>
<animateIconify>yes</animateIconify>
<font place="ActiveWindow">
<name>sans</name>
<size>8</size>
<!-- font size in points -->
<weight>bold</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="InactiveWindow">
<name>sans</name>
<size>8</size>
<!-- font size in points -->
<weight>bold</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="MenuHeader">
<name>sans</name>
<size>9</size>
<!-- font size in points -->
<weight>normal</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="MenuItem">
<name>sans</name>
<size>9</size>
<!-- font size in points -->
<weight>normal</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="ActiveOnScreenDisplay">
<name>sans</name>
<size>9</size>
<!-- font size in points -->
<weight>bold</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="InactiveOnScreenDisplay">
<name>sans</name>
<size>9</size>
<!-- font size in points -->
<weight>bold</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
</theme>
<desktops>
<!-- this stuff is only used at startup, pagers allow you to change them
during a session
these are default values to use when other ones are not already set
by other applications, or saved in your session
use obconf if you want to change these without having to log out
and back in -->
<number>1</number>
<firstdesk>1</firstdesk>
<names>
<!-- set names up here if you want to, like this:
<name>desktop 1</name>
<name>desktop 2</name>
-->
</names>
<popupTime>875</popupTime>
<!-- The number of milliseconds to show the popup for when switching
desktops. Set this to 0 to disable the popup. -->
</desktops>
<resize>
<drawContents>yes</drawContents>
<popupShow>Nonpixel</popupShow>
<!-- 'Always', 'Never', or 'Nonpixel' (xterms and such) -->
<popupPosition>Center</popupPosition>
<!-- 'Center', 'Top', or 'Fixed' -->
<popupFixedPosition>
<!-- these are used if popupPosition is set to 'Fixed' -->
<x>10</x>
<!-- positive number for distance from left edge, negative number for
distance from right edge, or 'Center' -->
<y>10</y>
<!-- positive number for distance from top edge, negative number for
distance from bottom edge, or 'Center' -->
</popupFixedPosition>
</resize>
<!-- You can reserve a portion of your screen where windows will not cover when
they are maximized, or when they are initially placed.
Many programs reserve space automatically, but you can use this in other
cases. -->
<margins>
<top>0</top>
<bottom>0</bottom>
<left>0</left>
<right>0</right>
</margins>
<dock>
<position>TopLeft</position>
<!-- (Top|Bottom)(Left|Right|)|Top|Bottom|Left|Right|Floating -->
<floatingX>0</floatingX>
<floatingY>0</floatingY>
<noStrut>no</noStrut>
<stacking>Above</stacking>
<!-- 'Above', 'Normal', or 'Below' -->
<direction>Vertical</direction>
<!-- 'Vertical' or 'Horizontal' -->
<autoHide>no</autoHide>
<hideDelay>300</hideDelay>
<!-- in milliseconds (1000 = 1 second) -->
<showDelay>300</showDelay>
<!-- in milliseconds (1000 = 1 second) -->
<moveButton>Middle</moveButton>
<!-- 'Left', 'Middle', 'Right' -->
</dock>
<keyboard>
<chainQuitKey>C-g</chainQuitKey>
<!-- Keybindings for desktop switching -->
<keybind key="C-A-Left">
<action name="GoToDesktop"><to>left</to><wrap>no</wrap></action>
</keybind>
<keybind key="C-A-Right">
<action name="GoToDesktop"><to>right</to><wrap>no</wrap></action>
</keybind>
<keybind key="C-A-Up">
<action name="GoToDesktop"><to>up</to><wrap>no</wrap></action>
</keybind>
<keybind key="C-A-Down">
<action name="GoToDesktop"><to>down</to><wrap>no</wrap></action>
</keybind>
<keybind key="S-A-Left">
<action name="SendToDesktop"><to>left</to><wrap>no</wrap></action>
</keybind>
<keybind key="S-A-Right">
<action name="SendToDesktop"><to>right</to><wrap>no</wrap></action>
</keybind>
<keybind key="S-A-Up">
<action name="SendToDesktop"><to>up</to><wrap>no</wrap></action>
</keybind>
<keybind key="S-A-Down">
<action name="SendToDesktop"><to>down</to><wrap>no</wrap></action>
</keybind>
<keybind key="W-F1">
<action name="GoToDesktop"><to>1</to></action>
</keybind>
<keybind key="W-F2">
<action name="GoToDesktop"><to>2</to></action>
</keybind>
<keybind key="W-F3">
<action name="GoToDesktop"><to>3</to></action>
</keybind>
<keybind key="W-F4">
<action name="GoToDesktop"><to>4</to></action>
</keybind>
<keybind key="W-d">
<action name="ToggleShowDesktop"/>
</keybind>
<!-- Keybindings for windows -->
<keybind key="A-F4">
<action name="Close"/>
</keybind>
<keybind key="A-Escape">
<action name="Lower"/>
<action name="FocusToBottom"/>
<action name="Unfocus"/>
</keybind>
<keybind key="A-space">
<!--action name="ShowMenu"><menu>client-menu</menu></action-->
</keybind>
<!-- Take a screenshot of the current window with scrot when Alt+Print are pressed -->
<keybind key="A-Print">
<action name="Execute"><command>scrot -s</command></action>
</keybind>
<!-- Keybindings for window switching -->
<keybind key="A-Tab">
<action name="NextWindow">
<finalactions>
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</finalactions>
</action>
</keybind>
<keybind key="A-S-Tab">
<action name="PreviousWindow">
<finalactions>
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</finalactions>
</action>
</keybind>
<keybind key="C-A-Tab">
<action name="NextWindow">
<panels>yes</panels><desktop>yes</desktop>
<finalactions>
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</finalactions>
</action>
</keybind>
<!-- Keybindings for window switching with the arrow keys -->
<keybind key="W-S-Right">
<action name="DirectionalCycleWindows">
<direction>right</direction>
</action>
</keybind>
<keybind key="W-S-Left">
<action name="DirectionalCycleWindows">
<direction>left</direction>
</action>
</keybind>
<keybind key="W-S-Up">
<action name="DirectionalCycleWindows">
<direction>up</direction>
</action>
</keybind>
<keybind key="W-S-Down">
<action name="DirectionalCycleWindows">
<direction>down</direction>
</action>
</keybind>
<!-- Keybindings for running applications -->
<keybind key="W-e">
<action name="Execute">
<startupnotify>
<enabled>true</enabled>
<name>Konqueror</name>
</startupnotify>
<command>kfmclient openProfile filemanagement</command>
</action>
</keybind>
<!-- Launch scrot when Print is pressed -->
<keybind key="Print">
<action name="Execute"><command>scrot</command></action>
</keybind>
</keyboard>
<mouse>
<dragThreshold>1</dragThreshold>
<!-- number of pixels the mouse must move before a drag begins -->
<doubleClickTime>500</doubleClickTime>
<!-- in milliseconds (1000 = 1 second) -->
<screenEdgeWarpTime>400</screenEdgeWarpTime>
<!-- Time before changing desktops when the pointer touches the edge of the
screen while moving a window, in milliseconds (1000 = 1 second).
Set this to 0 to disable warping -->
<screenEdgeWarpMouse>false</screenEdgeWarpMouse>
<!-- Set this to TRUE to move the mouse pointer across the desktop when
switching due to hitting the edge of the screen -->
<context name="Frame">
<mousebind button="A-Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="A-Left" action="Click">
<action name="Unshade"/>
</mousebind>
<mousebind button="A-Left" action="Drag">
<action name="Move"/>
</mousebind>
<mousebind button="A-Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="A-Right" action="Drag">
<action name="Resize"/>
</mousebind>
<mousebind button="A-Middle" action="Press">
<action name="Lower"/>
<action name="FocusToBottom"/>
<action name="Unfocus"/>
</mousebind>
<mousebind button="A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="C-A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="C-A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="A-S-Up" action="Click">
<action name="SendToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="A-S-Down" action="Click">
<action name="SendToDesktop"><to>next</to></action>
</mousebind>
</context>
<context name="Titlebar">
<mousebind button="Left" action="Drag">
<action name="Move"/>
</mousebind>
<mousebind button="Left" action="DoubleClick">
<action name="ToggleMaximize"/>
</mousebind>
<mousebind button="Up" action="Click">
<action name="if">
<shaded>no</shaded>
<then>
<action name="Shade"/>
<action name="FocusToBottom"/>
<action name="Unfocus"/>
<action name="Lower"/>
</then>
</action>
</mousebind>
<mousebind button="Down" action="Click">
<action name="if">
<shaded>yes</shaded>
<then>
<action name="Unshade"/>
<action name="Raise"/>
</then>
</action>
</mousebind>
</context>
<context name="Titlebar Top Right Bottom Left TLCorner TRCorner BRCorner BLCorner">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Middle" action="Press">
<action name="Lower"/>
<action name="FocusToBottom"/>
<action name="Unfocus"/>
</mousebind>
<!--mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="ShowMenu"><menu>client-menu</menu></action>
</mousebind-->
</context>
<context name="Top">
<mousebind button="Left" action="Drag">
<action name="Resize"><edge>top</edge></action>
</mousebind>
</context>
<context name="Left">
<mousebind button="Left" action="Drag">
<action name="Resize"><edge>left</edge></action>
</mousebind>
</context>
<context name="Right">
<mousebind button="Left" action="Drag">
<action name="Resize"><edge>right</edge></action>
</mousebind>
</context>
<context name="Bottom">
<mousebind button="Left" action="Drag">
<action name="Resize"><edge>bottom</edge></action>
</mousebind>
<!--mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="ShowMenu"><menu>client-menu</menu></action>
</mousebind-->
</context>
<context name="TRCorner BRCorner TLCorner BLCorner">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Left" action="Drag">
<action name="Resize"/>
</mousebind>
</context>
<context name="Client">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Middle" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
</context>
<context name="Icon">
<!--mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
<action name="ShowMenu"><menu>client-menu</menu></action>
</mousebind>
<mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="ShowMenu"><menu>client-menu</menu></action>
</mousebind-->
</context>
<context name="AllDesktops">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="ToggleOmnipresent"/>
</mousebind>
</context>
<context name="Shade">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="ToggleShade"/>
</mousebind>
</context>
<context name="Iconify">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="Iconify"/>
</mousebind>
</context>
<context name="Maximize">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Middle" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="ToggleMaximize"/>
</mousebind>
<mousebind button="Middle" action="Click">
<action name="ToggleMaximize"><direction>vertical</direction></action>
</mousebind>
<mousebind button="Right" action="Click">
<action name="ToggleMaximize"><direction>horizontal</direction></action>
</mousebind>
</context>
<context name="Close">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="Close"/>
</mousebind>
</context>
<context name="Desktop">
<mousebind button="Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="C-A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="C-A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
</context>
<context name="Root">
<!-- Menus -->
<!--mousebind button="Middle" action="Press">
<action name="ShowMenu"><menu>client-list-combined-menu</menu></action>
</mousebind>
<mousebind button="Right" action="Press">
<action name="ShowMenu"><menu>root-menu</menu></action>
</mousebind-->
</context>
<context name="MoveResize">
<mousebind button="Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
</context>
</mouse>
<menu>
<!-- You can specify more than one menu file in here and they are all loaded,
just don't make menu ids clash or, well, it'll be kind of pointless -->
<!-- default menu file (or custom one in $HOME/.config/openbox/) -->
<!-- system menu files on Debian systems -->
<!--file>/var/lib/openbox/debian-menu.xml</file-->
<file>menu.xml</file>
<hideDelay>200</hideDelay>
<!-- if a press-release lasts longer than this setting (in milliseconds), the
menu is hidden again -->
<middle>no</middle>
<!-- center submenus vertically about the parent entry -->
<submenuShowDelay>100</submenuShowDelay>
<!-- time to delay before showing a submenu after hovering over the parent
entry.
if this is a negative value, then the delay is infinite and the
submenu will not be shown until it is clicked on -->
<submenuHideDelay>400</submenuHideDelay>
<!-- time to delay before hiding a submenu when selecting another
entry in parent menu
if this is a negative value, then the delay is infinite and the
submenu will not be hidden until a different submenu is opened -->
<showIcons>yes</showIcons>
<!-- controls if icons appear in the client-list-(combined-)menu -->
<manageDesktops>yes</manageDesktops>
<!-- show the manage desktops section in the client-list-(combined-)menu -->
</menu>
<applications>
<!--
# this is an example with comments through out. use these to make your
# own rules, but without the comments of course.
# you may use one or more of the name/class/role/title/type rules to specify
# windows to match
<application name="the window's _OB_APP_NAME property (see obxprop)"
class="the window's _OB_APP_CLASS property (see obxprop)"
groupname="the window's _OB_APP_GROUP_NAME property (see obxprop)"
groupclass="the window's _OB_APP_GROUP_CLASS property (see obxprop)"
role="the window's _OB_APP_ROLE property (see obxprop)"
title="the window's _OB_APP_TITLE property (see obxprop)"
type="the window's _OB_APP_TYPE property (see obxprob)..
(if unspecified, then it is 'dialog' for child windows)">
# you may set only one of name/class/role/title/type, or you may use more
# than one together to restrict your matches.
# the name, class, role, and title use simple wildcard matching such as those
# used by a shell. you can use * to match any characters and ? to match
# any single character.
# the type is one of: normal, dialog, splash, utility, menu, toolbar, dock,
# or desktop
# when multiple rules match a window, they will all be applied, in the
# order that they appear in this list
# each rule element can be left out or set to 'default' to specify to not
# change that attribute of the window
<decor>yes</decor>
# enable or disable window decorations
<shade>no</shade>
# make the window shaded when it appears, or not
<position force="no">
# the position is only used if both an x and y coordinate are provided
# (and not set to 'default')
# when force is "yes", then the window will be placed here even if it
# says you want it placed elsewhere. this is to override buggy
# applications who refuse to behave
<x>center</x>
# a number like 50, or 'center' to center on screen. use a negative number
# to start from the right (or bottom for <y>), ie -50 is 50 pixels from
# the right edge (or bottom). use 'default' to specify using value
# provided by the application, or chosen by openbox, instead.
<y>200</y>
<monitor>1</monitor>
# specifies the monitor in a xinerama setup.
# 1 is the first head, or 'mouse' for wherever the mouse is
</position>
<size>
# the size to make the window.
<width>20</width>
# a number like 20, or 'default' to use the size given by the application.
# you can use fractions such as 1/2 or percentages such as 75% in which
# case the value is relative to the size of the monitor that the window
# appears on.
<height>30%</height>
</size>
<focus>yes</focus>
# if the window should try be given focus when it appears. if this is set
# to yes it doesn't guarantee the window will be given focus. some
# restrictions may apply, but Openbox will try to
<desktop>1</desktop>
# 1 is the first desktop, 'all' for all desktops
<layer>normal</layer>
# 'above', 'normal', or 'below'
<iconic>no</iconic>
# make the window iconified when it appears, or not
<skip_pager>no</skip_pager>
# asks to not be shown in pagers
<skip_taskbar>no</skip_taskbar>
# asks to not be shown in taskbars. window cycling actions will also
# skip past such windows
<fullscreen>yes</fullscreen>
# make the window in fullscreen mode when it appears
<maximized>true</maximized>
# 'Horizontal', 'Vertical' or boolean (yes/no)
</application>
# end of the example
-->
</applications>
</openbox_config>

View File

@ -1,39 +0,0 @@
{
"AutofillAddressEnabled": false,
"AutofillCreditCardEnabled": false,
"BrowserSignin": 0,
"DefaultNotificationsSetting": 2,
"DeveloperToolsAvailability": 2,
"EditBookmarksEnabled": false,
"FullscreenAllowed": true,
"IncognitoModeAvailability": 1,
"SyncDisabled": true,
"AutoplayAllowed": true,
"BrowserAddPersonEnabled": false,
"BrowserGuestModeEnabled": false,
"DefaultPopupsSetting": 2,
"DownloadRestrictions": 3,
"VideoCaptureAllowed": true,
"AllowFileSelectionDialogs": false,
"PromptForDownloadLocation": false,
"BookmarkBarEnabled": false,
"PasswordManagerEnabled": false,
"URLAllowlist": [
"file:///home/neko/Downloads"
],
"URLBlocklist": [
"file://*",
"chrome://policy"
],
"ExtensionInstallForcelist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx",
"mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx"
],
"ExtensionInstallAllowlist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm",
"mnjggcdmjocbbbhaepdhchncahnbgone"
],
"ExtensionInstallBlocklist": [
"*"
]
}

View File

@ -1,339 +0,0 @@
{
"homepage": "http://www.google.com",
"homepage_is_newtabpage": false,
"first_run_tabs": [
"https://www.google.com/_/chrome/newtab?ie=UTF-8"
],
"custom_links": {
"initialized": true,
"list": [
{
"title": "YouTube",
"url": "https://www.youtube.com/"
},
{
"title": "Netflix",
"url": "https://netflix.com"
},
{
"title": "Hulu",
"url": "https://www.hulu.com/"
},
{
"title": "9Anime",
"url": "https://9anime.to/"
},
{
"title": "Crunchy Roll",
"url": "https://www.crunchyroll.com/"
},
{
"title": "Funimation",
"url": "https://www.funimation.com/"
},
{
"title": "Disney+",
"url": "https://www.disneyplus.com/"
},
{
"title": "HBO Now",
"url": "https://play.hbonow.com/"
},
{
"title": "Amazon Video",
"url": "https://www.amazon.com/Amazon-Video/b?node=2858778011"
},
{
"title": "VRV",
"url": "https://vrv.co/"
},
{
"title": "Twitch",
"url": "https://www.twitch.tv/"
},
{
"title": "Mixer",
"url": "https://mixer.com/"
}
]
},
"browser": {
"custom_chrome_frame": false,
"show_home_button": true,
"window_placement": {
"maximized": true
}
},
"bookmark_bar": {
"show_on_all_tabs": false
},
"sync_promo": {
"show_on_first_run_allowed": false
},
"default_search_provider_data": {
"image_template_url_data": {
"alternate_urls": ["https://www.google.com/#q={searchTerms}", "https://www.google.com/search#q={searchTerms}", "https://www.google.com/webhp#q={searchTerms}", "https://www.google.com/s#q={searchTerms}", "https://www.google.com/s?q={searchTerms}"],
"contextual_search_url": "",
"created_by_policy": false,
"created_from_play_api": false,
"date_created": "0",
"doodle_url": "",
"favicon_url": "https://www.google.com/favicon.ico",
"id": "11",
"image_url": "https://www.google.com/searchbyimage/upload",
"image_url_post_params": "encoded_image={google:imageThumbnail},image_url={google:imageURL},sbisrc={google:imageSearchSource},original_width={google:imageOriginalWidth},original_height={google:imageOriginalHeight}",
"input_encodings": ["UTF-8"],
"is_active": 0,
"keyword": "g",
"last_modified": "0",
"last_visited": "0",
"logo_url": "",
"new_tab_url": "",
"originating_url": "",
"position": "IhxEc1dKb3ZJanppM1NCWTZhRHNGN0ljYkRHeVk9",
"preconnect_to_search_url": false,
"prepopulate_id": 1,
"safe_for_autoreplace": true,
"search_url_post_params": "",
"short_name": "Google",
"side_search_param": "",
"suggestions_url": "",
"suggestions_url_post_params": "",
"url": "https://www.google.com/search?q={searchTerms}&{google:originalQueryForSuggestion}{google:prefetchSource}{google:sourceId}{google:contextualSearchVersion}ie={inputEncoding}",
"usage_count": 0
},
"private_template_url_data": {
"alternate_urls": [],
"contextual_search_url": "",
"created_by_policy": false,
"created_from_play_api": false,
"date_created": "0",
"doodle_url": "",
"favicon_url": "https://duckduckgo.com/favicon.ico",
"id": "4",
"image_url": "",
"image_url_post_params": "",
"input_encodings": ["UTF-8"],
"is_active": 0,
"keyword": "d",
"last_modified": "0",
"last_visited": "0",
"logo_url": "",
"new_tab_url": "",
"originating_url": "",
"position": "Ih3bMkFheVB5WitEdGZmeU9hV0d5K3RGUXRhR3RFPQ==",
"preconnect_to_search_url": false,
"prepopulate_id": 7,
"safe_for_autoreplace": true,
"search_url_post_params": "",
"short_name": "DuckDuckGo",
"side_search_param": "",
"suggestions_url": "https://duckduckgo.com/ac/?q={searchTerms}&type=list",
"suggestions_url_post_params": "",
"url": "https://duckduckgo.com/?q={searchTerms}&{ddg:Referral}",
"usage_count": 0
},
"speeddials_template_url_data": {
"alternate_urls": ["https://www.google.com/#q={searchTerms}", "https://www.google.com/search#q={searchTerms}", "https://www.google.com/webhp#q={searchTerms}", "https://www.google.com/s#q={searchTerms}", "https://www.google.com/s?q={searchTerms}"],
"contextual_search_url": "",
"created_by_policy": false,
"created_from_play_api": false,
"date_created": "0",
"doodle_url": "",
"favicon_url": "https://www.google.com/favicon.ico",
"id": "11",
"image_url": "https://www.google.com/searchbyimage/upload",
"image_url_post_params": "encoded_image={google:imageThumbnail},image_url={google:imageURL},sbisrc={google:imageSearchSource},original_width={google:imageOriginalWidth},original_height={google:imageOriginalHeight}",
"input_encodings": ["UTF-8"],
"is_active": 0,
"keyword": "g",
"last_modified": "0",
"last_visited": "0",
"logo_url": "",
"new_tab_url": "",
"originating_url": "",
"position": "IhxEc1dKb3ZJanppM1NCWTZhRHNGN0ljYkRHeVk9",
"preconnect_to_search_url": false,
"prepopulate_id": 1,
"safe_for_autoreplace": true,
"search_url_post_params": "",
"short_name": "Google",
"side_search_param": "",
"suggestions_url": "",
"suggestions_url_post_params": "",
"url": "https://www.google.com/search?q={searchTerms}&{google:originalQueryForSuggestion}{google:prefetchSource}{google:sourceId}{google:contextualSearchVersion}ie={inputEncoding}",
"usage_count": 0
},
"template_url_data": {
"alternate_urls": ["https://www.google.com/#q={searchTerms}", "https://www.google.com/search#q={searchTerms}", "https://www.google.com/webhp#q={searchTerms}", "https://www.google.com/s#q={searchTerms}", "https://www.google.com/s?q={searchTerms}"],
"contextual_search_url": "",
"created_by_policy": false,
"created_from_play_api": false,
"date_created": "0",
"doodle_url": "",
"favicon_url": "https://www.google.com/favicon.ico",
"id": "11",
"image_url": "https://www.google.com/searchbyimage/upload",
"image_url_post_params": "encoded_image={google:imageThumbnail},image_url={google:imageURL},sbisrc={google:imageSearchSource},original_width={google:imageOriginalWidth},original_height={google:imageOriginalHeight}",
"input_encodings": ["UTF-8"],
"is_active": 0,
"keyword": "g",
"last_modified": "0",
"last_visited": "0",
"logo_url": "",
"new_tab_url": "",
"originating_url": "",
"position": "IhxEc1dKb3ZJanppM1NCWTZhRHNGN0ljYkRHeVk9",
"preconnect_to_search_url": false,
"prepopulate_id": 1,
"safe_for_autoreplace": true,
"search_url_post_params": "",
"short_name": "Google",
"side_search_param": "",
"suggestions_url": "",
"suggestions_url_post_params": "",
"url": "https://www.google.com/search?q={searchTerms}&{google:originalQueryForSuggestion}{google:prefetchSource}{google:sourceId}{google:contextualSearchVersion}ie={inputEncoding}",
"usage_count": 0
}
},
"distribution": {
"import_bookmarks_from_file": "bookmarks.html",
"import_bookmarks": true,
"import_history": true,
"import_home_page": true,
"import_search_engine": true,
"ping_delay": 60,
"do_not_create_desktop_shortcut": true,
"do_not_create_quick_launch_shortcut": true,
"do_not_create_taskbar_shortcut": true,
"do_not_launch_chrome": true,
"do_not_register_for_update_launch": true,
"make_chrome_default": true,
"make_chrome_default_for_user": true,
"system_level": false,
"verbose_logging": false
},
"enable_do_not_track": true,
"profile": {
"avatar_index": 34,
"default_content_setting_values": {
"clipboard": 2,
"cookies": 4,
"geolocation": 2,
"media_stream_camera": 2,
"media_stream_mic": 2,
"midi_sysex": 2,
"payment_handler": 2,
"usb_guard": 2
},
"name": "Neko",
"using_default_avatar": false,
"using_default_name": false,
"using_gaia_avatar": false
},
"signin": {
"allowed": false
},
"vivaldi": {
"address_bar": {
"autocomplete": {
"enabled": false
},
"omnibox": {
"show_browser_history": false,
"show_search_history": false,
"show_typed_history": false
},
"search": {
"display": 1,
"in_new_tab": false
},
"show_full_url": true,
"show_qr_generator": true,
"visible": true
},
"bookmarks": {
"deleted_partners": [
"f79cd6e8-ebc0-444d-ac96-00da456dcb59",
"d680347f-1073-46b9-a546-ae0238e7b9d9"
],
"language": "en-US",
"version": "24"
},
"downloads": {
"notify_on_complete": false,
"open_panel_on_new": false,
"start_automatically": false,
"update_default_download_when_saving_as": false
},
"history": {
"days_to_keep_visits": 0
},
"homepage": "vivaldi://startpage",
"incognito": {
"show_intro": false
},
"language_at_install": "en-US",
"menu": {
"icon_type": 1
},
"mouse_gestures": {
"enabled": false,
"rocker_gestures": {
"enabled": false
}
},
"popups": {
"show_in_tab": true
},
"settings": {
"in_tab": true
},
"startpage": {
"navigation": 1,
"speed_dial": {
"add_button_visible": false,
"allow_dnd": false,
"columns": 4,
"delete_visible": false,
"display_search": true,
"privacy_stats_show": false,
"tracker_suggestion_show": false,
"width": 170
}
},
"startup": {
"check_is_default": false,
"has_seen_feature": 1
},
"status_bar": {
"display": 0,
"minimized": 0
},
"system": {
"show_exit_confirmation_dialog": true
},
"tabs": {
"new_placement": 3,
"open_new_in_background": false,
"stacking": {
"open_accordions": []
}
},
"theme": {
"schedule": {
"o_s": {
"dark": "Vivaldi2",
"light": "Vivaldi2"
}
},
"use_animation": false
},
"translate": {
"enabled": false
},
"windows": {
"use_native_decoration": true
}
}
}

View File

@ -1,22 +0,0 @@
[program:vivaldi-stable]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/vivaldi-stable --no-sandbox --window-position=0,0 --display=%(ENV_DISPLAY)s --user-data-dir=/home/neko/.config/vivaldi --no-first-run --start-maximized --bwsi --force-dark-mode --disable-file-system --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage
stopsignal=INT
autorestart=true
priority=800
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/vivaldi.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true
[program:openbox]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/openbox --config-file /etc/neko/openbox.xml
autorestart=true
priority=300
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/openbox.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true

View File

@ -0,0 +1,18 @@
ARG BASE_IMAGE=m1k1o/neko:base
FROM $BASE_IMAGE
#
# install vncviewer
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends openbox xtightvncviewer; \
#
# clean up
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
ENV NEKO_VNC_URL=""
#
# copy configuation files
COPY supervisord.conf /etc/neko/supervisord/vncviewer.conf
COPY openbox.xml /etc/neko/openbox.xml

View File

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

View File

@ -1,11 +1,11 @@
[program:opera]
[program:vncviewer]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/opera --no-sandbox --no-first-run --start-maximized --force-dark-mode --disable-gpu
command=/usr/bin/vncviewer -autopass -x11cursor -nojpeg -quality 9 -compresslevel 9 %(ENV_NEKO_VNC_URL)s
stopsignal=INT
autorestart=true
priority=800
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/opera.log
stdout_logfile=/var/log/neko/vncviewer.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true

1
.github/FUNDING.yml vendored
View File

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

View File

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

View File

@ -1,125 +0,0 @@
name: "amd64 images"
on:
push:
tags:
- 'v*'
env:
REGISTRY: ghcr.io
IMAGE_NAME: m1k1o/neko
TAG_PREFIX: ""
BASE_DOCKERFILE: Dockerfile
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 }}
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 }}

View File

@ -1,161 +0,0 @@
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-client:
runs-on: ubuntu-latest
#
# do not run on forks
#
if: github.repository_owner == 'm1k1o'
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up node
uses: actions/setup-node@v3
with:
node-version: 18.x
-
name: Build client
run: |
cd client
npm install
npm run build
-
name: Upload client dist
uses: actions/upload-artifact@v3
with:
name: client-dist
path: client/dist
build-base:
runs-on: ubuntu-latest
#
# do not run on forks
#
if: github.repository_owner == 'm1k1o'
needs: [ build-client ]
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Download client dist
uses: actions/download-artifact@v3
with:
name: client-dist
path: client/dist
-
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 }}

View File

@ -1,126 +0,0 @@
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 }}

View File

@ -1,124 +0,0 @@
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
View File

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

View File

@ -186,9 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (C) 2020 Nurdism <nurdism.io@gmail.com>
Copyright (C) 2020-2023 m1k1o
All Rights Reserved.
Copyright 2020 Nurdism <nurdism.io@gmail.com>, 2020-2021 m1k1o
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

117
README.md
View File

@ -1,127 +1,40 @@
<div align="center">
<a href="https://github.com/m1k1o/neko" title="Neko's Github repository.">
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/logo.png" width="400" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/logo.png" width="450" height="auto"/>
</a>
<p align="center">
<a href="https://github.com/m1k1o/neko/releases">
<img src="https://img.shields.io/github/v/release/m1k1o/neko" alt="release">
</a>
<a href="https://github.com/m1k1o/neko/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/m1k1o/neko" alt="license">
</a>
<a href="https://hub.docker.com/u/m1k1o/neko">
<img src="https://img.shields.io/docker/pulls/m1k1o/neko" alt="pulls">
</a>
<a href="https://github.com/m1k1o/neko/issues">
<img src="https://img.shields.io/github/issues/m1k1o/neko" alt="issues">
</a>
<a href="https://github.com/sponsors/m1k1o">
<img src="https://img.shields.io/badge/-sponsor-red" alt="issues">
</a>
<a href="https://discord.gg/3U6hWpC">
<img src="https://discordapp.com/api/guilds/665851821906067466/widget.png" alt="Chat on discord">
</a>
<a href="https://github.com/m1k1o/neko/actions">
<img src="https://github.com/m1k1o/neko/actions/workflows/ghcr-amd.yml/badge.svg" alt="build">
<img src="https://github.com/m1k1o/neko/actions/workflows/build.yml/badge.svg" alt="build">
</a>
</p>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/intro.gif" width="650" height="auto"/>
<br/>
<br/>
<img src="https://i.imgur.com/ZSzbQr7.gif" width="650" height="auto"/>
<br/>
<br/>
</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 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.
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, 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/) or [hyperbeam](https://watch.hyperbeam.com).
- **Interactive presentation** - not only screen sharing, but others can control the screen.
- **Collaborative tool** - brainstorming ideas, cobrowsing, code debugging together.
- **Support/Teaching** - interactively guiding people in controlled environment.
- **Embed anything** - embed virtual browser in your web app - open source alternative to [hyperbeam API](https://hyperbeam.com/).
- open any third-party website or application, synchronize audio and video flawlessly among multiple participants.
- request rooms using API with [neko-rooms](https://github.com/m1k1o/neko-rooms).
Other use cases that benefit from single-user:
- **Personal workspace** - streaming containerized apps and desktops to end-users - similar to [kasm](https://www.kasmweb.com/).
- **Persistent browser** - own browser with persistent cookies available anywhere - similar to [mightyapp](https://www.mightyapp.com/).
- no state is left on the host browser after terminating the connection.
- sensitive data like cookies are not transferred - only video is shared.
- **Throwaway browser** - a better solution for planning secret parties and buying birthday gifts off the internet.
- use Tor Browser and [VPN](https://github.com/m1k1o/neko-vpn) for additional anonymity.
- mitigates risk of OS fingerprinting and browser vulnerabilities by running in container.
- **Session broadcasting** - broadcast room content using RTMP (to e.g. twitch or youtube...).
- **Session recording** - broadcast RTMP can be saved to a file using e.g. [nginx-rtmp](https://www.nginx.com/products/nginx/modules/rtmp-media-streaming/)
- have clean environment when recording tutorials.
- no need to hide bookmarks or use incognito mode.
- **Jump host** - access your internal applications securely without the need for VPN.
- **Automated browser** - you can install [playwright](https://playwright.dev/) or [puppeteer](https://pptr.dev/) and automate tasks while being able to actively intercept them.
Compared to clientless remote desktop gateway (e.g. [Apache Guacamole](https://guacamole.apache.org/) or [websockify](https://github.com/novnc/websockify) with [noVNC](https://novnc.com/)), installed with remote desktop server along with desired program (e.g. [linuxserver/firefox](https://docs.linuxserver.io/images/docker-firefox)) provides neko additionally:
- **Smooth video** because it uses WebRTC and not images sent over WebSockets.
- **Built in audio** support, what is not part of Apache Guacamole or noVNC.
- **Multi-participant control**, what is not natively supported by Apache Guacamole or noVNC.
### Supported browsers
<div align="center">
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/firefox.svg" title="m1k1o/neko:firefox" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/google-chrome.svg" title="m1k1o/neko:google-chrome" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/chromium.svg" title="m1k1o/neko:chromium" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/microsoft-edge.svg" title="m1k1o/neko:microsoft-edge" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/brave.svg" title="m1k1o/neko:brave" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/vivaldi.svg" title="m1k1o/neko:vivaldi" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/opera.svg" title="m1k1o/neko:opera" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/tor-browser.svg" title="m1k1o/neko:tor-browser" width="60" height="auto"/>
</div>
### Other programs
<div align="center">
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/remmina.png" title="m1k1o/neko:remmina" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/vlc.svg" title="m1k1o/neko:vlc" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/xfce.svg" title="m1k1o/neko:xfce" width="60" height="auto"/>
<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>
Then I found [this](https://github.com/nurdism/neko) project and started to dig into it. I really liked the idea of having collaborative browser browsing together with mutliple people, so I created a fork. Initially, I wanted to merge my changes to the upstream repository, but the original author did not have time for this project anymore and it got eventually archived.
### Features
* Text Chat (With basic markdown support, discord flavor)
* Admin users (Kick, Ban & Force Give/Release Controls, Lock room)
* Admin users (Kick, Ban & Force Give/Release Controls)
* Clipboard synchronization (on [supported browsers](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText))
* Emote overlay
* Ignore user (chat and emotes)
* Persistent settings
* Automatic Login with custom url args. (add `?usr=<your-user-name>&pwd=<room-pass>` to the url.)
* Broadcasting room content using RTMP (to e.g. twitch or youtube...)
* Bidirectional file transfer (if enabled)
<div align="center">
With `NEKO_FILE_TRANSFER_ENABLED=true`:
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/file-transfer.gif" width="650" height="auto"/>
</div>
### Why n.eko?
@ -129,13 +42,13 @@ I like cats 🐱 (`Neko` is the Japanese word for cat), I'm a weeb/nerd.
***But why the cat butt?*** Because cats are *assholes*, but you love them anyways.
## Multiple rooms
# Multiple rooms
For n.eko room management software, visit [neko-rooms](https://github.com/m1k1o/neko-rooms).
It also offers zero-knowledge [installation script (with HTTPS and Traefik)](https://github.com/m1k1o/neko-rooms/#zero-knowledge-installation-with-https-and-traefik).
It also offers zero-knowledge [installation script](https://github.com/m1k1o/neko-rooms/#zero-knowledge-installation).
## Documentation
# Documentation
* [Getting Started](https://neko.m1k1o.net/#/getting-started/)
* [Quick Start](https://neko.m1k1o.net/#/getting-started/quick-start)
@ -149,10 +62,6 @@ It also offers zero-knowledge [installation script (with HTTPS and Traefik)](htt
* [Technologies](https://neko.m1k1o.net/#/technologies)
* [Changelog](https://neko.m1k1o.net/#/changelog)
## How to contribute? How to build?
# How to contribute? How to build?
Navigate to [.docker](.docker) folder for further information.
## Support
If you want to support this project, you can do it [here](https://github.com/sponsors/m1k1o).

21271
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,22 +1,17 @@
<template>
<div id="neko" :class="[!videoOnly && side ? 'expanded' : '']">
<div id="neko" :class="[side ? 'expanded' : '']">
<template v-if="!$client.supported">
<neko-unsupported />
</template>
<template v-else>
<main class="neko-main">
<div v-if="!videoOnly" class="header-container">
<div v-if="!hideControls" class="header-container">
<neko-header />
</div>
<div class="video-container">
<neko-video
ref="video"
:hideControls="hideControls"
:extraControls="isEmbedMode"
@control-attempt="controlAttempt"
/>
<neko-video ref="video" :hideControls="hideControls" @control-attempt="controlAttempt" />
</div>
<div v-if="!videoOnly" class="room-container">
<div v-if="!hideControls" class="room-container">
<neko-members />
<div class="room-menu">
<div class="settings">
@ -31,11 +26,11 @@
</div>
</div>
</main>
<neko-side v-if="!videoOnly && side" />
<neko-side v-if="!hideControls && side" />
<neko-connect v-if="!connected" />
<neko-about v-if="about" />
<notifications
v-if="!videoOnly"
v-if="!hideControls"
group="neko"
position="top left"
style="top: 50px; pointer-events: none"
@ -181,41 +176,15 @@
shakeKbd = false
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() {
get hideControls() {
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) {
if (new URL(location.href).searchParams.has('volume')) {
this.$accessor.video.setVolume(volume)
}
}
@Watch('hideControls', { immediate: true })
onHideControls(enabled: boolean) {
if (enabled) {
onHideControls() {
this.$accessor.video.setMuted(false)
this.$accessor.settings.setSound(false)
}
}
controlAttempt() {
if (this.shakeKbd || this.$accessor.remote.hosted) return

View File

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

View File

@ -1,16 +1,44 @@
@import '~sweetalert2/src/variables';
$swal2-white: #fff;
$swal2-black: #000;
$swal2-outline-color: transparent;
// POPUP
// CONTAINER
$swal2-container-padding: .625em;
// BOX MODEL
$swal2-width: 32em;
$swal2-padding: 1.25em;
$swal2-border: none;
$swal2-border-radius: .3125em;
$swal2-box-shadow: #d9d9d9;
// ANIMATIONS
$swal2-show-animation: swal2-show .3s;
$swal2-hide-animation: swal2-hide .15s forwards;
// BACKGROUND
$swal2-background: $background-secondary;
// TYPOGRAPHY
$swal2-font: inherit;
$swal2-font-size: 1rem;
// BACKDROP
$swal2-backdrop: rgba($swal2-black, .4);
$swal2-backdrop-transition: background-color .1s;
// ICONS
$swal2-icon-size: 5em;
$swal2-icon-animations: true;
$swal2-icon-margin: 1.25em auto 1.875em;
$swal2-icon-zoom: null;
$swal2-success: #a5dc86;
$swal2-success-border: rgba($swal2-success, .3);
$swal2-error: #f27474;
$swal2-warning: #f8bb86;
$swal2-info: #3fc3ee;
$swal2-question: #87adbd;
$swal2-icon-font-family: inherit;
// IMAGE
$swal2-image-margin: 1.25em auto;
@ -18,53 +46,143 @@ $swal2-image-margin: 1.25em auto;
// TITLE
$swal2-title-margin: 0 0 .4em;
$swal2-title-color: $interactive-hover;
$swal2-title-font-size: 1.875em;
// HTML CONTAINER
$swal2-html-container-margin: 0;
$swal2-html-container-color: $interactive-hover;
// CONTENT
$swal2-content-justify-content: center;
$swal2-content-margin: 0;
$swal2-content-pading: 0;
$swal2-content-color: $interactive-hover;
$swal2-content-font-size: 1.125em;
$swal2-content-font-weight: normal;
$swal2-content-line-height: normal;
$swal2-content-text-align: center;
$swal2-content-word-wrap: break-word;
// INPUT
$swal2-input-margin: 1em auto;
$swal2-input-width: 100%;
$swal2-input-height: 2.625em;
$swal2-input-padding: 0 .75em;
$swal2-input-border: 1px solid lighten($swal2-black, 85);
$swal2-input-border-radius: .1875em;
$swal2-input-box-shadow: inset 0 1px 1px rgba($swal2-black, .06);
$swal2-input-focus-border: 1px solid #b4dbed;
$swal2-input-focus-outline: none;
$swal2-input-focus-box-shadow: 0 0 3px #c4e6f5;
$swal2-input-font-size: 1.125em;
$swal2-input-background: inherit;
$swal2-input-color: inherit;
$swal2-input-transition: border-color .3s, box-shadow .3s;
// TEXTAREA SPECIFIC VARIABLES
$swal2-textarea-height: 6.75em;
$swal2-textarea-padding: .75em;
// VALIDATION MESSAGE
$swal2-validation-message-justify-content: center;
$swal2-validation-message-padding: .625em;
$swal2-validation-message-background: lighten($swal2-black, 94);
$swal2-validation-message-color: lighten($swal2-black, 40);
$swal2-validation-message-font-size: 1em;
$swal2-validation-message-font-weight: 300;
$swal2-validation-message-icon-background: $swal2-error;
$swal2-validation-message-icon-color: $swal2-white;
$swal2-validation-message-icon-zoom: null;
// PROGRESS STEPS
$swal2-progress-steps-background: inherit;
$swal2-progress-steps-margin: 0 0 1.25em;
$swal2-progress-steps-padding: 0;
$swal2-progress-steps-font-weight: 600;
$swal2-progress-steps-distance: 2.5em;
$swal2-progress-step-width: 2em;
$swal2-progress-step-height: 2em;
$swal2-progress-step-border-radius: 2em;
$swal2-progress-step-background: #add8e6;
$swal2-progress-step-color: $swal2-white;
$swal2-active-step-background: #3085d6;
$swal2-active-step-color: $swal2-white;
// FOOTER
$swal2-footer-margin: 1.25em 0 0;
$swal2-footer-padding: 1em 0 0;
$swal2-footer-border-color: #eee;
$swal2-footer-color: lighten($swal2-black, 33);
$swal2-footer-font-size: 1em;
// TIMER POGRESS BAR
$swal2-timer-progress-bar-height: .25em;
$swal2-timer-progress-bar-background: rgba($swal2-black, .2);
// CLOSE BUTTON
$swal2-close-button-width: 1.2em;
$swal2-close-button-height: 1.2em;
$swal2-close-button-line-height: 1.2;
$swal2-close-button-position: absolute;
$swal2-close-button-gap: 0;
$swal2-close-button-transition: color .1s ease-out;
$swal2-close-button-border: none;
$swal2-close-button-border-radius: 0;
$swal2-close-button-outline: initial;
$swal2-close-button-background: transparent;
$swal2-close-button-color: lighten($swal2-black, 80);
$swal2-close-button-font-family: serif;
$swal2-close-button-font-size: 2.5em;
// CLOSE BUTTON:HOVER
$swal2-close-button-hover-transform: none;
$swal2-close-button-hover-color: $swal2-error;
$swal2-close-button-hover-background: transparent;
// ACTIONS
$swal2-actions-flex-wrap: wrap;
$swal2-actions-align-items: center;
$swal2-actions-justify-content: center;
$swal2-actions-width: 100%;
// COMMON VARIABLES FOR CONFIRM AND CANCEL BUTTONS
$swal2-button-focus-box-shadow: 0 0 0 1px $swal2-background, 0 0 0 3px $swal2-outline-color;
$swal2-actions-margin: 1.25em auto 0;
// CONFIRM BUTTON
$swal2-confirm-button-border: 0;
$swal2-confirm-button-border-radius: .25em;
$swal2-confirm-button-background-color: $background-tertiary;
$swal2-confirm-button-color: $swal2-white;
$swal2-confirm-button-font-size: 1.0625em;
$swal2-confirm-button-focus-box-shadow: 0 0 0 3px rgba($swal2-confirm-button-background-color, .5);
// CANCEL BUTTON
$swal2-cancel-button-border: 0;
$swal2-cancel-button-border-radius: .25em;
$swal2-cancel-button-background-color: $background-floating;
$swal2-cancel-button-color: $swal2-white;
$swal2-cancel-button-font-size: 1.0625em;
$swal2-cancel-button-focus-box-shadow: 0 0 0 3px rgba($swal2-cancel-button-background-color, .5);
// COMMON VARIABLES FOR CONFIRM AND CANCEL BUTTONS
$swal2-button-darken-hover: rgba($swal2-black, .1);
$swal2-button-darken-active: rgba($swal2-black, .2);
$swal2-button-focus-outline: none;
$swal2-button-focus-background-color: null;
$swal2-button-focus-box-shadow: 0 0 0 1px $swal2-background, 0 0 0 3px $swal2-outline-color;
// TOASTS
$swal2-toast-show-animation: swal2-toast-show .5s;
$swal2-toast-hide-animation: swal2-toast-hide .1s forwards;
$swal2-toast-border: none;
$swal2-toast-box-shadow: 0 0 .625em #d9d9d9;
$swal2-toast-background: $swal2-white;
$swal2-toast-close-button-width: .8em;
$swal2-toast-close-button-height: .8em;
$swal2-toast-close-button-line-height: .8;
$swal2-toast-width: auto;
$swal2-toast-padding: .625em;
$swal2-toast-title-margin: 0 .6em;
$swal2-toast-title-font-size: 1em;
$swal2-toast-content-font-size: 1em;
$swal2-toast-input-font-size: 1em;
$swal2-toast-validation-font-size: 1em;
$swal2-toast-buttons-font-size: 1em;
$swal2-toast-button-focus-box-shadow: 0 0 0 1px $swal2-background, 0 0 0 3px $swal2-outline-color;
$swal2-toast-footer-margin: .5em 0 0;
$swal2-toast-footer-padding: .5em 0 0;
$swal2-toast-footer-font-size: .8em;
@import "~sweetalert2/src/sweetalert2.scss";

View File

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

View File

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

View File

@ -18,20 +18,20 @@
<span @click="mute(child.data.member)" v-if="!child.data.member.muted">{{ $t('context.mute') }}</span>
<span @click="unmute(child.data.member)" v-else>{{ $t('context.unmute') }}</span>
</li>
<li v-if="child.data.member.id === host && !implicitHosting">
<li v-if="child.data.member.id === host">
<span @click="adminRelease(child.data.member)">{{ $t('context.release') }}</span>
</li>
<li v-if="child.data.member.id === host && !implicitHosting">
<li v-if="child.data.member.id === host">
<span @click="adminControl(child.data.member)">{{ $t('context.take') }}</span>
</li>
<li>
<span v-if="child.data.member.id !== host && !implicitHosting" @click="adminGive(child.data.member)">{{
<span v-if="child.data.member.id !== host" @click="adminGive(child.data.member)">{{
$t('context.give')
}}</span>
</li>
</template>
<template v-else>
<li v-if="hosting && !implicitHosting">
<li v-if="hosting">
<span @click="give(child.data.member)">{{ $t('context.give') }}</span>
</li>
</template>
@ -132,7 +132,7 @@
</style>
<script lang="ts">
import { Component, Ref, Vue } from 'vue-property-decorator'
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
import { Member } from '~/neko/types'
// @ts-ignore
@ -161,10 +161,6 @@
return this.$accessor.remote.id
}
get implicitHosting() {
return this.$accessor.remote.implicitHosting
}
open(event: MouseEvent, data: any) {
this.context.open(event, data)
}
@ -229,11 +225,11 @@
}
}
adminRelease() {
adminRelease(member: Member) {
this.$accessor.remote.adminRelease()
}
adminControl() {
adminControl(member: Member) {
this.$accessor.remote.adminControl()
}

View File

@ -1,6 +1,6 @@
<template>
<ul>
<li v-if="!implicitHosting && (!controlLocked || hosting)">
<li v-if="seesControl">
<i
:class="[
!disabeld && shakeKbd ? 'shake' : '',
@ -20,19 +20,7 @@
@click.stop.prevent="toggleControl"
/>
</li>
<li class="no-pointer" v-if="implicitHosting">
<i
:class="[controlLocked ? 'disabled' : '', 'fas', 'fa-mouse-pointer']"
v-tooltip="{
content: controlLocked ? $t('controls.hasnot') : $t('controls.has'),
placement: 'top',
offset: 5,
boundariesElement: 'body',
delay: { show: 300, hide: 100 },
}"
/>
</li>
<li v-if="implicitHosting || (!implicitHosting && (!controlLocked || hosting))">
<li v-if="seesControl">
<label
class="switch"
v-tooltip="{
@ -43,7 +31,7 @@
delay: { show: 300, hide: 100 },
}"
>
<input type="checkbox" v-model="locked" :disabled="!hosting || (implicitHosting && controlLocked)" />
<input type="checkbox" v-model="locked" :disabled="!hosting" />
<span />
</label>
</li>
@ -117,10 +105,6 @@
font-size: 24px;
cursor: pointer;
&.no-pointer {
cursor: default;
}
i {
padding: 0 5px;
@ -212,7 +196,7 @@
&:before {
color: $background-tertiary;
font-weight: 900;
font-family: 'Font Awesome 6 Free';
font-family: 'Font Awesome 5 Free';
content: '\f3c1';
font-size: 8px;
line-height: 18px;
@ -258,8 +242,12 @@
export default class extends Vue {
@Prop(Boolean) readonly shakeKbd!: boolean
get controlLocked() {
return 'control' in this.$accessor.locked && this.$accessor.locked['control'] && !this.$accessor.user.admin
get severLocked(): boolean {
return 'control' in this.$accessor.locked && this.$accessor.locked['control']
}
get seesControl(): boolean {
return !this.severLocked || this.$accessor.user.admin || this.hosting
}
get disabeld() {
@ -270,10 +258,6 @@
return this.$accessor.remote.hosting
}
get implicitHosting() {
return this.$accessor.remote.implicitHosting
}
get volume() {
return this.$accessor.video.volume
}

View File

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

View File

@ -1,520 +0,0 @@
<template>
<div class="files">
<div class="files-cwd">
<p>{{ cwd }}</p>
<i class="fas fa-rotate-right refresh" @click="refresh" />
</div>
<div class="files-list">
<div v-for="item in files" :key="item.name" class="files-list-item">
<i :class="fileIcon(item)" />
<p class="file-name" :title="item.name">{{ item.name }}</p>
<p class="file-size">{{ fileSize(item.size) }}</p>
<i v-if="item.type !== 'dir'" class="fas fa-download download" @click="download(item)" />
</div>
</div>
<div class="transfer-area">
<div class="transfers" v-if="transfers.length > 0">
<p v-if="downloads.length > 0" class="transfers-list-header">
<span>{{ $t('files.downloads') }}</span>
<i class="fas fa-xmark remove-transfer" @click="downloads.forEach((t) => removeTransfer(t))"></i>
</p>
<div v-for="download in downloads" :key="download.id" class="transfers-list-item">
<div class="transfer-info">
<i
class="fas transfer-status"
:class="{
'fa-clock': download.status === 'pending',
'fa-arrows-rotate': download.status === 'inprogress',
'fa-check': download.status === 'completed',
'fa-warning': download.status === 'failed',
}"
></i>
<p class="file-name" :title="download.name">{{ download.name }}</p>
<p class="file-size">{{ Math.min(100, Math.round((download.progress / download.size) * 100)) }}%</p>
<i class="fas fa-xmark remove-transfer" @click="removeTransfer(download)"></i>
</div>
<div v-if="download.status === 'failed'" class="transfer-error">{{ download.error }}</div>
<progress
v-else
class="transfer-progress"
:aria-label="download.name + ' progress'"
:value="download.progress"
:max="download.size"
></progress>
</div>
<p v-if="uploads.length > 0" class="transfers-list-header">
<span>{{ $t('files.uploads') }}</span>
<i class="fas fa-xmark remove-transfer" @click="uploads.forEach((t) => removeTransfer(t))"></i>
</p>
<div v-for="upload in uploads" :key="upload.id" class="transfers-list-item">
<div class="transfer-info">
<i
class="fas transfer-status"
:title="upload.status"
:class="{
'fa-clock': upload.status === 'pending',
'fa-arrows-rotate': upload.status === 'inprogress',
'fa-check': upload.status === 'completed',
'fa-warning': upload.status === 'failed',
}"
></i>
<p class="file-name" :title="upload.name">{{ upload.name }}</p>
<p class="file-size">{{ Math.min(100, Math.round((upload.progress / upload.size) * 100)) }}%</p>
<i class="fas fa-xmark remove-transfer" @click="removeTransfer(upload)"></i>
</div>
<div v-if="upload.status === 'failed'" class="transfer-error">{{ upload.error }}</div>
<progress
v-else
class="transfer-progress"
:aria-label="upload.name + ' progress'"
:value="upload.progress"
:max="upload.size"
></progress>
</div>
</div>
<div
class="upload-area"
:class="{ 'upload-area-drag': uploadAreaDrag }"
@dragover.prevent="uploadAreaDrag = true"
@dragleave.prevent="uploadAreaDrag = false"
@drop.prevent="(e) => upload(e.dataTransfer)"
@click="openFileBrowser"
>
<i class="fas fa-file-arrow-up" />
<p>{{ $t('files.upload_here') }}</p>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.files {
flex: 1;
flex-direction: column;
display: flex;
max-width: 100%;
.files-cwd {
display: flex;
flex-direction: row;
margin: 10px 10px 0px 10px;
padding: 0.5em;
font-weight: 600;
background-color: rgba($color: #fff, $alpha: 0.05);
border-radius: 5px;
}
.files-list {
margin: 10px 10px 10px 10px;
background-color: rgba($color: #fff, $alpha: 0.05);
border-radius: 5px;
overflow-y: scroll;
scrollbar-width: thin;
scrollbar-color: $background-tertiary transparent;
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: $background-tertiary;
border: 2px solid $background-primary;
border-radius: 4px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: $background-floating;
}
}
.files-list-item {
padding: 0.5em;
border-bottom: 2px solid rgba($color: #fff, $alpha: 0.1);
display: flex;
flex-direction: row;
line-height: 1.2;
}
.transfers-list-header {
display: flex;
justify-content: space-between;
border-bottom: 2px solid rgba($color: #fff, $alpha: 0.1);
}
.file-icon,
.transfer-status {
width: 14px;
margin-right: 0.5em;
}
.transfer-error {
border: 1px solid $style-error;
border-radius: 5px;
padding: 10px;
}
.files-list-item:last-child {
border-bottom: 0px;
}
.refresh {
margin-left: auto;
}
.file-name {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.file-size {
margin-left: auto;
margin-right: 0.5em;
color: rgba($color: #fff, $alpha: 0.4);
white-space: nowrap;
}
.refresh:hover,
.download:hover,
.remove-transfer:hover {
cursor: pointer;
}
.transfer-area {
margin-top: auto;
}
.transfers {
margin: 10px 10px 10px 10px;
background-color: rgba($color: #fff, $alpha: 0.05);
border-radius: 5px;
max-height: 50vh;
overflow-y: scroll;
overflow-x: hidden;
scrollbar-width: thin;
scrollbar-color: $background-tertiary transparent;
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: $background-tertiary;
border: 2px solid $background-primary;
border-radius: 4px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: $background-floating;
}
}
.transfers > p {
padding: 10px;
font-weight: 600;
}
.transfer-info {
display: flex;
flex-direction: row;
max-width: 100%;
padding: 10px;
}
.transfer-progress {
margin: 0px 10px 10px 10px;
width: 95%;
}
.upload-area {
display: flex;
flex-direction: column;
text-align: center;
justify-content: center;
margin: 10px 10px 10px 10px;
background-color: rgba($color: #fff, $alpha: 0.05);
border-radius: 5px;
}
.upload-area:hover {
cursor: pointer;
}
.upload-area-drag,
.upload-area:hover {
background-color: rgba($color: #fff, $alpha: 0.1);
}
.upload-area > i {
font-size: 4em;
margin: 10px 10px 10px 10px;
}
.upload-area > p {
margin: 0px 10px 10px 10px;
}
}
</style>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import Markdown from './markdown'
import Content from './context.vue'
import { FileTransfer, FileListItem } from '~/neko/types'
@Component({
name: 'neko-files',
components: {
'neko-markdown': Markdown,
'neko-context': Content,
},
})
export default class extends Vue {
public uploadAreaDrag: boolean = false
get cwd() {
return this.$accessor.files.cwd
}
get files() {
return this.$accessor.files.files
}
get transfers() {
return this.$accessor.files.transfers
}
get downloads() {
return this.$accessor.files.transfers.filter((t) => t.direction === 'download')
}
get uploads() {
return this.$accessor.files.transfers.filter((t) => t.direction === 'upload')
}
refresh() {
this.$accessor.files.refresh()
}
download(item: FileListItem) {
if (this.downloads.map((t) => t.name).includes(item.name)) {
return
}
const url =
'/file?pwd=' + encodeURIComponent(this.$accessor.password) + '&filename=' + encodeURIComponent(item.name)
const abortController = new AbortController()
let transfer: FileTransfer = {
id: Math.round(Math.random() * 10000),
name: item.name,
direction: 'download',
// this may be smaller than the actual transfer amount, but for large files the
// content length is not sent (chunked transfer)
size: item.size,
progress: 0,
status: 'pending',
abortController: abortController,
}
this.$http
.get(url, {
responseType: 'blob',
signal: abortController.signal,
withCredentials: false,
onDownloadProgress: (x) => {
transfer.progress = x.loaded
if (x.total && transfer.size !== x.total) {
transfer.size = x.total
}
if (transfer.progress === transfer.size) {
transfer.status = 'completed'
} else if (transfer.status !== 'inprogress') {
transfer.status = 'inprogress'
}
},
})
.then((res) => {
const url = window.URL.createObjectURL(new Blob([res.data]))
const link = document.createElement('a')
link.href = url
link.setAttribute('download', item.name)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
transfer.progress = transfer.size
transfer.status = 'completed'
})
.catch((error) => {
this.$log.error(error)
transfer.status = 'failed'
transfer.error = error.message
})
this.$accessor.files.addTransfer(transfer)
}
upload(dt: DataTransfer) {
const url = '/file?pwd=' + encodeURIComponent(this.$accessor.password)
this.uploadAreaDrag = false
for (const file of dt.files) {
const abortController = new AbortController()
const formdata = new FormData()
formdata.append('files', file, file.name)
let transfer: FileTransfer = {
id: Math.round(Math.random() * 10000),
name: file.name,
direction: 'upload',
size: file.size,
progress: 0,
status: 'pending',
abortController: abortController,
}
this.$http
.post(url, formdata, {
signal: abortController.signal,
withCredentials: false,
onUploadProgress: (x: any) => {
transfer.progress = x.loaded
if (transfer.size !== x.total) {
transfer.size = x.total
}
if (transfer.progress === transfer.size) {
transfer.status = 'completed'
} else if (transfer.status !== 'inprogress') {
transfer.status = 'inprogress'
}
},
})
.catch((error) => {
this.$log.error(error)
transfer.status = 'failed'
transfer.error = error.message
})
this.$accessor.files.addTransfer(transfer)
}
}
openFileBrowser() {
const input = document.createElement('input')
input.type = 'file'
input.setAttribute('multiple', 'true')
input.onchange = (e: Event) => {
if (e === null) return
const dt = new DataTransfer()
const target = e.target as HTMLInputElement
if (target.files === null) return
for (const f of target.files) {
dt.items.add(f)
}
this.upload(dt)
}
input.click()
}
removeTransfer(transfer: FileTransfer) {
if (transfer.status !== 'completed') {
transfer.abortController?.abort()
}
this.$accessor.files.removeTransfer(transfer)
}
fileIcon(file: FileListItem) {
let className = 'file-icon fas '
// if is directory
if (file.type === 'dir') {
className += 'fa-folder'
return className
}
// try to get file extension
const ext = file.name.split('.').pop()
if (ext === undefined) {
className += 'fa-file'
return className
}
// try to find icon
switch (ext.toLowerCase()) {
case 'txt':
case 'md':
className += 'fa-file-text'
break
case 'pdf':
className += 'fa-file-pdf'
break
case 'zip':
case 'rar':
case '7z':
case 'gz':
className += 'fa-archive'
break
case 'aac':
case 'flac':
case 'midi':
case 'mp3':
case 'ogg':
case 'wav':
className += 'fa-music'
break
case 'avi':
case 'mkv':
case 'mov':
case 'mpeg':
case 'mp4':
case 'webm':
className += 'fa-film'
break
case 'bmp':
case 'gif':
case 'jpeg':
case 'jpg':
case 'png':
case 'svg':
case 'tiff':
case 'webp':
className += 'fa-image'
break
default:
className += 'fa-file'
}
return className
}
fileSize(size: number) {
if (size < 1024) {
return size + ' B'
}
if (size < 1024 * 1024) {
return Math.round(size / 1024) + ' KB'
}
if (size < 1024 * 1024 * 1024) {
return Math.round(size / (1024 * 1024)) + ' MB'
}
if (size < 1024 * 1024 * 1024 * 1024) {
return Math.round(size / (1024 * 1024 * 1024)) + ' GB'
}
return Math.round(size / (1024 * 1024 * 1024 * 1024)) + ' TB'
}
}
</script>

View File

@ -31,19 +31,6 @@
}"
/>
</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">&bull;</span>
<i class="fas fa-bars toggle" @click="toggleMenu" />
@ -157,7 +144,7 @@
</style>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
import { AdminLockResource } from '~/neko/messages'
@Component({ name: 'neko-settings' })
@ -182,24 +169,26 @@
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`))

View File

@ -1,5 +1,5 @@
import md, { SingleNodeParserRule, HtmlOutputRule, defaultRules, State, Rules } from 'simple-markdown'
import { Component, Vue, Prop } from 'vue-property-decorator'
import { Component, Watch, Vue, Prop } from 'vue-property-decorator'
const { blockQuote, inlineCode, codeBlock, autolink, newline, escape, strong, text, link, url, em, u, br } =
defaultRules
@ -201,7 +201,7 @@ const rules: MarkdownRules = {
},
emoji: {
order: md.defaultRules.strong.order,
match: (source) => /^:([^:\s]+):/.exec(source),
match: (source) => /^:([a-zA-z_-]*):/.exec(source),
parse(capture) {
return {
id: capture[1],

View File

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

View File

@ -60,7 +60,7 @@
</style>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
import { messages } from '~/locale'
@Component({ name: 'neko-menu' })
@ -76,20 +76,5 @@
about() {
this.$accessor.client.toggleAbout()
}
mounted() {
const default_lang = new URL(location.href).searchParams.get('lang')
if (default_lang && this.langs.includes(default_lang)) {
this.$i18n.locale = default_lang
}
const show_side = new URL(location.href).searchParams.get('show_side')
if (show_side !== null) {
this.$accessor.client.setSide(show_side === '1')
}
const mute_chat = new URL(location.href).searchParams.get('mute_chat')
if (mute_chat !== null) {
this.$accessor.settings.setSound(mute_chat !== '1')
}
}
}
</script>

View File

@ -97,7 +97,7 @@
</style>
<script lang="ts">
import { Component, Ref, Vue } from 'vue-property-decorator'
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
import { ScreenResolution } from '~/neko/types'
// @ts-ignore
@ -110,7 +110,7 @@
},
})
export default class extends Vue {
@Ref('context') readonly context!: VueContext
@Ref('context') readonly context!: any
get width() {
return this.$accessor.video.width

View File

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

View File

@ -6,10 +6,6 @@
<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>
@ -18,7 +14,6 @@
</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>
@ -79,47 +74,23 @@
</style>
<script lang="ts">
import { Vue, Component, Watch } from 'vue-property-decorator'
import { Vue, Component } 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)
}

View File

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

View File

@ -8,11 +8,10 @@
<neko-emote :id="index" :key="index" />
</template>
</div>
<textarea
<div
ref="overlay"
class="overlay"
tabindex="0"
data-gramm="false"
@click.stop.prevent
@contextmenu.stop.prevent
@wheel.stop.prevent="onWheel"
@ -21,22 +20,19 @@
@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="playAndUnmute">
<div v-if="!playing && playable" class="player-overlay" @click.stop.prevent="toggle">
<i class="fas fa-play-circle" />
</div>
<div v-else-if="mutedOverlay && muted" class="player-overlay" @click.stop.prevent="unmute">
<div v-if="mutedOverlay && muted" class="player-overlay" @click.stop.prevent="unmute">
<i class="fas fa-volume-up" />
</div>
<div ref="aspect" class="player-aspect" />
</div>
<ul v-if="!fullscreen && !hideControls" class="video-menu top">
<li><i @click.stop.prevent="requestFullscreen" class="fas fa-expand"></i></li>
<li v-if="admin"><i @click.stop.prevent="openResolution" class="fas fa-desktop"></i></li>
<li v-if="!implicitHosting" :class="extraControls || 'extra-control'">
<li v-if="admin"><i @click.stop.prevent="onResolution" class="fas fa-desktop"></i></li>
<li class="request-control">
<i
:class="[hosted && !hosting ? 'disabled' : '', !hosted && !hosting ? 'faded' : '', 'fas', 'fa-keyboard']"
@click.stop.prevent="toggleControl"
@ -45,7 +41,7 @@
</ul>
<ul v-if="!fullscreen && !hideControls" class="video-menu bottom">
<li v-if="hosting && (!clipboard_read_available || !clipboard_write_available)">
<i @click.stop.prevent="openClipboard" class="fas fa-clipboard"></i>
<i @click.stop.prevent="onClipboard" class="fas fa-clipboard"></i>
</li>
<li>
<i
@ -72,7 +68,6 @@
display: flex;
justify-content: center;
align-items: center;
background: #000;
.video-menu {
position: absolute;
@ -109,12 +104,12 @@
}
}
/* usually extra controls are only shown on mobile */
&.extra-control {
&.request-control {
display: none;
}
@media (max-width: 768px) {
&.extra-control {
&.request-control {
display: inline-block;
}
}
@ -128,7 +123,7 @@
.player-container {
position: relative;
width: 100%;
max-width: calc(16 / 9 * 100vh);
max-width: 16 / 9 * 100vh;
video {
position: absolute;
@ -177,12 +172,6 @@
bottom: 0;
width: 100%;
height: 100%;
cursor: default;
outline: 0;
border: 0;
color: transparent;
background: transparent;
resize: none;
}
.player-aspect {
@ -197,7 +186,6 @@
<script lang="ts">
import { Component, Ref, Watch, Vue, Prop } from 'vue-property-decorator'
import ResizeObserver from 'resize-observer-polyfill'
import { elementRequestFullscreen, onFullscreenChange, isFullscreen, lockKeyboard, unlockKeyboard } from '~/utils'
import Emote from './emote.vue'
import Resolution from './resolution.vue'
@ -219,22 +207,20 @@
export default class extends Vue {
@Ref('component') readonly _component!: HTMLElement
@Ref('container') readonly _container!: HTMLElement
@Ref('overlay') readonly _overlay!: HTMLTextAreaElement
@Ref('overlay') readonly _overlay!: HTMLElement
@Ref('aspect') readonly _aspect!: HTMLElement
@Ref('player') readonly _player!: HTMLElement
@Ref('video') readonly _video!: HTMLVideoElement
@Ref('resolution') readonly _resolution!: Resolution
@Ref('clipboard') readonly _clipboard!: Clipboard
@Ref('resolution') readonly _resolution!: any
@Ref('clipboard') readonly _clipboard!: any
// 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 observer = new ResizeObserver(this.onResise.bind(this))
private focused = false
private fullscreen = false
private startsMuted = true
private mutedOverlay = true
get admin() {
@ -253,10 +239,6 @@
return this.$accessor.remote.hosting
}
get implicitHosting() {
return this.$accessor.remote.implicitHosting
}
get hosted() {
return this.$accessor.remote.hosted
}
@ -289,13 +271,8 @@
return this.$accessor.settings.autoplay
}
// server-side lock
get controlLocked() {
return 'control' in this.$accessor.locked && this.$accessor.locked['control'] && !this.$accessor.user.admin
}
get locked() {
return this.$accessor.remote.locked || (this.controlLocked && (!this.hosting || this.implicitHosting))
return this.$accessor.remote.locked
}
get scroll() {
@ -344,13 +321,13 @@
}
@Watch('width')
onWidthChanged() {
this.onResize()
onWidthChanged(width: number) {
this.onResise()
}
@Watch('height')
onHeightChanged() {
this.onResize()
onHeightChanged(height: number) {
this.onResise()
}
@Watch('volume')
@ -366,6 +343,7 @@
onMutedChanged(muted: boolean) {
if (this._video && this._video.muted != muted) {
this._video.muted = muted
this.startsMuted = muted
if (!muted) {
this.mutedOverlay = false
@ -388,29 +366,9 @@
}
@Watch('playing')
async onPlayingChanged(playing: boolean) {
onPlayingChanged(playing: boolean) {
if (this._video && this._video.paused && playing) {
// 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()
}
}
this.play()
}
if (this._video && !this._video.paused && !playing) {
@ -419,35 +377,34 @@
}
@Watch('clipboard')
async onClipboardChanged(clipboard: string) {
onClipboardChanged(clipboard: string) {
if (this.clipboard_write_available) {
try {
await navigator.clipboard.writeText(clipboard)
this.$accessor.remote.setClipboard(clipboard)
} catch (err: any) {
this.$log.error(err)
}
navigator.clipboard.writeText(clipboard).catch(console.error)
}
}
mounted() {
this._container.addEventListener('resize', this.onResize)
this._container.addEventListener('resize', this.onResise)
this.onVolumeChanged(this.volume)
this.onMutedChanged(this.muted)
this.onStreamChanged(this.stream)
this.onResize()
this.onResise()
this.observer.observe(this._component)
onFullscreenChange(this._player, () => {
this.fullscreen = isFullscreen()
this.fullscreen ? lockKeyboard() : unlockKeyboard()
this.onResize()
this._player.addEventListener('fullscreenchange', () => {
this.fullscreen = document.fullscreenElement !== null
this.onResise()
})
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()
})
@ -463,7 +420,7 @@
this.$accessor.video.setPlayable(false)
})
this._video.addEventListener('volumechange', () => {
this._video.addEventListener('volumechange', (event) => {
this.$accessor.video.setMuted(this._video.muted)
this.$accessor.video.setVolume(this._video.volume * 100)
})
@ -476,9 +433,11 @@
this.$accessor.video.pause()
})
document.addEventListener('focusin', this.onFocus.bind(this))
/* Initialize Guacamole Keyboard */
this.keyboard.onkeydown = (key: number) => {
if (!this.hosting || this.locked) {
if (!this.focused || !this.hosting || this.locked) {
return true
}
@ -486,7 +445,7 @@
return false
}
this.keyboard.onkeyup = (key: number) => {
if (!this.hosting || this.locked) {
if (!this.focused || !this.hosting || this.locked) {
return
}
@ -498,6 +457,7 @@
beforeDestroy() {
this.observer.disconnect()
this.$accessor.video.setPlayable(false)
document.removeEventListener('focusin', this.onFocus.bind(this))
/* Guacamole Keyboard does not provide destroy functions */
}
@ -553,7 +513,7 @@
try {
await this._video.play()
this.onResize()
this.onResise()
} catch (err: any) {
this.$log.error(err)
}
@ -579,11 +539,6 @@
}
}
playAndUnmute() {
this.$accessor.video.play()
this.$accessor.video.setMuted(false)
}
unmute() {
this.$accessor.video.setMuted(false)
}
@ -596,20 +551,38 @@
this.$accessor.remote.toggle()
}
requestControl() {
this.$accessor.remote.request()
_elementRequestFullscreen(el: HTMLElement) {
if (typeof el.requestFullscreen === 'function') {
el.requestFullscreen()
//@ts-ignore
} else if (typeof el.webkitRequestFullscreen === 'function') {
//@ts-ignore
el.webkitRequestFullscreen()
//@ts-ignore
} else if (typeof el.webkitEnterFullscreen === 'function') {
//@ts-ignore
el.webkitEnterFullscreen()
//@ts-ignore
} else if (typeof el.msRequestFullScreen === 'function') {
//@ts-ignore
el.msRequestFullScreen()
} else {
return false
}
return true
}
requestFullscreen() {
// try to fullscreen player element
if (elementRequestFullscreen(this._player)) {
this.onResize()
if (this._elementRequestFullscreen(this._player)) {
this.onResise()
return
}
// fallback to fullscreen video itself (on mobile devices)
if (elementRequestFullscreen(this._video)) {
this.onResize()
if (this._elementRequestFullscreen(this._video)) {
this.onResise()
return
}
}
@ -617,19 +590,15 @@
requestPictureInPicture() {
//@ts-ignore
this._video.requestPictureInPicture()
this.onResize()
this.onResise()
}
openResolution(event: MouseEvent) {
this._resolution.open(event)
async onFocus() {
if (!document.hasFocus() || !this.$accessor.active) {
return
}
openClipboard() {
this._clipboard.open()
}
async syncClipboard() {
if (this.clipboard_read_available) {
if (this.hosting && this.clipboard_read_available) {
try {
const text = await navigator.clipboard.readText()
if (this.clipboard !== text) {
@ -642,10 +611,9 @@
}
}
sendMousePos(e: MouseEvent) {
onMousePos(e: MouseEvent) {
const { w, h } = this.$accessor.video.resolution
const rect = this._overlay.getBoundingClientRect()
this.$client.sendData('mousemove', {
x: Math.round((w / rect.width) * (e.clientX - rect.left)),
y: Math.round((h / rect.height) * (e.clientY - rect.top)),
@ -657,6 +625,7 @@
if (!this.hosting || this.locked) {
return
}
this.onMousePos(e)
let x = e.deltaX
let y = e.deltaY
@ -679,8 +648,6 @@
x = Math.min(Math.max(x, -this.scroll), this.scroll)
y = Math.min(Math.max(y, -this.scroll), this.scroll)
this.sendMousePos(e)
if (!this.wheelThrottle) {
this.wheelThrottle = true
this.$client.sendData('wheel', { x, y })
@ -691,35 +658,6 @@
}
}
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)
@ -729,7 +667,7 @@
return
}
this.sendMousePos(e)
this.onMousePos(e)
this.$client.sendData('mousedown', { key: e.button + 1 })
}
@ -738,7 +676,7 @@
return
}
this.sendMousePos(e)
this.onMousePos(e)
this.$client.sendData('mouseup', { key: e.button + 1 })
}
@ -747,7 +685,7 @@
return
}
this.sendMousePos(e)
this.onMousePos(e)
}
onMouseEnter(e: MouseEvent) {
@ -757,10 +695,10 @@
numLock: e.getModifierState('NumLock'),
scrollLock: e.getModifierState('ScrollLock'),
})
this.syncClipboard()
}
this._overlay.focus()
this.onFocus()
this.focused = true
}
@ -777,22 +715,28 @@
this.focused = false
}
onResize() {
const { offsetWidth, offsetHeight } = !this.fullscreen ? this._component : document.body
onResise() {
let height = 0
if (!this.fullscreen) {
const { offsetWidth, offsetHeight } = this._component
this._player.style.width = `${offsetWidth}px`
this._player.style.height = `${offsetHeight}px`
this._container.style.maxWidth = `${(this.horizontal / this.vertical) * offsetHeight}px`
height = offsetHeight
} else {
const { offsetWidth, offsetHeight } = this._player
height = offsetHeight
}
this._container.style.maxWidth = `${(this.horizontal / this.vertical) * height}px`
this._aspect.style.paddingBottom = `${(this.vertical / this.horizontal) * 100}%`
}
@Watch('focused')
@Watch('hosting')
@Watch('locked')
onFocus() {
// in order to capture key events, overlay must be focused
if (this.focused && this.hosting && !this.locked) {
this._overlay.focus()
onResolution(event: MouseEvent) {
this._resolution.open(event)
}
onClipboard(event: MouseEvent) {
this._clipboard.open(event)
}
}
</script>

View File

@ -40,7 +40,7 @@ const exportMixin = {
},
$client () {
return window.$client
},
}
},
}
@ -53,7 +53,14 @@ const plugini18n: PluginObject<undefined> = {
}
function extend (component: any) {
return component.use(plugini18n).use(Logger).use(Axios).use(Swal).use(Anime).use(Client).extend(exportMixin)
return component
.use(plugini18n)
.use(Logger)
.use(Axios)
.use(Swal)
.use(Anime)
.use(Client)
.extend(exportMixin)
}
export const NekoConnect = extend(Connect)

View File

@ -7,7 +7,6 @@ export const send_a_message = 'Sende eine Nachricht'
export const side = {
chat: 'Chat',
files: 'Dateien',
settings: 'Einstellungen',
}
@ -69,14 +68,6 @@ 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 = {
@ -117,9 +108,3 @@ 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',
}

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