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
This commit is contained in:
nucleogenic 2022-11-14 17:32:15 +00:00 committed by GitHub
parent 5920315730
commit 3627b39af4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 4610 additions and 139 deletions

View File

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

@ -11,6 +11,7 @@ rascsi_interface_pb2.py
messages.pot
messages.mo
report.xml
node_modules
# Intermediate files from astyle
*.orig

View File

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

View File

@ -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`

View File

@ -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++;

View File

@ -0,0 +1,3 @@
{
"printWidth": 100
}

View 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

File diff suppressed because it is too large Load Diff

8
python/web/package.json Normal file
View 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"
}
}

View File

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

View File

@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -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 {

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

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

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

View 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-disc"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="3"></circle></svg>

After

Width:  |  Height:  |  Size: 295 B

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

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

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

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

View 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

View 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

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

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

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

View 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

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

View 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

View 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

View 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

View 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

View 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

View File

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

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

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

View File

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

View 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

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

View File

@ -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>
</div>
{% 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="login-status logged-out">
<form method="POST" action="/login">
<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">
<form method="POST" action="/login">
<div>{{ _("Log In to Use Web Interface") }}</div>
<label for="username">{{ _("Username") }}</label>
<input type="text" name="username" id="username">
<label for="password">{{ _("Password") }}</label>
<input type="password" name="password" id="password">
<input type="submit" value="Login">
</form>
</div>
<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 %}
{% else %}
<div align="center" class="logged_out">
{{ _("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">
{% for category, message in get_flashed_messages(with_categories=true) %}
{% if category == "stdout" or category == "stderr" %}
<pre>{{ message }}</pre>
{% else %}
<div class="{{ category }}">{{ message }}</div>
{% 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>{{ message }}</div>
{% endif %}
<a style="display: none;" href="/"></a>
</div>
{% endfor %}
{% endif %}
{% endfor %}
</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.") }}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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"] 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 %}
{% if device["id"] not in reserved_scsi_ids %}
<td align="center">{{ device.id }}</td>
<td class="id" align="center">{{ device.id }}</td>
{% if units %}
<td align="center">{{ device.unit }}</td>
<td class="unit" align="center">{{ device.unit }}</td>
{% endif %}
<td align="center">{{ device.device_name }}</td>
<td>
<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 }}">
@ -89,22 +96,22 @@
<input type="submit" value="{{ _("Attach") }}">
</form>
{% else %}
{% if device.params %}
{% for key in device.params %}
{% if key == "interface" %}
({{device.params[key]}})
{% elif key == "timeout" %}
({{key}}:{{device.params[key]}})
{% else %}
{{device.params[key]}}
{% endif %}
{% endfor %}
{% elif device.file %}
{{ device.file }}
{% endif %}
{% if device.params %}
{% for key in device.params %}
{% if key == "interface" %}
({{device.params[key]}})
{% elif key == "timeout" %}
({{key}}:{{device.params[key]}})
{% else %}
{{device.params[key]}}
{% endif %}
{% endfor %}
{% elif 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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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():
"""

View File

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

View File

@ -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}'."