1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
"""
PodcastItLater Shared UI Components.
Common UI components and utilities shared across web pages.
"""
# : out podcastitlater-ui
# : dep ludic
import ludic.html as html
def format_duration(seconds: int | None) -> str:
"""Format duration from seconds to human-readable format.
Examples:
300 -> "5m"
3840 -> "1h 4m"
11520 -> "3h 12m"
"""
if seconds is None or seconds <= 0:
return "Unknown"
# Constants for time conversion
seconds_per_minute = 60
minutes_per_hour = 60
seconds_per_hour = 3600
# Round up to nearest minute
minutes = (seconds + seconds_per_minute - 1) // seconds_per_minute
# Show as minutes only if under 60 minutes (exclusive)
# 3599 seconds rounds up to 60 minutes, which we keep as "60m"
if minutes <= minutes_per_hour:
# If exactly 3600 seconds (already 60 full minutes without rounding)
if seconds >= seconds_per_hour:
return "1h"
return f"{minutes}m"
hours = minutes // minutes_per_hour
remaining_minutes = minutes % minutes_per_hour
if remaining_minutes == 0:
return f"{hours}h"
return f"{hours}h {remaining_minutes}m"
def create_bootstrap_styles() -> html.style:
"""Load Bootstrap CSS and icons."""
return html.style(
"@import url('https://cdn.jsdelivr.net/npm/bootstrap@5.3.2"
"/dist/css/bootstrap.min.css');"
"@import url('https://cdn.jsdelivr.net/npm/bootstrap-icons"
"@1.11.3/font/bootstrap-icons.min.css');",
)
def create_auto_dark_mode_style() -> html.style:
"""Create CSS for automatic dark mode based on prefers-color-scheme."""
return html.style(
"""
/* Auto dark mode - applies Bootstrap dark theme via media query */
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-bg: #343a40;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-bg: #2b3035;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
}
/* Navbar dark mode */
.navbar.bg-body-tertiary {
background-color: #2b3035 !important;
}
.navbar .navbar-text {
color: #dee2e6 !important;
}
}
""",
)
def create_htmx_script() -> html.script:
"""Load HTMX library."""
return html.script(
src="https://unpkg.com/htmx.org@1.9.10",
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC",
crossorigin="anonymous",
)
def create_bootstrap_js() -> html.script:
"""Load Bootstrap JavaScript bundle."""
return html.script(
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js",
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL",
crossorigin="anonymous",
)
|