verynicegui 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- verynicegui/__init__.py +31 -0
- verynicegui/__main__.py +4 -0
- verynicegui/admin.py +351 -0
- verynicegui/app.py +209 -0
- verynicegui/auth.py +119 -0
- verynicegui/card.py +74 -0
- verynicegui/cli.py +204 -0
- verynicegui/i18n.py +55 -0
- verynicegui/layout.py +349 -0
- verynicegui/table.py +155 -0
- verynicegui/templates.py +177 -0
- verynicegui-1.0.0.dist-info/METADATA +278 -0
- verynicegui-1.0.0.dist-info/RECORD +16 -0
- verynicegui-1.0.0.dist-info/WHEEL +5 -0
- verynicegui-1.0.0.dist-info/entry_points.txt +2 -0
- verynicegui-1.0.0.dist-info/top_level.txt +1 -0
verynicegui/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from verynicegui.app import VeryNiceGUI
|
|
2
|
+
from verynicegui.layout import VeryNiceLayout, RightDrawer
|
|
3
|
+
from verynicegui.table import VeryNiceTable
|
|
4
|
+
from verynicegui.card import VeryNiceCard
|
|
5
|
+
from verynicegui.i18n import tr, set_language
|
|
6
|
+
from verynicegui.auth import (
|
|
7
|
+
get_current_user,
|
|
8
|
+
is_admin,
|
|
9
|
+
has_permission,
|
|
10
|
+
do_login,
|
|
11
|
+
do_logout,
|
|
12
|
+
load_users,
|
|
13
|
+
save_users,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"VeryNiceGUI",
|
|
18
|
+
"VeryNiceLayout",
|
|
19
|
+
"RightDrawer",
|
|
20
|
+
"VeryNiceTable",
|
|
21
|
+
"VeryNiceCard",
|
|
22
|
+
"tr",
|
|
23
|
+
"set_language",
|
|
24
|
+
"get_current_user",
|
|
25
|
+
"is_admin",
|
|
26
|
+
"has_permission",
|
|
27
|
+
"do_login",
|
|
28
|
+
"do_logout",
|
|
29
|
+
"load_users",
|
|
30
|
+
"save_users",
|
|
31
|
+
]
|
verynicegui/__main__.py
ADDED
verynicegui/admin.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
from nicegui import ui
|
|
2
|
+
from verynicegui.auth import (
|
|
3
|
+
load_users,
|
|
4
|
+
save_users,
|
|
5
|
+
hash_password,
|
|
6
|
+
is_admin,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _role_badge(role: str) -> None:
|
|
11
|
+
"""Renders a colorful badge representing the user's role."""
|
|
12
|
+
if role == "admin":
|
|
13
|
+
color = "background:#6366f1;color:white"
|
|
14
|
+
label = "Admin"
|
|
15
|
+
icon_name = "verified"
|
|
16
|
+
else:
|
|
17
|
+
color = "background:#0ea5e9;color:white"
|
|
18
|
+
label = "Kullanıcı"
|
|
19
|
+
icon_name = "person"
|
|
20
|
+
|
|
21
|
+
with ui.element("div").style(
|
|
22
|
+
f"display:inline-flex;align-items:center;gap:4px;padding:4px 12px;"
|
|
23
|
+
f"border-radius:20px;font-size:12px;font-weight:600;{color}"
|
|
24
|
+
):
|
|
25
|
+
ui.icon(icon_name, size="14px")
|
|
26
|
+
ui.label(label).style("font-size:11px;font-weight:600")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _perm_chip(label: str) -> None:
|
|
30
|
+
"""Renders a custom permission chip styled for dark/light themes."""
|
|
31
|
+
ui.element("span").classes("mn-perm-chip").text = label
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_available_categories(app_instance) -> list[dict]:
|
|
35
|
+
"""Retrieves all distinct categories defined in the menu hierarchy."""
|
|
36
|
+
categories = []
|
|
37
|
+
menu = app_instance.left_drawer_config.get("menu", [])
|
|
38
|
+
for item in menu:
|
|
39
|
+
category = item.get("category")
|
|
40
|
+
if category and category != "admin":
|
|
41
|
+
categories.append({
|
|
42
|
+
"category": category,
|
|
43
|
+
"title": item.get("title", category),
|
|
44
|
+
"icon": item.get("icon", "circle")
|
|
45
|
+
})
|
|
46
|
+
return categories
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def admin_page(app_instance) -> None:
|
|
50
|
+
"""Renders the built-in Admin User Management Dashboard."""
|
|
51
|
+
if not is_admin():
|
|
52
|
+
ui.navigate.to("/403")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
card_style = "background-color:var(--mn-card-bg); border:var(--mn-card-border)"
|
|
56
|
+
text_primary = "color:var(--mn-text-primary)"
|
|
57
|
+
text_secondary = "color:var(--mn-text-secondary)"
|
|
58
|
+
|
|
59
|
+
# Page Header
|
|
60
|
+
with ui.row().classes("items-center justify-between w-full mb-2"):
|
|
61
|
+
with ui.column().classes("gap-1"):
|
|
62
|
+
ui.label("Kullanıcı Yönetimi").classes("text-3xl font-bold").style(text_primary)
|
|
63
|
+
ui.label("SaaS platformu kullanıcılarını oluşturun, düzenleyin ve sayfa izinlerini yönetin.").style(text_secondary + ";font-size:14px")
|
|
64
|
+
|
|
65
|
+
ui.button(
|
|
66
|
+
"Yeni Kullanıcı",
|
|
67
|
+
icon="person_add",
|
|
68
|
+
on_click=lambda: create_dialog.open(),
|
|
69
|
+
).props("unelevated no-caps color=indigo").style("border-radius:12px; padding: 10px 20px")
|
|
70
|
+
|
|
71
|
+
# Refreshable Statistics Cards
|
|
72
|
+
@ui.refreshable
|
|
73
|
+
def stats_row():
|
|
74
|
+
users = load_users()
|
|
75
|
+
admin_count = sum(1 for u in users.values() if u.get("role") == "admin" or "*" in u.get("permissions", []))
|
|
76
|
+
total_count = len(users)
|
|
77
|
+
|
|
78
|
+
stat_cards = [
|
|
79
|
+
("group", str(total_count), "Toplam Kullanıcı", "#6366f1"),
|
|
80
|
+
("admin_panel_settings", str(admin_count), "Yönetici (Admin)", "#a855f7"),
|
|
81
|
+
("shield", str(total_count - admin_count), "Standart Kullanıcı", "#0ea5e9"),
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
with ui.row().classes("w-full gap-4 mb-4"):
|
|
85
|
+
for icon_name, value, label_text, color in stat_cards:
|
|
86
|
+
with ui.element("div").style(
|
|
87
|
+
f"{card_style};border-radius:16px;"
|
|
88
|
+
f"padding:20px 24px;flex:1;display:flex;align-items:center;gap:20px"
|
|
89
|
+
):
|
|
90
|
+
with ui.element("div").style(
|
|
91
|
+
f"width:52px;height:52px;border-radius:14px;"
|
|
92
|
+
f"background:{color}15;display:flex;align-items:center;justify-content:center;flex-shrink:0"
|
|
93
|
+
):
|
|
94
|
+
ui.icon(icon_name, size="28px").style(f"color:{color}")
|
|
95
|
+
with ui.column().classes("gap-0"):
|
|
96
|
+
ui.label(value).style(f"{text_primary};font-size:28px;font-weight:800;line-height:1")
|
|
97
|
+
ui.label(label_text).style(f"{text_secondary};font-size:13px;font-weight:500;margin-top:4px")
|
|
98
|
+
|
|
99
|
+
stats_row()
|
|
100
|
+
|
|
101
|
+
# Refreshable Users List
|
|
102
|
+
@ui.refreshable
|
|
103
|
+
def user_list():
|
|
104
|
+
users = load_users()
|
|
105
|
+
categories = get_available_categories(app_instance)
|
|
106
|
+
|
|
107
|
+
if not users:
|
|
108
|
+
ui.label("Henüz kayıtlı kullanıcı bulunmamaktadır.").style(text_secondary)
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
with ui.column().classes("w-full gap-4"):
|
|
112
|
+
for username, user_data in users.items():
|
|
113
|
+
_user_card(username, user_data, categories, card_style, text_primary, text_secondary)
|
|
114
|
+
|
|
115
|
+
def _user_card(uname, udata, categories, card_style, text_primary, text_secondary):
|
|
116
|
+
with ui.element("div").style(
|
|
117
|
+
f"{card_style};border-radius:16px;padding:20px 24px;"
|
|
118
|
+
f"display:flex;align-items:center;gap:20px;transition:box-shadow 0.2s"
|
|
119
|
+
):
|
|
120
|
+
# Initials Avatar
|
|
121
|
+
display_name = udata.get("display_name") or uname
|
|
122
|
+
initials = (display_name[:1] or "?").upper()
|
|
123
|
+
av_color = "#6366f1" if udata.get("role") == "admin" else "#0ea5e9"
|
|
124
|
+
with ui.element("div").style(
|
|
125
|
+
f"width:48px;height:48px;border-radius:50%;"
|
|
126
|
+
f"background:{av_color};display:flex;align-items:center;"
|
|
127
|
+
f"justify-content:center;flex-shrink:0;font-size:18px;font-weight:700;color:white"
|
|
128
|
+
):
|
|
129
|
+
ui.label(initials)
|
|
130
|
+
|
|
131
|
+
# Details Area
|
|
132
|
+
with ui.column().classes("gap-1").style("flex:1;min-width:0"):
|
|
133
|
+
with ui.row().classes("items-center gap-3 flex-wrap"):
|
|
134
|
+
ui.label(display_name).style(f"{text_primary};font-size:16px;font-weight:700")
|
|
135
|
+
ui.label(f"@{uname}").style(f"{text_secondary};font-size:12px")
|
|
136
|
+
_role_badge(udata.get("role", "user"))
|
|
137
|
+
|
|
138
|
+
# Permissions Chip Display
|
|
139
|
+
with ui.row().classes("items-center gap-1 flex-wrap mt-1"):
|
|
140
|
+
perms = udata.get("permissions", [])
|
|
141
|
+
if "*" in perms:
|
|
142
|
+
_perm_chip("Tüm Sayfalar (Admin)")
|
|
143
|
+
else:
|
|
144
|
+
rendered_any = False
|
|
145
|
+
for cat in categories:
|
|
146
|
+
if cat["category"] in perms:
|
|
147
|
+
_perm_chip(cat["title"])
|
|
148
|
+
rendered_any = True
|
|
149
|
+
if not rendered_any:
|
|
150
|
+
ui.label("Erişim Yetkisi Yok").style(f"{text_secondary};font-size:12px;font-style:italic")
|
|
151
|
+
|
|
152
|
+
# Actions Button Group
|
|
153
|
+
with ui.row().classes("items-center gap-2 flex-shrink-0"):
|
|
154
|
+
ui.button(icon="edit", on_click=lambda u=uname, d=udata: open_edit_dialog(u, d)).props(
|
|
155
|
+
"flat round color=indigo dense"
|
|
156
|
+
).tooltip("Düzenle")
|
|
157
|
+
|
|
158
|
+
if uname != "admin": # Protect primary admin user
|
|
159
|
+
ui.button(
|
|
160
|
+
icon="delete",
|
|
161
|
+
on_click=lambda u=uname: confirm_delete(u),
|
|
162
|
+
).props("flat round color=negative dense").tooltip("Sil")
|
|
163
|
+
|
|
164
|
+
user_list()
|
|
165
|
+
|
|
166
|
+
# ─── CREATE USER DIALOG ────────────────────────────────────────────────
|
|
167
|
+
with ui.dialog() as create_dialog, ui.card().style(
|
|
168
|
+
f"{card_style};border-radius:20px;padding:32px;width:480px;max-width:95vw"
|
|
169
|
+
):
|
|
170
|
+
ui.label("Yeni Kullanıcı").style(f"{text_primary};font-size:20px;font-weight:700")
|
|
171
|
+
ui.label("Kullanıcı profil bilgilerini ve sayfa yetkilerini ayarlayın.").style(
|
|
172
|
+
f"{text_secondary};font-size:13px;margin-top:-8px"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
new_uname = ui.input("Kullanıcı Adı").classes("w-full").props("outlined")
|
|
176
|
+
new_dname = ui.input("Görünen Ad").classes("w-full").props("outlined")
|
|
177
|
+
new_pwd = ui.input("Şifre", password=True, password_toggle_button=True).classes("w-full").props("outlined")
|
|
178
|
+
|
|
179
|
+
ui.label("Sayfa Kategorileri İzinleri").style(
|
|
180
|
+
f"{text_primary};font-size:14px;font-weight:600;margin-top:8px"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
categories = get_available_categories(app_instance)
|
|
184
|
+
perm_switches: dict[str, ui.switch] = {}
|
|
185
|
+
|
|
186
|
+
with ui.column().classes("w-full gap-2"):
|
|
187
|
+
for cat in categories:
|
|
188
|
+
with ui.row().classes("items-center justify-between w-full"):
|
|
189
|
+
with ui.row().classes("items-center gap-2"):
|
|
190
|
+
ui.icon(cat["icon"], size="18px").style("color:#6366f1")
|
|
191
|
+
ui.label(cat["title"]).style(f"{text_primary};font-size:14px")
|
|
192
|
+
sw = ui.switch().props("color=indigo")
|
|
193
|
+
perm_switches[cat["category"]] = sw
|
|
194
|
+
|
|
195
|
+
ui.separator().style("margin:8px 0")
|
|
196
|
+
|
|
197
|
+
with ui.row().classes("justify-end gap-2 w-full"):
|
|
198
|
+
ui.button("İptal", on_click=create_dialog.close).props("flat no-caps color=grey")
|
|
199
|
+
|
|
200
|
+
def do_create():
|
|
201
|
+
uname = (new_uname.value or "").strip()
|
|
202
|
+
pwd = new_pwd.value or ""
|
|
203
|
+
dname = (new_dname.value or "").strip() or uname
|
|
204
|
+
|
|
205
|
+
if not uname:
|
|
206
|
+
ui.notify("Kullanıcı adı gereklidir.", type="warning")
|
|
207
|
+
return
|
|
208
|
+
if len(pwd) < 4:
|
|
209
|
+
ui.notify("Şifre en az 4 karakter olmalıdır.", type="warning")
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
users = load_users()
|
|
213
|
+
if uname in users:
|
|
214
|
+
ui.notify(f"'{uname}' kullanıcısı zaten mevcut.", type="warning")
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
perms = [cat for cat, sw in perm_switches.items() if sw.value]
|
|
218
|
+
users[uname] = {
|
|
219
|
+
"password_hash": hash_password(pwd),
|
|
220
|
+
"role": "user",
|
|
221
|
+
"permissions": perms,
|
|
222
|
+
"display_name": dname,
|
|
223
|
+
"theme": "light"
|
|
224
|
+
}
|
|
225
|
+
save_users(users)
|
|
226
|
+
|
|
227
|
+
# Reset form fields
|
|
228
|
+
new_uname.value = ""
|
|
229
|
+
new_dname.value = ""
|
|
230
|
+
new_pwd.value = ""
|
|
231
|
+
for sw in perm_switches.values():
|
|
232
|
+
sw.value = False
|
|
233
|
+
|
|
234
|
+
create_dialog.close()
|
|
235
|
+
user_list.refresh()
|
|
236
|
+
stats_row.refresh()
|
|
237
|
+
ui.notify(f"'{uname}' kullanıcısı oluşturuldu.", type="positive", icon="check_circle")
|
|
238
|
+
|
|
239
|
+
ui.button("Oluştur", on_click=do_create).props("unelevated no-caps color=indigo").style("border-radius:10px")
|
|
240
|
+
|
|
241
|
+
# ─── EDIT USER DIALOG ──────────────────────────────────────────────────
|
|
242
|
+
edit_dialog = ui.dialog()
|
|
243
|
+
|
|
244
|
+
def open_edit_dialog(uname: str, udata: dict):
|
|
245
|
+
edit_dialog.clear()
|
|
246
|
+
|
|
247
|
+
with edit_dialog, ui.card().style(
|
|
248
|
+
f"{card_style};border-radius:20px;padding:32px;width:480px;max-width:95vw"
|
|
249
|
+
):
|
|
250
|
+
ui.label(f"Düzenle: @{uname}").style(f"{text_primary};font-size:20px;font-weight:700")
|
|
251
|
+
|
|
252
|
+
categories = get_available_categories(app_instance)
|
|
253
|
+
current_perms = udata.get("permissions", [])
|
|
254
|
+
is_super = "*" in current_perms
|
|
255
|
+
|
|
256
|
+
edit_dname = ui.input("Görünen Ad", value=udata.get("display_name", uname)).classes("w-full").props("outlined")
|
|
257
|
+
edit_pwd = ui.input("Yeni Şifre (boş bırakılırsa değişmez)", password=True, password_toggle_button=True).classes("w-full").props("outlined")
|
|
258
|
+
|
|
259
|
+
if is_super:
|
|
260
|
+
with ui.row().classes("items-center gap-3 w-full").style(
|
|
261
|
+
"background:rgba(99,102,241,0.1);border-radius:12px;padding:12px 16px;margin-top:8px"
|
|
262
|
+
):
|
|
263
|
+
ui.icon("verified", size="20px").style("color:#6366f1")
|
|
264
|
+
ui.label("Bu hesap tüm yetkilere sahiptir (Admin).").style(f"{text_secondary};font-size:13px")
|
|
265
|
+
edit_perm_switches = {}
|
|
266
|
+
else:
|
|
267
|
+
ui.label("Sayfa Kategorileri İzinleri").style(
|
|
268
|
+
f"{text_primary};font-size:14px;font-weight:600;margin-top:8px"
|
|
269
|
+
)
|
|
270
|
+
edit_perm_switches: dict[str, ui.switch] = {}
|
|
271
|
+
|
|
272
|
+
with ui.column().classes("w-full gap-2"):
|
|
273
|
+
for cat in categories:
|
|
274
|
+
with ui.row().classes("items-center justify-between w-full"):
|
|
275
|
+
with ui.row().classes("items-center gap-2"):
|
|
276
|
+
ui.icon(cat["icon"], size="18px").style("color:#6366f1")
|
|
277
|
+
ui.label(cat["title"]).style(f"{text_primary};font-size:14px")
|
|
278
|
+
sw = ui.switch(value=cat["category"] in current_perms).props("color=indigo")
|
|
279
|
+
edit_perm_switches[cat["category"]] = sw
|
|
280
|
+
|
|
281
|
+
ui.separator().style("margin:8px 0")
|
|
282
|
+
|
|
283
|
+
with ui.row().classes("justify-end gap-2 w-full"):
|
|
284
|
+
ui.button("İptal", on_click=edit_dialog.close).props("flat no-caps color=grey")
|
|
285
|
+
|
|
286
|
+
def do_save(un=uname, is_sup=is_super, sw_map=edit_perm_switches):
|
|
287
|
+
users = load_users()
|
|
288
|
+
if un not in users:
|
|
289
|
+
ui.notify("Kullanıcı bulunamadı.", type="negative")
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
dname = (edit_dname.value or "").strip()
|
|
293
|
+
if dname:
|
|
294
|
+
users[un]["display_name"] = dname
|
|
295
|
+
|
|
296
|
+
pwd = edit_pwd.value or ""
|
|
297
|
+
if pwd:
|
|
298
|
+
if len(pwd) < 4:
|
|
299
|
+
ui.notify("Şifre en az 4 karakter olmalıdır.", type="warning")
|
|
300
|
+
return
|
|
301
|
+
users[un]["password_hash"] = hash_password(pwd)
|
|
302
|
+
|
|
303
|
+
if not is_sup:
|
|
304
|
+
users[un]["permissions"] = [
|
|
305
|
+
cat for cat, sw in sw_map.items() if sw.value
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
save_users(users)
|
|
309
|
+
edit_dialog.close()
|
|
310
|
+
user_list.refresh()
|
|
311
|
+
ui.notify("Değişiklikler kaydedildi.", type="positive", icon="check_circle")
|
|
312
|
+
|
|
313
|
+
ui.button("Kaydet", on_click=do_save).props("unelevated no-caps color=indigo").style("border-radius:10px")
|
|
314
|
+
|
|
315
|
+
edit_dialog.open()
|
|
316
|
+
|
|
317
|
+
# ─── CONFIRM DELETE DIALOG ─────────────────────────────────────────────
|
|
318
|
+
def confirm_delete(uname: str):
|
|
319
|
+
with ui.dialog() as del_dialog, ui.card().style(
|
|
320
|
+
f"{card_style};border-radius:20px;padding:28px;width:380px"
|
|
321
|
+
):
|
|
322
|
+
with ui.row().classes("items-center gap-3 mb-2"):
|
|
323
|
+
with ui.element("div").style(
|
|
324
|
+
"width:44px;height:44px;background:rgba(239,68,68,0.12);"
|
|
325
|
+
"border-radius:12px;display:flex;align-items:center;justify-content:center"
|
|
326
|
+
):
|
|
327
|
+
ui.icon("warning", size="24px").style("color:#ef4444")
|
|
328
|
+
ui.label("Kullanıcıyı Sil").style(f"{text_primary};font-size:18px;font-weight:700")
|
|
329
|
+
|
|
330
|
+
ui.label(
|
|
331
|
+
f"'{uname}' kullanıcısını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz."
|
|
332
|
+
).style(f"{text_secondary};font-size:14px")
|
|
333
|
+
|
|
334
|
+
ui.separator().style("margin:12px 0")
|
|
335
|
+
|
|
336
|
+
with ui.row().classes("justify-end gap-2 w-full"):
|
|
337
|
+
ui.button("Vazgeç", on_click=del_dialog.close).props("flat no-caps color=grey")
|
|
338
|
+
|
|
339
|
+
def do_delete(u=uname):
|
|
340
|
+
users = load_users()
|
|
341
|
+
if u in users:
|
|
342
|
+
del users[u]
|
|
343
|
+
save_users(users)
|
|
344
|
+
del_dialog.close()
|
|
345
|
+
user_list.refresh()
|
|
346
|
+
stats_row.refresh()
|
|
347
|
+
ui.notify(f"'{u}' kullanıcısı başarıyla silindi.", type="info", icon="delete")
|
|
348
|
+
|
|
349
|
+
ui.button("Evet, Sil", on_click=do_delete).props("unelevated no-caps color=negative").style("border-radius:10px")
|
|
350
|
+
|
|
351
|
+
del_dialog.open()
|
verynicegui/app.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from nicegui import ui
|
|
3
|
+
from verynicegui.layout import VeryNiceLayout, RightDrawer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class VeryNiceGUI:
|
|
7
|
+
"""The main application wrapper for verynicegui.
|
|
8
|
+
|
|
9
|
+
Handles configuration, default page routing (login, 403, admin),
|
|
10
|
+
and automatic page layout injection via the @app.page decorator.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, title: str = "VeryNiceGUI App", header: dict = None, left_drawer: dict = None, footer: dict = None, translations: dict = None):
|
|
14
|
+
# Configure Header
|
|
15
|
+
self.header_config = {
|
|
16
|
+
"logo": "radar",
|
|
17
|
+
"title": title,
|
|
18
|
+
"show_theme_toggle": True
|
|
19
|
+
}
|
|
20
|
+
if header:
|
|
21
|
+
self.header_config.update(header)
|
|
22
|
+
|
|
23
|
+
# Configure Left Drawer
|
|
24
|
+
self.left_drawer_config = {
|
|
25
|
+
"menu": [],
|
|
26
|
+
"show_settings": True,
|
|
27
|
+
"title": "NAVİGASYON"
|
|
28
|
+
}
|
|
29
|
+
if left_drawer:
|
|
30
|
+
self.left_drawer_config.update(left_drawer)
|
|
31
|
+
|
|
32
|
+
# Configure Footer
|
|
33
|
+
self.footer_config = {
|
|
34
|
+
"left": "",
|
|
35
|
+
"middle": "",
|
|
36
|
+
"right": ""
|
|
37
|
+
}
|
|
38
|
+
if footer:
|
|
39
|
+
self.footer_config.update(footer)
|
|
40
|
+
|
|
41
|
+
# Add global head script and styles for preventing theme loading flashes
|
|
42
|
+
ui.add_head_html("""
|
|
43
|
+
<script>
|
|
44
|
+
(function() {
|
|
45
|
+
var theme = 'light';
|
|
46
|
+
var match = document.cookie.match(new RegExp('(^| )theme=([^;]+)'));
|
|
47
|
+
if (match) theme = match[2];
|
|
48
|
+
if (theme === 'dark') {
|
|
49
|
+
document.documentElement.classList.add('body--dark');
|
|
50
|
+
} else {
|
|
51
|
+
document.documentElement.classList.remove('body--dark');
|
|
52
|
+
}
|
|
53
|
+
})();
|
|
54
|
+
</script>
|
|
55
|
+
<style>
|
|
56
|
+
html, body, .q-layout, .q-page-container {
|
|
57
|
+
background-color: #f8fafc !important;
|
|
58
|
+
}
|
|
59
|
+
html.body--dark, html.body--dark body, html.body--dark .q-layout, html.body--dark .q-page-container,
|
|
60
|
+
body.body--dark, body.body--dark .q-layout, body.body--dark .q-page-container {
|
|
61
|
+
background-color: #0f172a !important;
|
|
62
|
+
}
|
|
63
|
+
</style>
|
|
64
|
+
<meta name="darkreader-lock">
|
|
65
|
+
""", shared=True)
|
|
66
|
+
|
|
67
|
+
# Register custom translations
|
|
68
|
+
if translations:
|
|
69
|
+
from verynicegui.i18n import custom_translations
|
|
70
|
+
for lang, trans_dict in translations.items():
|
|
71
|
+
if lang not in custom_translations:
|
|
72
|
+
custom_translations[lang] = {}
|
|
73
|
+
custom_translations[lang].update(trans_dict)
|
|
74
|
+
|
|
75
|
+
self.registered_routes = set()
|
|
76
|
+
|
|
77
|
+
def page(self, route: str, title: str = None, allowed_categories: list[str] = None, standalone: bool = False):
|
|
78
|
+
"""Decorator to register a page route.
|
|
79
|
+
|
|
80
|
+
Automatically wraps pages with authentication checks, authorization (RBAC),
|
|
81
|
+
and layout injection (SaaSLayout).
|
|
82
|
+
If the page function signature contains a parameter named 'drawer' or 'right_drawer',
|
|
83
|
+
a toggleable RightDrawer component is instantiated and injected.
|
|
84
|
+
"""
|
|
85
|
+
self.registered_routes.add(route)
|
|
86
|
+
|
|
87
|
+
def decorator(func):
|
|
88
|
+
# Inspect signature to see if page function requests the right drawer or request
|
|
89
|
+
sig = inspect.signature(func)
|
|
90
|
+
drawer_param_name = None
|
|
91
|
+
for param in sig.parameters.values():
|
|
92
|
+
if param.name in ("drawer", "right_drawer"):
|
|
93
|
+
drawer_param_name = param.name
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
from fastapi import Request
|
|
97
|
+
|
|
98
|
+
@ui.page(route)
|
|
99
|
+
def page_wrapper(request: Request):
|
|
100
|
+
# Resolve arguments for user-defined function
|
|
101
|
+
func_kwargs = {}
|
|
102
|
+
if "request" in sig.parameters:
|
|
103
|
+
func_kwargs["request"] = request
|
|
104
|
+
|
|
105
|
+
if standalone:
|
|
106
|
+
func(**func_kwargs)
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
# Auth Guard
|
|
110
|
+
from verynicegui.auth import require_login
|
|
111
|
+
if not require_login():
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# RBAC Category Guard
|
|
115
|
+
if allowed_categories:
|
|
116
|
+
from verynicegui.auth import has_permission
|
|
117
|
+
authorized = any(has_permission(cat) for cat in allowed_categories)
|
|
118
|
+
if not authorized:
|
|
119
|
+
ui.navigate.to("/403")
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
# Instantiate right drawer if requested by the page function
|
|
123
|
+
right_drawer = None
|
|
124
|
+
if drawer_param_name:
|
|
125
|
+
right_drawer = RightDrawer()
|
|
126
|
+
func_kwargs[drawer_param_name] = right_drawer
|
|
127
|
+
|
|
128
|
+
# Render page inside standard VeryNiceLayout
|
|
129
|
+
with VeryNiceLayout(
|
|
130
|
+
title=title or self.header_config.get("title", "Page"),
|
|
131
|
+
app_instance=self,
|
|
132
|
+
right_drawer=right_drawer,
|
|
133
|
+
current_route=route
|
|
134
|
+
):
|
|
135
|
+
func(**func_kwargs)
|
|
136
|
+
|
|
137
|
+
return page_wrapper
|
|
138
|
+
return decorator
|
|
139
|
+
|
|
140
|
+
def _register_default_routes(self):
|
|
141
|
+
"""Auto-registers system routes (login, logout, 403, and admin) if not overridden."""
|
|
142
|
+
from verynicegui.templates import login_page, forbidden_page
|
|
143
|
+
from verynicegui.auth import do_logout, require_login, is_admin
|
|
144
|
+
from verynicegui.admin import admin_page
|
|
145
|
+
|
|
146
|
+
# Login page (Standalone)
|
|
147
|
+
if "/login" not in self.registered_routes:
|
|
148
|
+
@ui.page("/login")
|
|
149
|
+
def login_route():
|
|
150
|
+
login_page(
|
|
151
|
+
app_title=self.header_config.get("title", "VeryNiceGUI"),
|
|
152
|
+
logo_icon=self.header_config.get("logo", "radar")
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Forbidden page (Standalone)
|
|
156
|
+
if "/403" not in self.registered_routes:
|
|
157
|
+
@ui.page("/403")
|
|
158
|
+
def forbidden_route():
|
|
159
|
+
if not require_login():
|
|
160
|
+
return
|
|
161
|
+
forbidden_page(app_title=self.header_config.get("title", "VeryNiceGUI"))
|
|
162
|
+
|
|
163
|
+
# Logout route (API / Action redirect)
|
|
164
|
+
if "/logout" not in self.registered_routes:
|
|
165
|
+
@ui.page("/logout")
|
|
166
|
+
def logout_route():
|
|
167
|
+
do_logout()
|
|
168
|
+
ui.navigate.to("/login")
|
|
169
|
+
|
|
170
|
+
# Admin route (Built-in user management with standard layout)
|
|
171
|
+
if "/admin" not in self.registered_routes:
|
|
172
|
+
@ui.page("/admin")
|
|
173
|
+
def admin_route():
|
|
174
|
+
if not require_login():
|
|
175
|
+
return
|
|
176
|
+
if not is_admin():
|
|
177
|
+
ui.navigate.to("/403")
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
with VeryNiceLayout(
|
|
181
|
+
title="Ayarlar / Admin",
|
|
182
|
+
app_instance=self,
|
|
183
|
+
current_route="/admin"
|
|
184
|
+
):
|
|
185
|
+
admin_page(app_instance=self)
|
|
186
|
+
|
|
187
|
+
def run(self, **kwargs):
|
|
188
|
+
"""Initializes default system routes and runs NiceGUI."""
|
|
189
|
+
# Auto-serve assets directory relative to the running entrypoint script
|
|
190
|
+
import sys
|
|
191
|
+
from pathlib import Path
|
|
192
|
+
if sys.argv and sys.argv[0]:
|
|
193
|
+
entry_dir = Path(sys.argv[0]).parent.resolve()
|
|
194
|
+
assets_path = entry_dir / "assets"
|
|
195
|
+
if assets_path.exists() and assets_path.is_dir():
|
|
196
|
+
from nicegui import app as nicegui_app
|
|
197
|
+
try:
|
|
198
|
+
nicegui_app.add_static_files("/assets", str(assets_path))
|
|
199
|
+
except Exception:
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
self._register_default_routes()
|
|
203
|
+
|
|
204
|
+
# Merge defaults
|
|
205
|
+
kwargs.setdefault("port", 8080)
|
|
206
|
+
kwargs.setdefault("reload", True)
|
|
207
|
+
kwargs.setdefault("storage_secret", "very-nice-gui-secret-key-123456")
|
|
208
|
+
|
|
209
|
+
ui.run(**kwargs)
|