Files

2150 lines
44 KiB
Python

import sys
import pwd
import sqlite3
from PySide6.QtCore import Qt, QTime
from PySide6.QtWidgets import (
QApplication,
QCheckBox,
QDialog,
QDialogButtonBox,
QFormLayout,
QFrame,
QGridLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QMainWindow,
QMessageBox,
QPushButton,
QProgressBar,
QScrollArea,
QSpinBox,
QStackedWidget,
QTimeEdit,
QVBoxLayout,
QWidget,
)
from app.database import DATABASE_PATH
# ============================================================
# Database
# ============================================================
class Database:
@staticmethod
def get_connection():
connection = sqlite3.connect(DATABASE_PATH)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys = ON")
return connection
@staticmethod
def get_users():
if not DATABASE_PATH.exists():
return []
connection = Database.get_connection()
try:
return connection.execute(
"""
SELECT *
FROM users
ORDER BY username COLLATE NOCASE
"""
).fetchall()
finally:
connection.close()
@staticmethod
def get_user(user_id):
connection = Database.get_connection()
try:
return connection.execute(
"""
SELECT *
FROM users
WHERE id = ?
""",
(user_id,),
).fetchone()
finally:
connection.close()
@staticmethod
def get_remaining_time(user_id):
if not DATABASE_PATH.exists():
return 0, 0
connection = Database.get_connection()
try:
allowance_row = connection.execute(
"""
SELECT COALESCE(
allowance_seconds,
0
) AS total
FROM daily_allowances
WHERE user_id = ?
AND weekday = CAST(
strftime('%w', 'now', 'localtime')
AS INTEGER
)
""",
(user_id,),
).fetchone()
allowance = (
allowance_row["total"]
if allowance_row
else 0
)
usage_row = connection.execute(
"""
SELECT COALESCE(
used_seconds,
0
) AS total
FROM usage
WHERE user_id = ?
AND date = date('now', 'localtime')
""",
(user_id,),
).fetchone()
used = (
usage_row["total"]
if usage_row
else 0
)
remaining = max(
allowance - used,
0,
)
return remaining, allowance
finally:
connection.close()
@staticmethod
def get_allowances(user_id):
connection = Database.get_connection()
try:
rows = connection.execute(
"""
SELECT weekday, allowance_seconds
FROM daily_allowances
WHERE user_id = ?
""",
(user_id,),
).fetchall()
return {
row["weekday"]: row["allowance_seconds"]
for row in rows
}
finally:
connection.close()
@staticmethod
def get_access_windows(user_id):
connection = Database.get_connection()
try:
rows = connection.execute(
"""
SELECT
weekday,
start_minute,
end_minute
FROM access_windows
WHERE user_id = ?
ORDER BY weekday, start_minute
""",
(user_id,),
).fetchall()
result = {}
for row in rows:
result.setdefault(
row["weekday"],
[],
).append(
(
row["start_minute"],
row["end_minute"],
)
)
return result
finally:
connection.close()
@staticmethod
def create_user(
username,
enabled,
allowances,
access_windows,
):
connection = Database.get_connection()
try:
cursor = connection.execute(
"""
INSERT INTO users (
username,
enabled
)
VALUES (?, ?)
""",
(
username,
1 if enabled else 0,
),
)
user_id = cursor.lastrowid
for weekday, seconds in allowances.items():
connection.execute(
"""
INSERT INTO daily_allowances (
user_id,
weekday,
allowance_seconds
)
VALUES (?, ?, ?)
""",
(
user_id,
weekday,
seconds,
),
)
for weekday, windows in access_windows.items():
for start_minute, end_minute in windows:
connection.execute(
"""
INSERT INTO access_windows (
user_id,
weekday,
start_minute,
end_minute
)
VALUES (?, ?, ?, ?)
""",
(
user_id,
weekday,
start_minute,
end_minute,
),
)
connection.commit()
return user_id
finally:
connection.close()
@staticmethod
def update_user(
user_id,
enabled,
allowances,
access_windows,
):
connection = Database.get_connection()
try:
connection.execute(
"""
UPDATE users
SET enabled = ?
WHERE id = ?
""",
(
1 if enabled else 0,
user_id,
),
)
for weekday, seconds in allowances.items():
connection.execute(
"""
INSERT INTO daily_allowances (
user_id,
weekday,
allowance_seconds
)
VALUES (?, ?, ?)
ON CONFLICT(user_id, weekday)
DO UPDATE SET
allowance_seconds =
excluded.allowance_seconds
""",
(
user_id,
weekday,
seconds,
),
)
# Replace access windows completely.
connection.execute(
"""
DELETE FROM access_windows
WHERE user_id = ?
""",
(user_id,),
)
for weekday, windows in access_windows.items():
for start_minute, end_minute in windows:
connection.execute(
"""
INSERT INTO access_windows (
user_id,
weekday,
start_minute,
end_minute
)
VALUES (?, ?, ?, ?)
""",
(
user_id,
weekday,
start_minute,
end_minute,
),
)
connection.commit()
finally:
connection.close()
@staticmethod
def delete_user(user_id):
connection = Database.get_connection()
try:
connection.execute(
"""
DELETE FROM users
WHERE id = ?
""",
(user_id,),
)
connection.commit()
finally:
connection.close()
@staticmethod
def set_enabled(user_id, enabled):
connection = Database.get_connection()
try:
connection.execute(
"""
UPDATE users
SET enabled = ?
WHERE id = ?
""",
(
1 if enabled else 0,
user_id,
),
)
connection.commit()
finally:
connection.close()
@staticmethod
def add_temporary_time(
user_id,
seconds,
):
connection = Database.get_connection()
try:
connection.execute(
"""
INSERT INTO temporary_grants (
user_id,
seconds,
remaining_seconds
)
VALUES (?, ?, ?)
""",
(
user_id,
seconds,
seconds,
),
)
connection.commit()
finally:
connection.close()
# ============================================================
# Linux users
# ============================================================
def linux_user_exists(username):
try:
pwd.getpwnam(username)
return True
except KeyError:
return False
# ============================================================
# Helpers
# ============================================================
# SQLite strftime('%w') numbering:
#
# Sunday = 0
# Monday = 1
# Tuesday = 2
# Wednesday = 3
# Thursday = 4
# Friday = 5
# Saturday = 6
WEEKDAYS = [
(0, "Sunday"),
(1, "Monday"),
(2, "Tuesday"),
(3, "Wednesday"),
(4, "Thursday"),
(5, "Friday"),
(6, "Saturday"),
]
def format_seconds(seconds):
seconds = max(
int(seconds),
0,
)
hours = seconds // 3600
minutes = (seconds % 3600) // 60
if hours:
return f"{hours}h {minutes:02d}m"
return f"{minutes}m"
# ============================================================
# User Card
# ============================================================
class UserCard(QFrame):
def __init__(
self,
user,
edit_callback,
toggle_callback,
parent=None,
):
super().__init__(parent)
self.user_id = user["id"]
self.setObjectName("UserCard")
layout = QVBoxLayout(self)
layout.setContentsMargins(
20,
18,
20,
18,
)
layout.setSpacing(12)
# ----------------------------------------------------
# Header
# ----------------------------------------------------
header = QHBoxLayout()
username = QLabel(
user["username"]
)
username.setObjectName(
"UserName"
)
header.addWidget(username)
header.addStretch()
status = QLabel(
"Enabled"
if user["enabled"]
else "Disabled"
)
status.setObjectName(
"StatusEnabled"
if user["enabled"]
else "StatusDisabled"
)
header.addWidget(status)
layout.addLayout(header)
# ----------------------------------------------------
# Remaining time
# ----------------------------------------------------
remaining, allowance = (
Database.get_remaining_time(
self.user_id
)
)
today_label = QLabel(
"Today's allowance"
)
today_label.setObjectName(
"SecondaryText"
)
layout.addWidget(today_label)
time_label = QLabel(
f"{format_seconds(remaining)} remaining"
)
time_label.setObjectName(
"TimeRemaining"
)
layout.addWidget(time_label)
progress = QProgressBar()
progress.setTextVisible(False)
progress.setMinimum(0)
progress.setMaximum(
max(allowance, 1)
)
progress.setValue(
min(
remaining,
allowance,
)
)
layout.addWidget(progress)
# ----------------------------------------------------
# Buttons
# ----------------------------------------------------
buttons = QHBoxLayout()
manage_button = QPushButton(
"Manage"
)
manage_button.clicked.connect(
lambda: edit_callback(
self.user_id
)
)
buttons.addWidget(
manage_button
)
toggle_text = (
"Disable"
if user["enabled"]
else "Enable"
)
toggle_button = QPushButton(
toggle_text
)
toggle_button.clicked.connect(
lambda: toggle_callback(
self.user_id,
not bool(
user["enabled"]
),
)
)
buttons.addWidget(
toggle_button
)
layout.addLayout(buttons)
# ============================================================
# Dashboard
# ============================================================
class DashboardPage(QWidget):
def __init__(
self,
edit_callback,
toggle_callback,
parent=None,
):
super().__init__(parent)
self.edit_callback = edit_callback
self.toggle_callback = toggle_callback
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(
30,
30,
30,
30,
)
self.layout.setSpacing(20)
title = QLabel(
"Dashboard"
)
title.setObjectName(
"PageTitle"
)
self.layout.addWidget(title)
self.subtitle = QLabel()
self.subtitle.setObjectName(
"SecondaryText"
)
self.layout.addWidget(
self.subtitle
)
self.scroll = QScrollArea()
self.scroll.setWidgetResizable(
True
)
self.scroll.setFrameShape(
QFrame.NoFrame
)
self.container = QWidget()
self.cards_layout = QVBoxLayout(
self.container
)
self.cards_layout.setContentsMargins(
0,
0,
0,
0,
)
self.cards_layout.setSpacing(
15
)
self.scroll.setWidget(
self.container
)
self.layout.addWidget(
self.scroll
)
self.refresh()
def refresh(self):
while self.cards_layout.count():
item = self.cards_layout.takeAt(0)
widget = item.widget()
if widget:
widget.deleteLater()
users = Database.get_users()
self.subtitle.setText(
f"{len(users)} managed user"
+ (
""
if len(users) == 1
else "s"
)
)
if not users:
empty = QLabel(
"No users are currently managed."
)
empty.setObjectName(
"EmptyText"
)
empty.setAlignment(
Qt.AlignCenter
)
self.cards_layout.addWidget(
empty
)
else:
for user in users:
card = UserCard(
user,
edit_callback=self.edit_callback,
toggle_callback=self.toggle_callback,
)
self.cards_layout.addWidget(
card
)
self.cards_layout.addStretch()
# ============================================================
# Manage User Dialog
# ============================================================
class UserDialog(QDialog):
def __init__(
self,
user=None,
parent=None,
):
super().__init__(parent)
self.user = user
if user:
self.setWindowTitle(
f"Manage User: "
f"{user['username']}"
)
else:
self.setWindowTitle(
"Add User"
)
self.setMinimumWidth(720)
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(
25,
25,
25,
25,
)
main_layout.setSpacing(18)
# ----------------------------------------------------
# User information
# ----------------------------------------------------
form = QFormLayout()
self.username_input = QLineEdit()
if user:
self.username_input.setText(
user["username"]
)
self.username_input.setReadOnly(
True
)
form.addRow(
"Linux username:",
self.username_input,
)
self.enabled_checkbox = QCheckBox(
"Enable user"
)
self.enabled_checkbox.setChecked(
bool(
user["enabled"]
)
if user
else True
)
form.addRow(
"",
self.enabled_checkbox,
)
main_layout.addLayout(
form
)
# ----------------------------------------------------
# Daily allowance
# ----------------------------------------------------
allowance_title = QLabel(
"Daily Allowance"
)
allowance_title.setObjectName(
"SectionTitle"
)
main_layout.addWidget(
allowance_title
)
allowance_description = QLabel(
"Maximum amount of computer time "
"the user can consume each day."
)
allowance_description.setObjectName(
"SecondaryText"
)
allowance_description.setWordWrap(
True
)
main_layout.addWidget(
allowance_description
)
existing_allowances = (
Database.get_allowances(
user["id"]
)
if user
else {}
)
self.day_controls = {}
allowance_grid = QGridLayout()
allowance_grid.setHorizontalSpacing(
15
)
allowance_grid.setVerticalSpacing(
8
)
allowance_grid.addWidget(
QLabel("Day"),
0,
0,
)
allowance_grid.addWidget(
QLabel("Hours"),
0,
1,
)
allowance_grid.addWidget(
QLabel("Minutes"),
0,
2,
)
for row, (
weekday,
name,
) in enumerate(
WEEKDAYS,
start=1,
):
allowance_grid.addWidget(
QLabel(name),
row,
0,
)
hours = QSpinBox()
hours.setRange(
0,
24,
)
minutes = QSpinBox()
minutes.setRange(
0,
59,
)
seconds = existing_allowances.get(
weekday,
0,
)
hours.setValue(
seconds // 3600
)
minutes.setValue(
(seconds % 3600) // 60
)
allowance_grid.addWidget(
hours,
row,
1,
)
allowance_grid.addWidget(
minutes,
row,
2,
)
self.day_controls[
weekday
] = (
hours,
minutes,
)
main_layout.addLayout(
allowance_grid
)
# ----------------------------------------------------
# Access windows
# ----------------------------------------------------
window_title = QLabel(
"Access Windows"
)
window_title.setObjectName(
"SectionTitle"
)
main_layout.addWidget(
window_title
)
window_description = QLabel(
"Define when the user is allowed to "
"use the computer. Disable a day "
"to prevent access on that day."
)
window_description.setObjectName(
"SecondaryText"
)
window_description.setWordWrap(
True
)
main_layout.addWidget(
window_description
)
existing_windows = (
Database.get_access_windows(
user["id"]
)
if user
else {}
)
self.window_controls = {}
windows_grid = QGridLayout()
windows_grid.setHorizontalSpacing(
15
)
windows_grid.setVerticalSpacing(
8
)
windows_grid.addWidget(
QLabel("Day"),
0,
0,
)
windows_grid.addWidget(
QLabel("Allowed"),
0,
1,
)
windows_grid.addWidget(
QLabel("Start"),
0,
2,
)
windows_grid.addWidget(
QLabel("End"),
0,
3,
)
for row, (
weekday,
name,
) in enumerate(
WEEKDAYS,
start=1,
):
windows_grid.addWidget(
QLabel(name),
row,
0,
)
enabled = QCheckBox()
start_time = QTimeEdit()
start_time.setDisplayFormat(
"HH:mm"
)
start_time.setTime(
QTime(16, 0)
)
end_time = QTimeEdit()
end_time.setDisplayFormat(
"HH:mm"
)
end_time.setTime(
QTime(21, 0)
)
existing = existing_windows.get(
weekday,
[]
)
if existing:
start_minute, end_minute = (
existing[0]
)
enabled.setChecked(
True
)
start_time.setTime(
QTime(
start_minute // 60,
start_minute % 60,
)
)
end_time.setTime(
QTime(
end_minute // 60,
end_minute % 60,
)
)
else:
enabled.setChecked(
False
)
start_time.setEnabled(
enabled.isChecked()
)
end_time.setEnabled(
enabled.isChecked()
)
enabled.toggled.connect(
start_time.setEnabled
)
enabled.toggled.connect(
end_time.setEnabled
)
windows_grid.addWidget(
enabled,
row,
1,
)
windows_grid.addWidget(
start_time,
row,
2,
)
windows_grid.addWidget(
end_time,
row,
3,
)
self.window_controls[
weekday
] = (
enabled,
start_time,
end_time,
)
main_layout.addLayout(
windows_grid
)
# ----------------------------------------------------
# Extra time
# ----------------------------------------------------
if user:
extra_title = QLabel(
"Add Extra Time"
)
extra_title.setObjectName(
"SectionTitle"
)
main_layout.addWidget(
extra_title
)
extra_description = QLabel(
"Add temporary time without changing "
"the normal daily allowance."
)
extra_description.setObjectName(
"SecondaryText"
)
extra_description.setWordWrap(
True
)
main_layout.addWidget(
extra_description
)
extra_row = QHBoxLayout()
self.extra_hours = QSpinBox()
self.extra_hours.setRange(
0,
24,
)
self.extra_hours.setSuffix(
" h"
)
self.extra_minutes = QSpinBox()
self.extra_minutes.setRange(
0,
59,
)
self.extra_minutes.setSuffix(
" min"
)
add_time_button = QPushButton(
"Add Time"
)
add_time_button.clicked.connect(
self.add_extra_time
)
extra_row.addWidget(
self.extra_hours
)
extra_row.addWidget(
self.extra_minutes
)
extra_row.addWidget(
add_time_button
)
extra_row.addStretch()
main_layout.addLayout(
extra_row
)
# ----------------------------------------------------
# Dialog buttons
# ----------------------------------------------------
buttons = QDialogButtonBox(
QDialogButtonBox.Save
| QDialogButtonBox.Cancel
)
buttons.accepted.connect(
self.save_user
)
buttons.rejected.connect(
self.reject
)
main_layout.addWidget(
buttons
)
def add_extra_time(self):
hours = self.extra_hours.value()
minutes = self.extra_minutes.value()
seconds = (
hours * 3600
+ minutes * 60
)
if seconds <= 0:
QMessageBox.warning(
self,
"Invalid Time",
"Please enter an amount of time "
"greater than zero.",
)
return
try:
Database.add_temporary_time(
self.user["id"],
seconds,
)
self.extra_hours.setValue(
0
)
self.extra_minutes.setValue(
0
)
QMessageBox.information(
self,
"Time Added",
f"Added {format_seconds(seconds)} "
f"to {self.user['username']}.",
)
except Exception as exc:
QMessageBox.critical(
self,
"Error",
f"Could not add time:\n\n{exc}",
)
def save_user(self):
username = (
self.username_input
.text()
.strip()
)
if not username:
QMessageBox.warning(
self,
"Missing Username",
"Enter a Linux username.",
)
return
if (
not self.user
and not linux_user_exists(
username
)
):
QMessageBox.warning(
self,
"User Not Found",
f"The Linux user "
f"'{username}' does not exist.",
)
return
# ----------------------------------------------------
# Allowances
# ----------------------------------------------------
allowances = {}
for weekday, controls in (
self.day_controls.items()
):
hours, minutes = controls
seconds = (
hours.value() * 3600
+ minutes.value() * 60
)
allowances[
weekday
] = seconds
# ----------------------------------------------------
# Access windows
# ----------------------------------------------------
access_windows = {}
for weekday, controls in (
self.window_controls.items()
):
enabled, start_time, end_time = (
controls
)
if not enabled.isChecked():
continue
start = start_time.time()
end = end_time.time()
start_minute = (
start.hour() * 60
+ start.minute()
)
end_minute = (
end.hour() * 60
+ end.minute()
)
if start_minute == end_minute:
QMessageBox.warning(
self,
"Invalid Access Window",
"Start and end time cannot "
"be the same.",
)
return
access_windows[
weekday
] = [
(
start_minute,
end_minute,
)
]
# ----------------------------------------------------
# Save
# ----------------------------------------------------
try:
if self.user:
Database.update_user(
self.user["id"],
self.enabled_checkbox.isChecked(),
allowances,
access_windows,
)
else:
Database.create_user(
username,
self.enabled_checkbox.isChecked(),
allowances,
access_windows,
)
except sqlite3.IntegrityError:
QMessageBox.warning(
self,
"User Already Exists",
"This user is already managed.",
)
return
except Exception as exc:
QMessageBox.critical(
self,
"Error",
f"Could not save the user:\n\n{exc}",
)
return
self.accept()
# ============================================================
# Users Page
# ============================================================
class UsersPage(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
layout = QVBoxLayout(self)
layout.setContentsMargins(
30,
30,
30,
30,
)
layout.setSpacing(20)
header = QHBoxLayout()
title = QLabel(
"Users"
)
title.setObjectName(
"PageTitle"
)
header.addWidget(title)
header.addStretch()
add_button = QPushButton(
"+ Add User"
)
add_button.clicked.connect(
lambda: self.window().add_user()
)
header.addWidget(
add_button
)
layout.addLayout(
header
)
description = QLabel(
"Manage Linux users and configure "
"their parental-control limits."
)
description.setObjectName(
"SecondaryText"
)
layout.addWidget(
description
)
self.scroll = QScrollArea()
self.scroll.setWidgetResizable(
True
)
self.scroll.setFrameShape(
QFrame.NoFrame
)
self.container = QWidget()
self.users_layout = QVBoxLayout(
self.container
)
self.users_layout.setContentsMargins(
0,
0,
0,
0,
)
self.users_layout.setSpacing(
12
)
self.scroll.setWidget(
self.container
)
layout.addWidget(
self.scroll
)
self.refresh()
def refresh(self):
while self.users_layout.count():
item = self.users_layout.takeAt(0)
widget = item.widget()
if widget:
widget.deleteLater()
users = Database.get_users()
for user in users:
row = QFrame()
row.setObjectName(
"UserRow"
)
row_layout = QHBoxLayout(
row
)
username = QLabel(
user["username"]
)
username.setObjectName(
"UserName"
)
row_layout.addWidget(
username
)
row_layout.addStretch()
status = QLabel(
"Enabled"
if user["enabled"]
else "Disabled"
)
row_layout.addWidget(
status
)
manage = QPushButton(
"Manage User"
)
manage.clicked.connect(
lambda checked=False,
user_id=user["id"]:
self.window().edit_user(
user_id
)
)
row_layout.addWidget(
manage
)
delete = QPushButton(
"Delete"
)
delete.clicked.connect(
lambda checked=False,
user_id=user["id"]:
self.window().delete_user(
user_id
)
)
row_layout.addWidget(
delete
)
self.users_layout.addWidget(
row
)
self.users_layout.addStretch()
# ============================================================
# Main Window
# ============================================================
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle(
"Linux Parental Control"
)
self.resize(
1100,
750,
)
self.pages = QStackedWidget()
self.dashboard = DashboardPage(
edit_callback=self.edit_user,
toggle_callback=self.toggle_user,
)
self.users = UsersPage()
self.pages.addWidget(
self.dashboard
)
self.pages.addWidget(
self.users
)
self.create_sidebar()
# --------------------------------------------------------
# Sidebar
# --------------------------------------------------------
def create_sidebar(self):
central = QWidget()
main_layout = QHBoxLayout(
central
)
main_layout.setContentsMargins(
0,
0,
0,
0,
)
main_layout.setSpacing(
0
)
sidebar = QFrame()
sidebar.setObjectName(
"Sidebar"
)
sidebar.setFixedWidth(
220
)
sidebar_layout = QVBoxLayout(
sidebar
)
sidebar_layout.setContentsMargins(
15,
20,
15,
20,
)
sidebar_layout.setSpacing(
8
)
title = QLabel(
"Parental Control"
)
title.setObjectName(
"SidebarTitle"
)
sidebar_layout.addWidget(
title
)
sidebar_layout.addSpacing(
20
)
dashboard_button = QPushButton(
"Dashboard"
)
dashboard_button.clicked.connect(
lambda: self.show_page(0)
)
sidebar_layout.addWidget(
dashboard_button
)
users_button = QPushButton(
"Users"
)
users_button.clicked.connect(
lambda: self.show_page(1)
)
sidebar_layout.addWidget(
users_button
)
sidebar_layout.addStretch()
refresh_button = QPushButton(
"Refresh"
)
refresh_button.clicked.connect(
self.refresh_all
)
sidebar_layout.addWidget(
refresh_button
)
main_layout.addWidget(
sidebar
)
main_layout.addWidget(
self.pages
)
self.setCentralWidget(
central
)
def show_page(self, index):
self.pages.setCurrentIndex(
index
)
# --------------------------------------------------------
# Add user
# --------------------------------------------------------
def add_user(self):
dialog = UserDialog(
parent=self
)
if dialog.exec():
self.refresh_all()
# --------------------------------------------------------
# Manage user
# --------------------------------------------------------
def edit_user(self, user_id):
user = Database.get_user(
user_id
)
if not user:
QMessageBox.warning(
self,
"User Not Found",
"The selected user no longer exists.",
)
return
dialog = UserDialog(
user=user,
parent=self,
)
if dialog.exec():
self.refresh_all()
# --------------------------------------------------------
# Enable / Disable
# --------------------------------------------------------
def toggle_user(
self,
user_id,
enabled,
):
user = Database.get_user(
user_id
)
if not user:
return
action = (
"enable"
if enabled
else "disable"
)
reply = QMessageBox.question(
self,
f"{action.title()} User",
f"Are you sure you want to "
f"{action} user "
f"\"{user['username']}\"?",
QMessageBox.Yes
| QMessageBox.No,
)
if reply != QMessageBox.Yes:
return
try:
Database.set_enabled(
user_id,
enabled,
)
except Exception as exc:
QMessageBox.critical(
self,
"Error",
f"Could not update the user:\n\n{exc}",
)
return
self.refresh_all()
# --------------------------------------------------------
# Delete user
# --------------------------------------------------------
def delete_user(self, user_id):
user = Database.get_user(
user_id
)
if not user:
return
reply = QMessageBox.question(
self,
"Delete Managed User",
f"Remove "
f"\"{user['username']}\" "
f"from parental control?\n\n"
f"This will NOT delete the Linux account.",
QMessageBox.Yes
| QMessageBox.No,
)
if reply != QMessageBox.Yes:
return
try:
Database.delete_user(
user_id
)
except Exception as exc:
QMessageBox.critical(
self,
"Error",
f"Could not delete the user:\n\n{exc}",
)
return
self.refresh_all()
# --------------------------------------------------------
# Refresh
# --------------------------------------------------------
def refresh_all(self):
self.dashboard.refresh()
self.users.refresh()
# ============================================================
# Theme
# ============================================================
def apply_theme(app):
app.setStyleSheet(
"""
QWidget {
font-family: "Noto Sans";
font-size: 14px;
}
QMainWindow {
background: #f5f5f5;
}
QFrame#Sidebar {
background: #202124;
}
QLabel#SidebarTitle {
color: white;
font-size: 20px;
font-weight: bold;
}
QPushButton {
padding: 9px 14px;
border-radius: 7px;
border: 1px solid #cccccc;
background: white;
}
QPushButton:hover {
background: #eeeeee;
}
QPushButton:pressed {
background: #dddddd;
}
QFrame#Sidebar QPushButton {
color: white;
background: transparent;
border: none;
text-align: left;
padding: 12px;
}
QFrame#Sidebar QPushButton:hover {
background: #303134;
}
QLabel#PageTitle {
font-size: 28px;
font-weight: bold;
}
QLabel#SectionTitle {
font-size: 18px;
font-weight: bold;
}
QLabel#UserName {
font-size: 18px;
font-weight: bold;
}
QLabel#SecondaryText {
color: #666666;
}
QLabel#TimeRemaining {
font-size: 22px;
font-weight: bold;
}
QLabel#StatusEnabled {
color: #188038;
font-weight: bold;
}
QLabel#StatusDisabled {
color: #d93025;
font-weight: bold;
}
QLabel#EmptyText {
color: #777777;
font-size: 16px;
padding: 40px;
}
QFrame#UserCard {
background: white;
border: 1px solid #dddddd;
border-radius: 12px;
}
QFrame#UserRow {
background: white;
border: 1px solid #dddddd;
border-radius: 8px;
}
QProgressBar {
border: none;
border-radius: 5px;
background: #e5e5e5;
height: 10px;
}
QProgressBar::chunk {
border-radius: 5px;
background: #4f7cff;
}
QLineEdit,
QSpinBox,
QTimeEdit {
padding: 7px;
border: 1px solid #cccccc;
border-radius: 6px;
background: white;
}
QCheckBox {
spacing: 8px;
}
QScrollArea {
background: transparent;
}
"""
)
# ============================================================
# Main
# ============================================================
def main():
app = QApplication(sys.argv)
app.setApplicationName(
"Linux Parental Control"
)
apply_theme(app)
window = MainWindow()
window.show()
sys.exit(
app.exec()
)
if __name__ == "__main__":
main()