Repository navigation
Expand file tree
/
Copy pathutils.py
More file actions
187 lines (151 loc) · 6.15 KB
/
Copy pathutils.py
File metadata and controls
187 lines (151 loc) · 6.15 KB
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
"""
DevTime v7 — утилиты и сервис таймера.
"""
import time
import logging
from datetime import datetime
from typing import Optional
log = logging.getLogger(__name__)
# ── Форматирование ────────────────────────────────────────────────────────────
def fmt_h(seconds: int) -> str:
"""1h 23m или 45m или 12s"""
s = int(seconds)
if s < 60: return f"{s}с"
if s < 3600: return f"{s // 60}м"
h = s // 3600
m = (s % 3600) // 60
return f"{h}ч {m}м" if m else f"{h}ч"
def fmt_h_dec(seconds: int) -> str:
"""12.3ч — для больших чисел"""
return f"{seconds / 3600:.1f}ч"
def fmt_hms(seconds: int) -> str:
"""00:32:15"""
s = int(seconds)
return f"{s // 3600:02d}:{(s % 3600) // 60:02d}:{s % 60:02d}"
def fmt_ts() -> str:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def ts_from_time(t: float) -> str:
return datetime.fromtimestamp(t).strftime("%Y-%m-%d %H:%M:%S")
# ── Цвета проектов ────────────────────────────────────────────────────────────
PROJECT_COLORS = [
"#4fc3f7", # голубой
"#7c4dff", # фиолетовый
"#00e676", # зелёный
"#ffab40", # оранжевый
"#f06292", # розовый
"#4db6ac", # бирюзовый
"#ff5252", # красный
"#aed581", # лайм
"#ba68c8", # сиреневый
"#4fc3f7", # снова голубой
]
def color_for_name(name: str) -> str:
"""Детерминированный цвет по имени проекта."""
idx = sum(ord(c) for c in name) % len(PROJECT_COLORS)
return PROJECT_COLORS[idx]
def hex_to_rgb(hex_color: str) -> tuple:
h = hex_color.lstrip("#")
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
def lighten(hex_color: str, factor: float = 0.3) -> str:
r, g, b = hex_to_rgb(hex_color)
r = int(r + (255 - r) * factor)
g = int(g + (255 - g) * factor)
b = int(b + (255 - b) * factor)
return f"#{r:02x}{g:02x}{b:02x}"
def darken(hex_color: str, factor: float = 0.4) -> str:
r, g, b = hex_to_rgb(hex_color)
r = int(r * (1 - factor))
g = int(g * (1 - factor))
b = int(b * (1 - factor))
return f"#{r:02x}{g:02x}{b:02x}"
def blend(fg: str, bg: str, alpha: float) -> str:
"""Альфа-композитинг: fg поверх bg с прозрачностью alpha (0..1) → 6-значный hex.
Tkinter Canvas НЕ понимает 8-значный hex с альфой (#RRGGBBAA),
поэтому полупрозрачность считаем сами и отдаём непрозрачный цвет.
"""
a = max(0.0, min(1.0, alpha))
fr, fg_, fb = hex_to_rgb(fg)
br, bg_, bb = hex_to_rgb(bg)
r = int(fr * a + br * (1 - a))
g = int(fg_ * a + bg_ * (1 - a))
b = int(fb * a + bb * (1 - a))
return f"#{r:02x}{g:02x}{b:02x}"
# ── Сервис таймера ────────────────────────────────────────────────────────────
class _Session:
"""Одна активная сессия. Хранит реальный started_at через все паузы."""
__slots__ = ("real_started_ts", "segment_start_ts", "accumulated_s", "paused")
def __init__(self):
now = time.time()
self.real_started_ts: float = now
self.segment_start_ts: float = now
self.accumulated_s: int = 0
self.paused: bool = False
def elapsed(self) -> int:
if self.paused:
return self.accumulated_s
return self.accumulated_s + int(time.time() - self.segment_start_ts)
def do_pause(self):
self.accumulated_s += int(time.time() - self.segment_start_ts)
self.paused = True
def do_resume(self):
self.segment_start_ts = time.time()
self.paused = False
class TimerService:
def __init__(self):
self._sessions: dict[int, _Session] = {}
def start(self, aid: int) -> bool:
if aid in self._sessions:
return False
self._sessions[aid] = _Session()
return True
def pause(self, aid: int) -> bool:
s = self._sessions.get(aid)
if s is None or s.paused:
return False
s.do_pause()
return True
def resume(self, aid: int) -> bool:
s = self._sessions.get(aid)
if s is None or not s.paused:
return False
s.do_resume()
return True
def stop(self, aid: int) -> Optional[dict]:
s = self._sessions.pop(aid, None)
if s is None:
return None
if not s.paused:
s.accumulated_s += int(time.time() - s.segment_start_ts)
return {
"started_at": ts_from_time(s.real_started_ts),
"ended_at": fmt_ts(),
"duration_s": max(s.accumulated_s, 1),
}
def elapsed(self, aid: int) -> int:
s = self._sessions.get(aid)
return s.elapsed() if s else 0
def is_running(self, aid: int) -> bool:
s = self._sessions.get(aid)
return s is not None and not s.paused
def is_paused(self, aid: int) -> bool:
s = self._sessions.get(aid)
return s is not None and s.paused
def is_active(self, aid: int) -> bool:
return aid in self._sessions
def stop_all(self) -> dict[int, dict]:
result = {}
for aid in list(self._sessions.keys()):
data = self.stop(aid)
if data:
result[aid] = data
return result
def snapshot(self) -> dict[int, dict]:
"""Слепок активных сессий для crash-safe автосейва в БД."""
out = {}
for aid, s in self._sessions.items():
out[aid] = {
"started_at": ts_from_time(s.real_started_ts),
"elapsed_s": s.elapsed(),
"paused": s.paused,
}
return out