New theme for web UI (#957)
* Docker environment fixes * New theme for web UI * Apply breaking wrap to filenames only * Reduce font sizes, whitespace and padding * Right align action fields/buttons * Improve mobile header, hide superfluous UI elements when logged out, drop placeholders from login labels, various other adjustments * Force footer to bottom of screen * Show manual link to logged out users * Reduce header text size on desktop * Fix incorrect selector ID * Fix selector referencing old class name * Fix right-aligned message when images table empty * Add CSS linter/auto-formatter * Run Stylelint + Prettier against modern theme CSS * Select default theme based on browser’s user agent * Style inputs on mobile/tablet devices * Fixes for Safari 14 on iOS + iPad OS * Explicitly define mobile browser support, switch to bare ua-parser without user-agent wrapper * Add LICENSE file for modern theme icons * Improve theme selection query string/field naming. * Remove patch workaround from Docker build * Update log level for UAs to info * Move Bootstrap Reboot CSS to CDN * Account for LUN column in attached devices table * Prevent wrapping of config forms on small viewports * Fix Stylelint issues * Auto-format CSS with Prettier
@ -7,11 +7,10 @@
|
||||
!/docker/rascsi-web/start.sh
|
||||
!/doc
|
||||
!/python
|
||||
!/src
|
||||
!/cpp
|
||||
!/test
|
||||
!/easyinstall.sh
|
||||
!/LICENCE
|
||||
!/lido-driver.img
|
||||
!/README.md
|
||||
|
||||
# From .gitignore
|
||||
|
1
.gitignore
vendored
@ -11,6 +11,7 @@ rascsi_interface_pb2.py
|
||||
messages.pot
|
||||
messages.mo
|
||||
report.xml
|
||||
node_modules
|
||||
|
||||
# Intermediate files from astyle
|
||||
*.orig
|
||||
|
@ -6,19 +6,27 @@ FROM "${OS_ARCH}/${OS_DISTRO}:${OS_VERSION}"
|
||||
EXPOSE 80 443
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends sudo systemd rsyslog procps man-db man2html
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
sudo \
|
||||
systemd \
|
||||
rsyslog \
|
||||
procps \
|
||||
man-db \
|
||||
wget \
|
||||
git
|
||||
|
||||
RUN groupadd pi
|
||||
RUN useradd --create-home --shell /bin/bash -g pi pi
|
||||
RUN echo "pi ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
||||
RUN echo "pi:rascsi" | chpasswd
|
||||
|
||||
RUN mkdir /home/pi/afpshare
|
||||
RUN mkdir /home/pi/shared_files
|
||||
RUN touch /etc/dhcpcd.conf
|
||||
RUN mkdir -p /etc/network/interfaces.d/
|
||||
|
||||
WORKDIR /home/pi/RASCSI
|
||||
USER pi
|
||||
WORKDIR /home/pi/RASCSI
|
||||
COPY --chown=pi:pi . .
|
||||
|
||||
# Install standalone RaSCSI web UI
|
||||
|
@ -6,20 +6,16 @@ FROM "${OS_ARCH}/${OS_DISTRO}:${OS_VERSION}"
|
||||
EXPOSE 6868
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends sudo systemd rsyslog patch
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends sudo systemd rsyslog patch wget
|
||||
|
||||
RUN groupadd pi
|
||||
RUN useradd --create-home --shell /bin/bash -g pi pi
|
||||
RUN echo "pi ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
||||
|
||||
WORKDIR /home/pi/RASCSI
|
||||
USER pi
|
||||
WORKDIR /home/pi/RASCSI
|
||||
COPY --chown=pi:pi . .
|
||||
|
||||
# Workaround for Bullseye amd64 compilation error
|
||||
# https://github.com/akuker/RASCSI/issues/821
|
||||
RUN patch -p0 < docker/rascsi/cfilesystem.patch
|
||||
|
||||
# Install RaSCSI standalone
|
||||
RUN ./easyinstall.sh --run_choice=10 --cores=`nproc`
|
||||
|
||||
|
@ -1,24 +0,0 @@
|
||||
--- src/raspberrypi/devices/cfilesystem.cpp 2022-09-08 12:07:14.000000000 +0100
|
||||
+++ src/raspberrypi/devices/cfilesystem.cpp.patched 2022-09-08 12:12:55.000000000 +0100
|
||||
@@ -1075,12 +1075,15 @@
|
||||
m_dirHuman.name[i] = ' ';
|
||||
}
|
||||
|
||||
- for (i = 0; i < 10; i++) {
|
||||
- if (p < m_pszHumanExt)
|
||||
- m_dirHuman.add[i] = *p++;
|
||||
- else
|
||||
- m_dirHuman.add[i] = '\0';
|
||||
- }
|
||||
+ // This code causes a compilation error on Debian "bullseye" (amd64)
|
||||
+ // https://github.com/akuker/RASCSI/issues/821
|
||||
+ //
|
||||
+ // for (i = 0; i < 10; i++) {
|
||||
+ // if (p < m_pszHumanExt)
|
||||
+ // m_dirHuman.add[i] = *p++;
|
||||
+ // else
|
||||
+ // m_dirHuman.add[i] = '\0';
|
||||
+ // }
|
||||
|
||||
if (*p == '.')
|
||||
p++;
|
3
python/web/.prettierrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"printWidth": 100
|
||||
}
|
6
python/web/.stylelintrc.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": ["stylelint-config-standard", "stylelint-config-prettier"],
|
||||
"rules": {
|
||||
"no-descending-specificity": null
|
||||
}
|
||||
}
|
3343
python/web/package-lock.json
generated
Normal file
8
python/web/package.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"prettier": "2.7.1",
|
||||
"stylelint": "^14.14.1",
|
||||
"stylelint-config-prettier": "^9.0.3",
|
||||
"stylelint-config-standard": "^29.0.0"
|
||||
}
|
||||
}
|
@ -5,3 +5,4 @@ protobuf==3.20.2
|
||||
requests==2.28.1
|
||||
simplepam==0.1.5
|
||||
flask_babel==2.0.0
|
||||
ua-parser==0.16.1
|
@ -22,3 +22,12 @@ AUTH_GROUP = "rascsi"
|
||||
|
||||
# The language locales supported by RaSCSI
|
||||
LANGUAGES = ["en", "de", "sv", "fr", "es"]
|
||||
|
||||
# Available themes
|
||||
TEMPLATE_THEMES = ["classic", "modern"]
|
||||
|
||||
# Default theme for modern browsers
|
||||
TEMPLATE_THEME_DEFAULT = "modern"
|
||||
|
||||
# Fallback theme for older browsers
|
||||
TEMPLATE_THEME_LEGACY = "classic"
|
BIN
python/web/src/static/logo.png
Normal file
After Width: | Height: | Size: 59 KiB |
@ -47,11 +47,15 @@ div.footer {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
div.logged_in {
|
||||
div.footer div.theme-change-hint {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
div.logged-in {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
div.logged_out {
|
||||
div.logged-out {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
@ -66,9 +70,12 @@ div.flash {
|
||||
|
||||
div.flash div {
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
white-space: pre-line;
|
||||
padding: 2px 5px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
div.flash div div {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
div.flash div.success {
|
26
python/web/src/static/themes/modern/icons/LICENSE
Normal file
@ -0,0 +1,26 @@
|
||||
Feather Icons
|
||||
https://github.com/feathericons/feather
|
||||
|
||||
---
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2017 Cole Bemis
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-hard-drive"><line x1="22" y1="12" x2="2" y2="12"></line><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path><line x1="6" y1="16" x2="6.01" y2="16"></line><line x1="10" y1="16" x2="10.01" y2="16"></line></svg>
|
After Width: | Height: | Size: 484 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-globe"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
|
After Width: | Height: | Size: 409 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-disc"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="3"></circle></svg>
|
After Width: | Height: | Size: 295 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-server"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>
|
After Width: | Height: | Size: 431 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-printer"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
|
After Width: | Height: | Size: 407 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-save"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>
|
After Width: | Height: | Size: 392 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-slash"><circle cx="12" cy="12" r="10"></circle><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"></line></svg>
|
After Width: | Height: | Size: 312 B |
1
python/web/src/static/themes/modern/icons/error.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-octagon"><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
|
After Width: | Height: | Size: 409 B |
1
python/web/src/static/themes/modern/icons/file-copy.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-copy"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
After Width: | Height: | Size: 351 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-trash"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
|
After Width: | Height: | Size: 356 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
After Width: | Height: | Size: 371 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-zap"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>
|
After Width: | Height: | Size: 282 B |
1
python/web/src/static/themes/modern/icons/file-info.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
After Width: | Height: | Size: 308 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
|
After Width: | Height: | Size: 365 B |
1
python/web/src/static/themes/modern/icons/info.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-info"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
After Width: | Height: | Size: 347 B |
1
python/web/src/static/themes/modern/icons/log-out.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-log-out"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>
|
After Width: | Height: | Size: 360 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-book"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>
|
After Width: | Height: | Size: 345 B |
1
python/web/src/static/themes/modern/icons/manual.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-book"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>
|
After Width: | Height: | Size: 345 B |
1
python/web/src/static/themes/modern/icons/success.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
After Width: | Height: | Size: 255 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="red" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x-circle"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>
|
After Width: | Height: | Size: 337 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up-circle"><circle cx="12" cy="12" r="10"></circle><polyline points="16 12 12 8 8 12"></polyline><line x1="12" y1="16" x2="12" y2="8"></line></svg>
|
After Width: | Height: | Size: 357 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-pause-circle"><circle cx="12" cy="12" r="10"></circle><line x1="10" y1="15" x2="10" y2="9"></line><line x1="14" y1="15" x2="14" y2="9"></line></svg>
|
After Width: | Height: | Size: 352 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="green" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check-circle"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>
|
After Width: | Height: | Size: 321 B |
1
python/web/src/static/themes/modern/icons/warning.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-triangle"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
After Width: | Height: | Size: 424 B |
890
python/web/src/static/themes/modern/style.css
Normal file
@ -0,0 +1,890 @@
|
||||
@import url("//cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.2/css/bootstrap-reboot.min.css");
|
||||
|
||||
:root {
|
||||
--success: var(--bs-success);
|
||||
--danger: var(--bs-danger);
|
||||
--info: #80eaff;
|
||||
--warning: var(--bs-warning);
|
||||
--dark: var(--bs-dark);
|
||||
--light: var(--bs-light);
|
||||
--primary: var(--bs-primary);
|
||||
--secondary: var(--bs-secondary);
|
||||
--text-color: var(--bs-body-color);
|
||||
--border-radius: 0.2rem;
|
||||
--input-padding: 0.25rem 0.5rem;
|
||||
--font-size: 0.85rem;
|
||||
--icon-size: 1.2rem;
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
General layout
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: var(--font-size);
|
||||
}
|
||||
|
||||
div.content {
|
||||
flex-grow: 1;
|
||||
padding: 1rem;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
hr {
|
||||
display: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Tables
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
table {
|
||||
width: 100%;
|
||||
border: 1px solid var(--dark);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table th,
|
||||
table td {
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
table th {
|
||||
background: var(--dark);
|
||||
border: 1px solid var(--dark);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
table td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Forms
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
form {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
button,
|
||||
label {
|
||||
margin: 0.15rem 0;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
button {
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid #ccc;
|
||||
font-size: var(--font-size);
|
||||
font-weight: 400;
|
||||
line-height: 1.25;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
input[type="submit"],
|
||||
button {
|
||||
padding: var(--input-padding);
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
input[type="url"],
|
||||
input[type="password"] {
|
||||
display: inline-block;
|
||||
padding: var(--input-padding);
|
||||
background-color: #fff;
|
||||
background-clip: padding-box;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
select {
|
||||
display: inline-block;
|
||||
padding: 0.275rem 2.25rem 0.275em 0.75rem;
|
||||
-moz-padding-start: calc(0.75rem - 3px);
|
||||
background-color: #fff;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.75rem center;
|
||||
background-size: 16px 12px;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Dropzone
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
.dropzone {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dropzone p,
|
||||
.dropzone .dz-default {
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
|
||||
.dropzone .dz-button {
|
||||
width: 100%;
|
||||
padding: 2rem 4rem;
|
||||
border: 2px dashed darkcyan;
|
||||
background: lightcyan;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-image,
|
||||
.dropzone .dz-preview .dz-success-mark,
|
||||
.dropzone .dz-preview .dz-error-mark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview {
|
||||
display: inline-block;
|
||||
background: var(--light) url("icons/upload-queued.svg") no-repeat 1rem center;
|
||||
padding: 1rem 1rem 1rem 3.5rem;
|
||||
margin: 1rem 1rem 0 0;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.dropzone .dz-preview.dz-processing {
|
||||
background: #ededbe url("icons/upload-in-progress.svg") no-repeat 1rem center;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview.dz-success {
|
||||
background: #e0f5df url("icons/upload-success.svg") no-repeat 1rem center;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview.dz-error {
|
||||
background: #fae2e2 url("icons/upload-error.svg") no-repeat 1rem center;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-error-message {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.dropzone .dz-preview.dz-processing .dz-progress {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview.dz-error .dz-progress,
|
||||
.dropzone .dz-preview:not(.dz-processing) .dz-progress {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-progress .dz-upload {
|
||||
width: 1px;
|
||||
background: var(--dark);
|
||||
display: block;
|
||||
height: 0.5rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Header
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
div.header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
div.header div.title {
|
||||
order: 1;
|
||||
text-align: left;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
div.header div.title h1 {
|
||||
margin: 0;
|
||||
color: #f9f9f9;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
div.header div.title a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div.header div.hostname {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.header div.login-status {
|
||||
order: 10;
|
||||
}
|
||||
|
||||
div.header div.login-status form {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
div.header span.logged-in-as-text em {
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
div.header div.login-form-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.header div.authentication-disabled {
|
||||
background: var(--danger);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
div.header {
|
||||
min-height: 3.5rem; /* Safari 14 iOS and iPad OS */
|
||||
}
|
||||
|
||||
body:not(.logged-in) div.header {
|
||||
flex-wrap: wrap;
|
||||
min-height: 8.875rem; /* Safari 14 iOS and iPad OS */
|
||||
}
|
||||
|
||||
div.header div.title {
|
||||
background: var(--dark);
|
||||
}
|
||||
|
||||
div.header div.title a {
|
||||
display: block;
|
||||
background: url("/static/logo.png") no-repeat;
|
||||
background-size: auto 2rem;
|
||||
background-position: 1rem center;
|
||||
padding: 1rem 1rem 1rem 3.5rem;
|
||||
}
|
||||
|
||||
div.header div.login-status.logged-out {
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
|
||||
div.header div.login-status.logged-out form {
|
||||
align-items: end;
|
||||
padding: 1rem;
|
||||
background: var(--light);
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
div.header div.login-status.logged-out form span {
|
||||
display: block;
|
||||
padding: 0 0.1rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
div.header div.login-status.logged-out form label {
|
||||
display: block;
|
||||
text-align: left;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.header div.login-status.logged-out form input[type="submit"] {
|
||||
flex-grow: 0.5;
|
||||
margin-top: auto; /* Safari 14 iOS and iPad OS */
|
||||
}
|
||||
|
||||
div.header div.login-status.logged-out form input:not([type="submit"]) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.header div.login-status.logged-in {
|
||||
background: var(--dark);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.header div.login-status.logged-in span.logged-in-as-text,
|
||||
div.header div.login-status.logged-in span.separator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.header div.login-status.logged-in a {
|
||||
margin-right: 1rem;
|
||||
color: var(--secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 821px) {
|
||||
div.header {
|
||||
background: var(--dark);
|
||||
align-items: center;
|
||||
padding: 0.5rem 1.25rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
div.header div.title a {
|
||||
display: inline-block;
|
||||
background: url("/static/logo.png") no-repeat;
|
||||
background-size: auto 40px;
|
||||
background-position: left center;
|
||||
padding-left: 3rem;
|
||||
}
|
||||
|
||||
div.header div.title a h1 {
|
||||
font-size: 1.5rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
@supports (-webkit-background-clip: text) {
|
||||
div.header div.title a:hover h1 {
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgb(101 204 51 / 100%) 0%,
|
||||
rgb(255 204 51 / 100%) 10%,
|
||||
rgb(255 153 51 / 100%) 20%,
|
||||
rgb(205 51 50 / 100%) 55%,
|
||||
rgb(152 50 153 / 100%) 100%
|
||||
);
|
||||
|
||||
-webkit-background-clip: text; /* stylelint-disable-line */
|
||||
-webkit-text-fill-color: transparent; /* stylelint-disable-line */
|
||||
}
|
||||
}
|
||||
|
||||
div.header div.login-status.logged-out form label,
|
||||
div.header div.login-status.logged-out form input {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
div.header div.login-status.logged-out form input:not([type="submit"]) {
|
||||
width: 8rem;
|
||||
background: var(--dark);
|
||||
border-color: var(--secondary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
div.header div.login-status.logged-out form input::-webkit-credentials-auto-fill-button {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
div.header div.login-status.logged-out form input[type="submit"] {
|
||||
background: var(--secondary);
|
||||
border-color: var(--secondary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
div.header div.login-status.logged-in a {
|
||||
background: var(--danger) url("icons/log-out.svg") no-repeat right 0.5rem center;
|
||||
background-size: var(--icon-size);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.25rem 2.25rem 0.25rem 0.75rem;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
div.header div.login-status.logged-in span.logged-in-as-text {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
div.header div.login-status.logged-in span.separator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Footer
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
div.footer {
|
||||
flex-shrink: 0;
|
||||
background: var(--dark);
|
||||
padding: 1rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
div.footer a {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
div.footer div.theme-change-hint {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
div.footer div.theme-change-hint a {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Flash messages
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
div.flash > div {
|
||||
margin: 1rem 1rem 0;
|
||||
padding: 0.5rem 0.75rem 0.5rem 3rem;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: #efefef;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 1rem center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.flash > div a {
|
||||
display: inline-block !important;
|
||||
padding: 0.25rem 0.75rem;
|
||||
margin-left: auto;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.flash > div a::before {
|
||||
content: "×";
|
||||
}
|
||||
|
||||
div.flash > div.info {
|
||||
background-color: var(--info);
|
||||
background-image: url("icons/info.svg");
|
||||
}
|
||||
|
||||
div.flash > div.error {
|
||||
background-color: var(--danger);
|
||||
background-image: url("icons/error.svg");
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
div.flash > div.success {
|
||||
background-color: var(--success);
|
||||
background-image: url("icons/success.svg");
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
div.flash > div.warning {
|
||||
background-color: var(--warning);
|
||||
background-image: url("icons/warning.svg");
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Section headings
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
section > details {
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
div.content > section:first-child > details {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
section > details summary {
|
||||
background: var(--secondary);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.5rem 1rem;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
section > details ul {
|
||||
background-color: lightcyan;
|
||||
border: 2px solid var(--secondary);
|
||||
padding: 1rem 1rem 1rem 2rem;
|
||||
margin-top: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
section > details summary {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Index > Section: Current RaSCSI configuration
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
body:not(.logged-in) section:not(#current-config, #manual) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body:not(.logged-in) section#current-config form#config-actions,
|
||||
body:not(.logged-in) section#current-config form#config-save {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body:not(.logged-in) section#current-config table#attached-devices th.actions,
|
||||
body:not(.logged-in) section#current-config table#attached-devices td.actions,
|
||||
body:not(.logged-in) section#current-config table#attached-devices form {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body:not(.logged-in) section#current-config form#detach-all-devices {
|
||||
display: none;
|
||||
}
|
||||
|
||||
section#current-config form#config-actions select,
|
||||
section#current-config form#config-save input[type="text"] {
|
||||
max-width: 10rem;
|
||||
}
|
||||
|
||||
table#attached-devices th.id,
|
||||
table#attached-devices td.id,
|
||||
table#attached-devices th.unit,
|
||||
table#attached-devices td.unit {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table#attached-devices th.actions,
|
||||
table#attached-devices td.actions {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table#attached-devices td.parameters form {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
table#attached-devices td.parameters form label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
table#attached-devices td.parameters form select {
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
table#attached-devices span.filename {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
table#attached-devices tr.reserved td {
|
||||
background-color: #ffe9e9;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
table#attached-devices th.product,
|
||||
table#attached-devices td.product {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 625px) {
|
||||
table#attached-devices td.parameters form {
|
||||
display: block;
|
||||
max-width: none;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table#attached-devices td.parameters form select {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 821px) {
|
||||
section#current-config form#config-actions {
|
||||
float: left;
|
||||
height: 2.75rem;
|
||||
}
|
||||
|
||||
section#current-config form#config-save {
|
||||
float: right;
|
||||
height: 2.75rem;
|
||||
}
|
||||
|
||||
section#current-config form#config-save input[type="text"] {
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
table#attached-devices tr.device-assigned td.name,
|
||||
table#attached-devices tr.reserved td.name {
|
||||
background-image: url("icons/device-other.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: 1rem center;
|
||||
background-size: var(--icon-size);
|
||||
padding-left: 3rem;
|
||||
}
|
||||
|
||||
table#attached-devices tr.reserved td.name {
|
||||
background-image: url("icons/device-reserved.svg");
|
||||
}
|
||||
|
||||
table#attached-devices tr.device-sccd td.name,
|
||||
table#attached-devices tr.device-scmo td.name {
|
||||
background-image: url("icons/device-optical.svg");
|
||||
}
|
||||
|
||||
table#attached-devices tr.device-scdp td.name {
|
||||
background-image: url("icons/device-network.svg");
|
||||
}
|
||||
|
||||
table#attached-devices tr.device-schd td.name {
|
||||
background-image: url("icons/device-hard-drive.svg");
|
||||
}
|
||||
|
||||
table#attached-devices tr.device-scrm td.name {
|
||||
background-image: url("icons/device-removable.svg");
|
||||
}
|
||||
|
||||
table#attached-devices tr.device-sclp td.name {
|
||||
background-image: url("icons/device-printer.svg");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Index > Section:Image/file management
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
section#files table#images td:first-child {
|
||||
word-break: break-all;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
section#files table#images th:last-child,
|
||||
section#files table#images td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
section#files table#images tr.directory-empty td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
section#files p {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
section#files table#images tr th:nth-child(2),
|
||||
section#files table#images tr td:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
section#files table#images form.file-attach {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px dotted #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 821px) {
|
||||
section#files table#images form.file-copy input[type="submit"],
|
||||
section#files table#images form.file-rename input[type="submit"],
|
||||
section#files table#images form.file-delete input[type="submit"],
|
||||
section#files table#images form.file-info input[type="submit"] {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 1rem;
|
||||
text-indent: -1000px;
|
||||
width: 2.5rem;
|
||||
}
|
||||
|
||||
section#files table#images form.file-attach input[type="submit"],
|
||||
section#attach-devices form.device-attach input[type="submit"] {
|
||||
background: #efefef url("icons/file-device-attach.svg") no-repeat 0.5rem center;
|
||||
background-size: 1rem;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
section#files table#images form.file-copy input[type="submit"] {
|
||||
background-image: url("icons/file-copy.svg");
|
||||
}
|
||||
|
||||
section#files table#images form.file-rename input[type="submit"] {
|
||||
background-image: url("icons/file-rename.svg");
|
||||
}
|
||||
|
||||
section#files table#images form.file-delete input[type="submit"] {
|
||||
background-image: url("icons/file-delete.svg");
|
||||
}
|
||||
|
||||
section#files table#images form.file-info input[type="submit"] {
|
||||
background-image: url("icons/file-info.svg");
|
||||
}
|
||||
|
||||
section#files table#images form.file-extract input[type="submit"] {
|
||||
background: #efefef url("icons/file-extract.svg") no-repeat 0.5rem center;
|
||||
background-size: 1rem;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Index > Section: Attach peripheral devices
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
section#attach-devices table th:last-child,
|
||||
section#attach-devices table td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
section#attach-devices form {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
section#attach-devices table tr th:nth-child(2),
|
||||
section#attach-devices table tr td:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
section#attach-devices form label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
section#attach-devices form select {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Index > Section: Create image
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
section#create-image > p a {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Index > Section: Logging
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
section#logging div:first-of-type {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Index > Section: System
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
@media (min-width: 821px) {
|
||||
section#system input[type="submit"] {
|
||||
background: var(--danger);
|
||||
border-color: var(--danger);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Index > Section: Manual
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
section#manual {
|
||||
margin: 2rem 0 1rem;
|
||||
}
|
||||
|
||||
section#manual a {
|
||||
margin: auto;
|
||||
display: block;
|
||||
padding: 0 0 0 2rem;
|
||||
background: url("icons/manual.svg") no-repeat left center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
section#manual a p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Drives page
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
body.page-drives div.content h2:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
body.page-drives div.content h2 {
|
||||
margin: 2rem 0 1rem;
|
||||
}
|
||||
|
||||
body.page-drives div.content p:first-of-type {
|
||||
background: lightcyan;
|
||||
border: 2px solid darkcyan;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
body.page-drives div.content p:nth-of-type(3) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
body.page-drives div.content p.home {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Disk info page
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
body.page-diskinfo div.content p.home {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Device info page
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
body.page-deviceinfo div.content table th {
|
||||
background: #efefef;
|
||||
color: var(--text-color);
|
||||
border-color: #ccc;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
body.page-deviceinfo div.content p.home {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Logs page
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
body.page-logs div.content p.home {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Manual page
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
body.page-manpage div#manpage-content {
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
body.page-manpage div#manpage-content h2 {
|
||||
margin: 2rem 0 0.5rem;
|
||||
}
|
||||
|
||||
body.page-manpage div.content p.home {
|
||||
margin-top: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
@ -22,16 +22,16 @@
|
||||
<meta name="msapplication-TileImage" content="/pwa/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename=current_theme_stylesheet) }}">
|
||||
|
||||
<script type="application/javascript">
|
||||
var processNotify = function(Notification) {
|
||||
document.getElementById("flash").innerHTML = "<div class=\"info\">" + Notification + "{{ _(" This process may take a while, and will continue in the background if you navigate away from this page.") }}</div>";
|
||||
document.getElementById("flash").innerHTML = "<div class=\"info\"><div>" + Notification + "{{ _(" This process may take a while, and will continue in the background if you navigate away from this page.") }}</div></div>";
|
||||
window.scrollTo(0,0);
|
||||
}
|
||||
|
||||
var shutdownNotify = function(Notification) {
|
||||
document.getElementById("flash").innerHTML = "<div class=\"info\">" + Notification + "{{ _(" The Web Interface will become unresponsive momentarily. Reload this page after the Pi has started up again.") }}</div>";
|
||||
document.getElementById("flash").innerHTML = "<div class=\"warning\"><div>" + Notification + "{{ _(" The Web Interface will become unresponsive momentarily. Reload this page after the Pi has started up again.") }}</div></div>";
|
||||
window.scrollTo(0,0);
|
||||
}
|
||||
</script>
|
||||
@ -39,52 +39,77 @@
|
||||
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.9.3/min/dropzone.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body class="{{ body_class }}{% if env["logged_in"] %} logged-in{% endif %}">
|
||||
<div class="header">
|
||||
{% if env["auth_active"] %}
|
||||
{% if env["username"] %}
|
||||
<div align="center" class="logged_in">
|
||||
{{ _("Logged in as <em>%(username)s</em>", username=env["username"]) }} - <a href="/logout">{{ _("Log Out") }}</a>
|
||||
{% if env["logged_in"] %}
|
||||
<div align="center" class="login-status logged-in">
|
||||
<span class="logged-in-as-text">{{ _("Logged in as <em>%(username)s</em>", username=env["username"]) }}</span>
|
||||
<span class="separator">-</span>
|
||||
<a href="/logout">{{ _("Log Out") }}</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div align="center" class="logged_out">
|
||||
<div align="center" class="login-status logged-out">
|
||||
<form method="POST" action="/login">
|
||||
<div>{{ _("Log In to Use Web Interface") }}</div>
|
||||
<div class="login-form-title">{{ _("Log In to Use Web Interface") }}</div>
|
||||
<span>
|
||||
<label for="username">{{ _("Username") }}</label>
|
||||
<input type="text" name="username" id="username">
|
||||
</span>
|
||||
<span>
|
||||
<label for="password">{{ _("Password") }}</label>
|
||||
<input type="password" name="password" id="password">
|
||||
</span>
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div align="center" class="logged_out">
|
||||
<div align="center" class="login-status authentication-disabled">
|
||||
{{ _("Web Interface Authentication Disabled") }} - {{ _("See <a href=\"%(url)s\" target=\"_blank\">Wiki</a> for more information", url="https://github.com/akuker/RASCSI/wiki/Web-Interface#enable-authentication") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div align="center">
|
||||
|
||||
<div align="center" class="title">
|
||||
<a href="/">
|
||||
<h1>{{ _("RaSCSI Reloaded Control Page") }}</h1>
|
||||
<h1>{{ _("RaSCSI Reloaded") }}</h1>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
hostname: {{ env["host"] }} ip: {{ env["ip_addr"] }}
|
||||
|
||||
<div class="hostname">
|
||||
<span>{{ _("IP") }}: {{ env["ip_addr"] }}</span>
|
||||
<span>{{ _("Hostname") }}: {{ env["host"] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flash" id="flash">
|
||||
{% if get_flashed_messages(): %}
|
||||
{% for category, message in get_flashed_messages(with_categories=true) %}
|
||||
<div class="{{ category }}">
|
||||
{% if category == "stdout" or category == "stderr" %}
|
||||
<pre>{{ message }}</pre>
|
||||
{% else %}
|
||||
<div class="{{ category }}">{{ message }}</div>
|
||||
<div>{{ message }}</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<a style="display: none;" href="/"></a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
{{ content_class }}
|
||||
{% block content %}{% endblock content %}
|
||||
</div>
|
||||
<div align="center" class="footer">
|
||||
<div class="theme-change-hint">
|
||||
{% if current_theme == "classic" %}
|
||||
{{ _('Switch to the <a href="/theme?name=%(theme)s">%(theme)s theme</a>', theme="modern") }}
|
||||
{% else %}
|
||||
{{ _('Switch to the <a href="/theme?name=%(theme)s">%(theme)s theme</a>', theme="classic") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{% if env["netatalk_configured"] == 1 %}
|
||||
{{ _("The AppleShare server is running. No active connections.") }}
|
||||
|
@ -52,6 +52,6 @@
|
||||
</table>
|
||||
</p>
|
||||
{% endfor %}
|
||||
<p><a href="/">{{ _("Go to Home") }}</a></p>
|
||||
<p class="home"><a href="/">{{ _("Go to Home") }}</a></p>
|
||||
|
||||
{% endblock content %}
|
||||
|
@ -3,6 +3,6 @@
|
||||
{% block content %}
|
||||
<h2>{{ _("Disk Image Details: %(file_name)s", file_name=file_name) }}</h2>
|
||||
<p><pre>{{ diskinfo }}</pre></p>
|
||||
<p><a href="/">{{ _("Go to Home") }}</a></p>
|
||||
<p class="home"><a href="/">{{ _("Go to Home") }}</a></p>
|
||||
|
||||
{% endblock content %}
|
||||
|
@ -112,6 +112,6 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<p><small>{{ _("%(disk_space)s MiB disk space remaining on the Pi", disk_space=env["free_disk_space"]) }}</small></p>
|
||||
<p><a href="/">{{ _("Go to Home") }}</a></p>
|
||||
<p class="home"><a href="/">{{ _("Go to Home") }}</a></p>
|
||||
|
||||
{% endblock content %}
|
||||
|
@ -1,6 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<section id="current-config">
|
||||
<details>
|
||||
<summary class="heading">
|
||||
{{ _("Current RaSCSI Configuration") }}
|
||||
@ -12,8 +13,8 @@
|
||||
</details>
|
||||
|
||||
<p>
|
||||
<form action="/config/load" method="post">
|
||||
<label for="config_load_name">{{ _("File name") }}</label>
|
||||
<form action="/config/load" method="post" id="config-actions">
|
||||
<label for="config_load_name">{{ _("File Name:") }}</label>
|
||||
<select name="name" id="config_load_name" required="" width="14">
|
||||
{% if config_files %}
|
||||
{% for config in config_files|sort %}
|
||||
@ -33,35 +34,41 @@
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<form action="/config/save" method="post">
|
||||
<label for="config_save_name">{{ _("File name") }}</label>
|
||||
<input name="name" id="config_save_name" value="default" size="20">
|
||||
<form action="/config/save" method="post" id="config-save">
|
||||
<label for="config_save_name">{{ _("File Name:") }}</label>
|
||||
<input type="text" name="name" id="config_save_name" value="default" size="20">
|
||||
.{{ CONFIG_FILE_SUFFIX }}
|
||||
<input type="submit" value="{{ _("Save") }}">
|
||||
</form>
|
||||
</p>
|
||||
|
||||
<table border="black" cellpadding="3" summary="List of attached devices">
|
||||
<table id="attached-devices" border="black" cellpadding="3" summary="List of attached devices">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="col">{{ _("ID") }}</th>
|
||||
<th class="id" scope="col">{{ _("ID") }}</th>
|
||||
{% if units %}
|
||||
<th scope="col">{{ _("LUN") }}</th>
|
||||
<th class="unit" scope="col">{{ _("LUN") }}</th>
|
||||
{% endif %}
|
||||
<th scope="col">{{ _("Device") }}</th>
|
||||
<th scope="col">{{ _("Parameters") }}</th>
|
||||
<th scope="col">{{ _("Product") }}</th>
|
||||
<th scope="col">{{ _("Actions") }}</th>
|
||||
<th class="name" scope="col">{{ _("Device") }}</th>
|
||||
<th class="parameters" scope="col">{{ _("Parameters") }}</th>
|
||||
<th class="product" scope="col">{{ _("Product") }}</th>
|
||||
<th class="actions" scope="col">{{ _("Actions") }}</th>
|
||||
</tr>
|
||||
{% for device in devices | sort(attribute='id') %}
|
||||
<tr>
|
||||
{% if device["id"] not in reserved_scsi_ids %}
|
||||
<td align="center">{{ device.id }}</td>
|
||||
{% if units %}
|
||||
<td align="center">{{ device.unit }}</td>
|
||||
{% if device["id"] in reserved_scsi_ids %}
|
||||
<tr class="reserved">
|
||||
{% elif device.device_type %}
|
||||
<tr class="device-assigned device-{{ device.device_type|lower }}">
|
||||
{% else %}
|
||||
<tr class="free">
|
||||
{% endif %}
|
||||
<td align="center">{{ device.device_name }}</td>
|
||||
<td>
|
||||
{% if device["id"] not in reserved_scsi_ids %}
|
||||
<td class="id" align="center">{{ device.id }}</td>
|
||||
{% if units %}
|
||||
<td class="unit" align="center">{{ device.unit }}</td>
|
||||
{% endif %}
|
||||
<td class="name" align="center">{{ device.device_name }}</td>
|
||||
<td class="parameters">
|
||||
{% if "No Media" in device.status %}
|
||||
<form action="/scsi/attach" method="post">
|
||||
<input name="scsi_id" type="hidden" value="{{ device.id }}">
|
||||
@ -100,11 +107,11 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% elif device.file %}
|
||||
{{ device.file }}
|
||||
<span class="filename">{{ device.file }}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td align="center">
|
||||
<td class="product" align="center">
|
||||
{% if device.vendor != "RaSCSI" %}
|
||||
{{ device.vendor }}
|
||||
{% endif %}
|
||||
@ -113,7 +120,7 @@
|
||||
{{ device.revision }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td align="center">
|
||||
<td class="actions" align="center">
|
||||
{% if device.id in scsi_ids["occupied_ids"] %}
|
||||
{% if device.device_type in REMOVABLE_DEVICE_TYPES and "No Media" not in device.status %}
|
||||
<form action="/scsi/eject" method="post" onsubmit="return confirm('{{ _("Eject Disk? WARNING: On Mac OS, eject the Disk in the Finder instead!") }}')">
|
||||
@ -136,14 +143,14 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
{% else %}
|
||||
<td class="inactive">{{ device.id }}</td>
|
||||
<td class="id inactive">{{ device.id }}</td>
|
||||
{% if units %}
|
||||
<td class="inactive"></td>
|
||||
<td class="units inactive"></td>
|
||||
{% endif %}
|
||||
<td class="inactive">{{ _("Reserved ID") }}</td>
|
||||
<td class="inactive">{{ RESERVATIONS[device.id] }}</td>
|
||||
<td class="inactive"></td>
|
||||
<td class="inactive">
|
||||
<td class="name inactive">{{ _("Reserved ID") }}</td>
|
||||
<td class="parameters inactive">{{ RESERVATIONS[device.id] }}</td>
|
||||
<td class="product inactive"></td>
|
||||
<td class="actions inactive">
|
||||
<form action="/scsi/release" method="post">
|
||||
<input name="scsi_id" type="hidden" value="{{ device.id }}">
|
||||
<input type="submit" value="{{ _("Release") }}">
|
||||
@ -156,16 +163,18 @@
|
||||
</table>
|
||||
|
||||
<p>
|
||||
<form action="/scsi/detach_all" method="post" onsubmit="return confirm('{{ _("Detach all SCSI Devices?") }}')">
|
||||
<form action="/scsi/detach_all" method="post" id="detach-all-devices" onsubmit="return confirm('{{ _("Detach all SCSI Devices?") }}')">
|
||||
<input type="submit" value="{{ _("Detach All Devices") }}">
|
||||
</form>
|
||||
<form action="/scsi/info" method="post">
|
||||
<form action="/scsi/info" method="post" id="show-device-info">
|
||||
<input type="submit" value="{{ _("Show Device Info") }}">
|
||||
</form>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<hr/>
|
||||
|
||||
<section id="files">
|
||||
<details>
|
||||
<summary class="heading">
|
||||
{{ _("Image File Management") }}
|
||||
@ -187,13 +196,20 @@
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<table border="black" cellpadding="3" summary="List of files in the image directory">
|
||||
<table id="images" border="black" cellpadding="3" summary="List of files in the image directory">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="col">{{ _("File") }}</th>
|
||||
<th scope="col">{{ _("Size") }}</th>
|
||||
<th scope="col">{{ _("Actions") }}</th>
|
||||
</tr>
|
||||
{% if not files|length: %}
|
||||
<tr class="directory-empty">
|
||||
<td colspan="3">
|
||||
{{ _("The images directory is currently empty.") }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% for file in files|sort(attribute='name') %}
|
||||
<tr>
|
||||
{% if file["prop"] %}
|
||||
@ -227,10 +243,10 @@
|
||||
<details>
|
||||
<summary>
|
||||
<label>{{ member["path"] }}</label>
|
||||
<form action="/files/extract_image" method="post">
|
||||
<form action="/files/extract_image" method="post" class="file-extract">
|
||||
<input name="archive_file" type="hidden" value="{{ file['name'] }}">
|
||||
<input name="archive_members" type="hidden" value="{{ member["path"] }}|{{ member["related_properties_file"] }}">
|
||||
<input type="submit" value="{{ _("Extract") }}" onclick="processNotify('{{ _("Extracting a single file...") }}')">
|
||||
<input type="submit" value="{{ _("Extract") }}" title="{{ _("Extract") }}" onclick="processNotify('{{ _("Extracting a single file...") }}')">
|
||||
</form>
|
||||
</summary>
|
||||
<ul class="inline_list">
|
||||
@ -239,10 +255,10 @@
|
||||
</details>
|
||||
{% else %}
|
||||
<label>{{ member["path"] }}</label>
|
||||
<form action="/files/extract_image" method="post">
|
||||
<form action="/files/extract_image" method="post" class="file-extract">
|
||||
<input name="archive_file" type="hidden" value="{{ file["name"] }}">
|
||||
<input name="archive_members" type="hidden" value="{{ member["path"] }}">
|
||||
<input type="submit" value="{{ _("Extract") }}" onclick="processNotify('{{ _("Extracting a single file...") }}')">
|
||||
<input type="submit" value="{{ _("Extract") }}" title="{{ _("Extract") }}" onclick="processNotify('{{ _("Extracting a single file...") }}')">
|
||||
</form>
|
||||
{% endif %}
|
||||
</li>
|
||||
@ -265,14 +281,14 @@
|
||||
{{ _("In use") }}
|
||||
{% else %}
|
||||
{% if file["archive_contents"] %}
|
||||
<form action="/files/extract_image" method="post">
|
||||
<form action="/files/extract_image" method="post" class="file-extract">
|
||||
<input name="archive_file" type="hidden" value="{{ file['name'] }}">
|
||||
{% set pipe = joiner("|") %}
|
||||
<input name="archive_members" type="hidden" value="{% for member in file["archive_contents"] %}{{ pipe() }}{{ member["path"] }}{% endfor %}">
|
||||
<input type="submit" value="{{ _("Extract All") }}" onclick="processNotify('{{ _("Extracting all files...") }}')">
|
||||
<input type="submit" value="{{ _("Extract") }}" title="{{ _("Extract") }}" onclick="processNotify('{{ _("Extracting all files...") }}')">
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="/scsi/attach" method="post">
|
||||
<form action="/scsi/attach" method="post" class="file-attach">
|
||||
<input name="file_name" type="hidden" value="{{ file['name'] }}">
|
||||
<input name="file_size" type="hidden" value="{{ file['size'] }}">
|
||||
<label for="image_list_scsi_id_{{ file["name"] }}">{{ _("ID") }}</label>
|
||||
@ -303,28 +319,28 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
<input type="submit" value="{{ _("Attach") }}">
|
||||
<input type="submit" value="{{ _("Attach") }}" title="{{ _("Attach") }}">
|
||||
{% endif %}
|
||||
</form>
|
||||
<form action="/files/rename" method="post" onsubmit="var new_file_name = prompt('{{ _("Enter new file name for: %(file_name)s", file_name=file["name"]) }}', '{{ file['name'] }}'); if (new_file_name === null) event.preventDefault(); document.getElementById('new_file_name_{{ loop.index }}').value = new_file_name;">
|
||||
<form action="/files/rename" method="post" class="file-rename" onsubmit="var new_file_name = prompt('{{ _("Enter new file name for: %(file_name)s", file_name=file["name"]) }}', '{{ file['name'] }}'); if (new_file_name === null) event.preventDefault(); document.getElementById('new_file_name_{{ loop.index }}').value = new_file_name;">
|
||||
<input name="file_name" type="hidden" value="{{ file['name'] }}">
|
||||
<input name="new_file_name" id="new_file_name_{{ loop.index }}" type="hidden" value="">
|
||||
<input type="submit" value="{{ _("Rename") }}">
|
||||
<input type="submit" value="{{ _("Rename") }}" title="{{ _("Rename") }}">
|
||||
</form>
|
||||
<form action="/files/copy" method="post" onsubmit="var copy_file_name = prompt('{{ _("Save copy of %(file_name)s as:", file_name=file["name"]) }}', '{{ file['name'] }}'); if (copy_file_name === null) event.preventDefault(); document.getElementById('copy_file_name_{{ loop.index }}').value = copy_file_name;">
|
||||
<form action="/files/copy" method="post" class="file-copy" onsubmit="var copy_file_name = prompt('{{ _("Save copy of %(file_name)s as:", file_name=file["name"]) }}', '{{ file['name'] }}'); if (copy_file_name === null) event.preventDefault(); document.getElementById('copy_file_name_{{ loop.index }}').value = copy_file_name;">
|
||||
<input name="file_name" type="hidden" value="{{ file['name'] }}">
|
||||
<input name="copy_file_name" id="copy_file_name_{{ loop.index }}" type="hidden" value="">
|
||||
<input type="submit" value="{{ _("Copy") }}">
|
||||
<input type="submit" value="{{ _("Copy") }}" title="{{ _("Copy") }}">
|
||||
</form>
|
||||
<form action="/files/delete" method="post" onsubmit="return confirm('{{ _("Delete file: %(file_name)s?", file_name=file["name"]) }}')">
|
||||
<form action="/files/delete" method="post" class="file-delete" onsubmit="return confirm('{{ _("Delete file: %(file_name)s?", file_name=file["name"]) }}')">
|
||||
<input name="file_name" type="hidden" value="{{ file['name'] }}">
|
||||
<input type="submit" value="{{ _("Delete") }}">
|
||||
<input type="submit" value="{{ _("Delete") }}" title="{{ _("Delete") }}">
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if not file["archive_contents"] %}
|
||||
<form action="/files/diskinfo" method="post">
|
||||
<form action="/files/diskinfo" method="post" class="file-info">
|
||||
<input name="file_name" type="hidden" value="{{ file['name'] }}">
|
||||
<input type="submit" value="{{ _("?") }}">
|
||||
<input type="submit" value="{{ _("?") }}" title="{{ _("Info") }}">
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
@ -333,8 +349,11 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<p><small>{{ _("%(disk_space)s MiB disk space remaining on the Pi", disk_space=env["free_disk_space"]) }}</small></p>
|
||||
</section>
|
||||
|
||||
<hr/>
|
||||
|
||||
<section id="attach-devices">
|
||||
<details>
|
||||
<summary class="heading">
|
||||
{{ _("Attach Peripheral Device") }}
|
||||
@ -367,7 +386,7 @@
|
||||
<div>{{ type }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<form action="/scsi/attach_device" method="post">
|
||||
<form action="/scsi/attach_device" method="post" class="device-attach">
|
||||
<input name="type" type="hidden" value="{{ type }}">
|
||||
{% for key, value in device_types[type]["params"] | dictsort %}
|
||||
<label for="param_{{ type }}_{{ key }}">{{ key }}:</label>
|
||||
@ -424,14 +443,17 @@
|
||||
</select>
|
||||
<label for="{{ type }}_unit">{{ _("LUN") }}</label>
|
||||
<input class="lun" name="unit" id="{{ type }}_unit" type="number" value="0" min="0" max="31" step="1" size="3">
|
||||
<input type="submit" value="{{ _("Attach") }}">
|
||||
<input type="submit" value="{{ _("Attach") }}" title="{{ _("Attach") }}">
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<hr/>
|
||||
|
||||
<section id="upload">
|
||||
<details>
|
||||
<summary class="heading">
|
||||
{{ _("Upload File from Local Computer") }}
|
||||
@ -481,9 +503,11 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</section>
|
||||
|
||||
<hr/>
|
||||
|
||||
<section id="download-url">
|
||||
<details>
|
||||
<summary class="heading">
|
||||
{{ _("Download File from the Web") }}
|
||||
@ -503,12 +527,14 @@
|
||||
<input name="url" id="download_url" required="" type="url">
|
||||
<input type="submit" value="{{ _("Download") }}" onclick="processNotify('{{ _("Downloading File...") }}')">
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<hr/>
|
||||
|
||||
<section id="download-to-iso">
|
||||
<details>
|
||||
<summary class="heading">
|
||||
{{ _("Download File and Create CD-ROM image") }}
|
||||
{{ _("Download File and Create CD-ROM Image") }}
|
||||
</summary>
|
||||
<ul>
|
||||
<li>{{ _("Create an ISO file system CD-ROM image with the downloaded file, and mount it on the given SCSI ID.") }}</li>
|
||||
@ -551,9 +577,11 @@
|
||||
</select>
|
||||
<input type="submit" value="{{ _("Download and Mount CD-ROM image") }}" onclick="processNotify('{{ _("Downloading File and generating CD-ROM image...") }}')">
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<hr/>
|
||||
|
||||
<section id="create-image">
|
||||
<details>
|
||||
<summary class="heading">
|
||||
{{ _("Create Empty Disk Image File") }}
|
||||
@ -610,9 +638,11 @@
|
||||
</form>
|
||||
|
||||
<p><a href="/drive/list">{{ _("Create a named disk image that mimics real-life drives") }}</a></p>
|
||||
</section>
|
||||
|
||||
<hr/>
|
||||
|
||||
<section id="logging">
|
||||
<details>
|
||||
<summary class="heading">
|
||||
{{ _("Logging") }}
|
||||
@ -661,9 +691,11 @@
|
||||
<input type="submit" value="{{ _("Set Log Level") }}">
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr/>
|
||||
|
||||
<section id="language">
|
||||
<details>
|
||||
<summary class="heading">
|
||||
{{ _("Language") }}
|
||||
@ -684,9 +716,11 @@
|
||||
</select>
|
||||
<input type="submit" value="{{ _("Change Language") }}">
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<hr/>
|
||||
|
||||
<section id="system">
|
||||
<details>
|
||||
<summary class="heading">
|
||||
{{ _("Raspberry Pi Operations") }}
|
||||
@ -702,9 +736,12 @@
|
||||
<form action="/pi/shutdown" method="post" onclick="if (confirm('{{ _("Shut down the Raspberry Pi?") }}')) shutdownNotify('{{ _("Shutting down the Raspberry Pi...") }}'); else event.preventDefault();">
|
||||
<input type="submit" value="{{ _("Shut Down Raspberry Pi") }}">
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<hr/>
|
||||
|
||||
<section id="manual">
|
||||
<a href="/sys/manpage?app=rascsi"><p>{{ _("Read the RaSCSI Manual") }}</p></a>
|
||||
</section>
|
||||
|
||||
{% endblock content %}
|
||||
|
@ -3,6 +3,6 @@
|
||||
{% block content %}
|
||||
<h2>{{ _("System Logs: %(scope)s %(lines)s lines", scope=scope, lines=lines) }}</h2>
|
||||
<p><pre>{{ logs }}</pre></p>
|
||||
<p><a href="/">{{ _("Go to Home") }}</a></p>
|
||||
<p class="home"><a href="/">{{ _("Go to Home") }}</a></p>
|
||||
|
||||
{% endblock content %}
|
||||
|
@ -2,7 +2,10 @@
|
||||
|
||||
{% block content %}
|
||||
<h2>{{ _("Manual for %(app)s", app=app) }}</h2>
|
||||
{{ manpage | safe }}
|
||||
<p><a href="/">{{ _("Go to Home") }}</a></p>
|
||||
|
||||
<div id="manpage-content">
|
||||
{{ manpage | safe }}
|
||||
</div>
|
||||
|
||||
<p class="home"><a href="/">{{ _("Go to Home") }}</a></p>
|
||||
{% endblock content %}
|
||||
|
@ -5,7 +5,7 @@ Module for the Flask app rendering and endpoints
|
||||
import sys
|
||||
import logging
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from pathlib import Path, PurePath
|
||||
from functools import wraps
|
||||
from grp import getgrall
|
||||
|
||||
@ -56,6 +56,7 @@ from web_utils import (
|
||||
is_bridge_configured,
|
||||
is_safe_path,
|
||||
upload_with_dropzonejs,
|
||||
browser_supports_modern_themes,
|
||||
)
|
||||
from settings import (
|
||||
WEB_DIR,
|
||||
@ -65,6 +66,9 @@ from settings import (
|
||||
DRIVE_PROPERTIES_FILE,
|
||||
AUTH_GROUP,
|
||||
LANGUAGES,
|
||||
TEMPLATE_THEMES,
|
||||
TEMPLATE_THEME_DEFAULT,
|
||||
TEMPLATE_THEME_LEGACY,
|
||||
)
|
||||
|
||||
|
||||
@ -87,6 +91,7 @@ def get_env_info():
|
||||
"running_env": sys_cmd.running_env(),
|
||||
"username": username,
|
||||
"auth_active": auth_active(AUTH_GROUP)["status"],
|
||||
"logged_in": username and auth_active(AUTH_GROUP)["status"],
|
||||
"ip_addr": ip_addr,
|
||||
"host": host,
|
||||
"free_disk_space": int(sys_cmd.disk_space()["free"] / 1024 / 1024),
|
||||
@ -133,7 +138,18 @@ def response(
|
||||
flash(message, category)
|
||||
|
||||
if template:
|
||||
if session.get("theme") and session["theme"] in TEMPLATE_THEMES:
|
||||
theme = session["theme"]
|
||||
elif browser_supports_modern_themes():
|
||||
theme = TEMPLATE_THEME_DEFAULT
|
||||
else:
|
||||
theme = TEMPLATE_THEME_LEGACY
|
||||
|
||||
kwargs["env"] = get_env_info()
|
||||
kwargs["body_class"] = f"page-{PurePath(template).stem.lower()}"
|
||||
kwargs["current_theme_stylesheet"] = f"themes/{theme}/style.css"
|
||||
kwargs["current_theme"] = theme
|
||||
kwargs["available_themes"] = TEMPLATE_THEMES
|
||||
return render_template(template, **kwargs)
|
||||
|
||||
if redirect_url:
|
||||
@ -1207,6 +1223,20 @@ def change_language():
|
||||
return response(message=_("Changed Web Interface language to %(locale)s", locale=language_name))
|
||||
|
||||
|
||||
@APP.route("/theme", methods=["GET", "POST"])
|
||||
def change_theme():
|
||||
if request.method == "GET":
|
||||
theme = request.args.get("name")
|
||||
else:
|
||||
theme = request.form.get("name")
|
||||
|
||||
if theme not in TEMPLATE_THEMES:
|
||||
return response(error=True, message=_("The requested theme does not exist."))
|
||||
|
||||
session["theme"] = theme
|
||||
return response(message=_("Theme changed to '%(theme)s'.", theme=theme))
|
||||
|
||||
|
||||
@APP.before_first_request
|
||||
def detect_locale():
|
||||
"""
|
||||
|
@ -6,6 +6,7 @@ import logging
|
||||
from grp import getgrall
|
||||
from os import path
|
||||
from pathlib import Path
|
||||
from ua_parser import user_agent_parser
|
||||
|
||||
from flask import request, make_response
|
||||
from flask_babel import _
|
||||
@ -295,3 +296,35 @@ def upload_with_dropzonejs(image_dir):
|
||||
return make_response(_("Transferred file corrupted!"), 500)
|
||||
|
||||
return make_response(_("File upload successful!"), 200)
|
||||
|
||||
|
||||
def browser_supports_modern_themes():
|
||||
"""
|
||||
Determines if the browser supports the HTML/CSS/JS features used in non-legacy themes.
|
||||
"""
|
||||
user_agent_string = request.headers.get("User-Agent")
|
||||
if not user_agent_string:
|
||||
return False
|
||||
|
||||
user_agent = user_agent_parser.Parse(user_agent_string)
|
||||
if not user_agent['user_agent']['family']:
|
||||
return False
|
||||
|
||||
# (family, minimum version)
|
||||
supported_browsers = [
|
||||
('Safari', 14),
|
||||
('Chrome', 100),
|
||||
('Firefox', 100),
|
||||
('Edge', 100),
|
||||
('Mobile Safari', 14),
|
||||
('Chrome Mobile', 100),
|
||||
]
|
||||
|
||||
current_ua_family = user_agent['user_agent']['family']
|
||||
current_ua_version = float(user_agent['user_agent']['major'])
|
||||
logging.info(f"Identified browser as family={current_ua_family}, version={current_ua_version}")
|
||||
|
||||
for supported_browser, supported_version in supported_browsers:
|
||||
if current_ua_family == supported_browser and current_ua_version >= supported_version:
|
||||
return True
|
||||
return False
|
||||
|
@ -150,3 +150,49 @@ def test_save_load_and_delete_configs(env, http_client):
|
||||
)
|
||||
|
||||
assert config_json_file not in http_client.get("/").json()["data"]["config_files"]
|
||||
|
||||
|
||||
# route("/theme", methods=["POST"])
|
||||
@pytest.mark.parametrize(
|
||||
"theme",
|
||||
[
|
||||
"modern",
|
||||
"classic",
|
||||
],
|
||||
)
|
||||
def test_set_theme(http_client, theme):
|
||||
response = http_client.post(
|
||||
"/theme",
|
||||
data={
|
||||
"theme": theme,
|
||||
},
|
||||
)
|
||||
|
||||
response_data = response.json()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response_data["status"] == STATUS_SUCCESS
|
||||
assert response_data["messages"][0]["message"] == f"Theme changed to '{theme}'."
|
||||
|
||||
|
||||
# route("/theme", methods=["GET"])
|
||||
@pytest.mark.parametrize(
|
||||
"theme",
|
||||
[
|
||||
"modern",
|
||||
"classic",
|
||||
],
|
||||
)
|
||||
def test_set_theme_via_query_string(http_client, theme):
|
||||
response = http_client.get(
|
||||
"/theme",
|
||||
params={
|
||||
"v": theme,
|
||||
},
|
||||
)
|
||||
|
||||
response_data = response.json()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response_data["status"] == STATUS_SUCCESS
|
||||
assert response_data["messages"][0]["message"] == f"Theme changed to '{theme}'."
|
||||
|