bouquin 0.7.2__tar.gz → 0.7.3__tar.gz

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.
Files changed (45) hide show
  1. {bouquin-0.7.2 → bouquin-0.7.3}/PKG-INFO +1 -1
  2. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/db.py +57 -4
  3. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/locales/en.json +2 -0
  4. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/main_window.py +11 -2
  5. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/settings.py +5 -0
  6. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/settings_dialog.py +20 -0
  7. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/time_log.py +102 -47
  8. {bouquin-0.7.2 → bouquin-0.7.3}/pyproject.toml +1 -1
  9. {bouquin-0.7.2 → bouquin-0.7.3}/LICENSE +0 -0
  10. {bouquin-0.7.2 → bouquin-0.7.3}/README.md +0 -0
  11. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/__init__.py +0 -0
  12. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/__main__.py +0 -0
  13. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/bug_report_dialog.py +0 -0
  14. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/code_block_editor_dialog.py +0 -0
  15. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/code_highlighter.py +0 -0
  16. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/document_utils.py +0 -0
  17. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/documents.py +0 -0
  18. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/find_bar.py +0 -0
  19. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/flow_layout.py +0 -0
  20. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/fonts/DejaVu.license +0 -0
  21. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/fonts/DejaVuSans.ttf +0 -0
  22. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/fonts/Noto.license +0 -0
  23. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/fonts/NotoSansSymbols2-Regular.ttf +0 -0
  24. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/history_dialog.py +0 -0
  25. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/icons/bouquin.svg +0 -0
  26. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/invoices.py +0 -0
  27. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/key_prompt.py +0 -0
  28. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/keys/mig5.asc +0 -0
  29. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/locales/fr.json +0 -0
  30. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/locales/it.json +0 -0
  31. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/lock_overlay.py +0 -0
  32. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/main.py +0 -0
  33. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/markdown_editor.py +0 -0
  34. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/markdown_highlighter.py +0 -0
  35. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/pomodoro_timer.py +0 -0
  36. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/reminders.py +0 -0
  37. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/save_dialog.py +0 -0
  38. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/search.py +0 -0
  39. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/statistics_dialog.py +0 -0
  40. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/strings.py +0 -0
  41. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/tag_browser.py +0 -0
  42. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/tags_widget.py +0 -0
  43. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/theme.py +0 -0
  44. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/toolbar.py +0 -0
  45. {bouquin-0.7.2 → bouquin-0.7.3}/bouquin/version_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bouquin
3
- Version: 0.7.2
3
+ Version: 0.7.3
4
4
  Summary: Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher.
5
5
  Home-page: https://git.mig5.net/mig5/bouquin
6
6
  License: GPL-3.0-or-later
@@ -92,6 +92,7 @@ class DBConfig:
92
92
  idle_minutes: int = 15 # 0 = never lock
93
93
  theme: str = "system"
94
94
  move_todos: bool = False
95
+ move_todos_include_weekends: bool = False
95
96
  tags: bool = True
96
97
  time_log: bool = True
97
98
  reminders: bool = True
@@ -1351,7 +1352,7 @@ class DBManager:
1351
1352
  project_id: int,
1352
1353
  start_date_iso: str,
1353
1354
  end_date_iso: str,
1354
- granularity: str = "day", # 'day' | 'week' | 'month' | 'none'
1355
+ granularity: str = "day", # 'day' | 'week' | 'month' | 'activity' | 'none'
1355
1356
  ) -> list[tuple[str, str, str, int]]:
1356
1357
  """
1357
1358
  Return (time_period, activity_name, total_minutes) tuples between start and end
@@ -1360,7 +1361,8 @@ class DBManager:
1360
1361
  - 'YYYY-MM-DD' for day
1361
1362
  - 'YYYY-WW' for week
1362
1363
  - 'YYYY-MM' for month
1363
- For 'none' granularity, each individual time log entry becomes a row.
1364
+ For 'activity' granularity, results are grouped by activity only (no time bucket).
1365
+ For 'none' granularity, each individual time log entry becomes a row.
1364
1366
  """
1365
1367
  cur = self.conn.cursor()
1366
1368
 
@@ -1387,6 +1389,26 @@ class DBManager:
1387
1389
  for r in rows
1388
1390
  ]
1389
1391
 
1392
+ if granularity == "activity":
1393
+ rows = cur.execute(
1394
+ """
1395
+ SELECT
1396
+ a.name AS activity_name,
1397
+ SUM(t.minutes) AS total_minutes
1398
+ FROM time_log t
1399
+ JOIN activities a ON a.id = t.activity_id
1400
+ WHERE t.project_id = ?
1401
+ AND t.page_date BETWEEN ? AND ?
1402
+ GROUP BY activity_name
1403
+ ORDER BY LOWER(activity_name);
1404
+ """,
1405
+ (project_id, start_date_iso, end_date_iso),
1406
+ ).fetchall()
1407
+
1408
+ # period column is unused for activity grouping in the UI, but we keep
1409
+ # the tuple shape consistent.
1410
+ return [("", r["activity_name"], "", r["total_minutes"]) for r in rows]
1411
+
1390
1412
  if granularity == "day":
1391
1413
  bucket_expr = "page_date"
1392
1414
  elif granularity == "week":
@@ -1417,11 +1439,14 @@ class DBManager:
1417
1439
  self,
1418
1440
  start_date_iso: str,
1419
1441
  end_date_iso: str,
1420
- granularity: str = "day", # 'day' | 'week' | 'month' | 'none'
1442
+ granularity: str = "day", # 'day' | 'week' | 'month' | 'activity' | 'none'
1421
1443
  ) -> list[tuple[str, str, str, str, int]]:
1422
1444
  """
1423
1445
  Return (project_name, time_period, activity_name, note, total_minutes)
1424
- across *all* projects between start and end, grouped by project + period + activity.
1446
+ across *all* projects between start and end.
1447
+ - For 'day'/'week'/'month', grouped by project + period + activity.
1448
+ - For 'activity', grouped by project + activity.
1449
+ - For 'none', one row per time_log entry.
1425
1450
  """
1426
1451
  cur = self.conn.cursor()
1427
1452
 
@@ -1455,6 +1480,34 @@ class DBManager:
1455
1480
  for r in rows
1456
1481
  ]
1457
1482
 
1483
+ if granularity == "activity":
1484
+ rows = cur.execute(
1485
+ """
1486
+ SELECT
1487
+ p.name AS project_name,
1488
+ a.name AS activity_name,
1489
+ SUM(t.minutes) AS total_minutes
1490
+ FROM time_log t
1491
+ JOIN projects p ON p.id = t.project_id
1492
+ JOIN activities a ON a.id = t.activity_id
1493
+ WHERE t.page_date BETWEEN ? AND ?
1494
+ GROUP BY p.id, activity_name
1495
+ ORDER BY LOWER(p.name), LOWER(activity_name);
1496
+ """,
1497
+ (start_date_iso, end_date_iso),
1498
+ ).fetchall()
1499
+
1500
+ return [
1501
+ (
1502
+ r["project_name"],
1503
+ "",
1504
+ r["activity_name"],
1505
+ "",
1506
+ r["total_minutes"],
1507
+ )
1508
+ for r in rows
1509
+ ]
1510
+
1458
1511
  if granularity == "day":
1459
1512
  bucket_expr = "page_date"
1460
1513
  elif granularity == "week":
@@ -103,6 +103,7 @@
103
103
  "autosave": "autosave",
104
104
  "unchecked_checkbox_items_moved_to_next_day": "Unchecked checkbox items moved to next day",
105
105
  "move_unchecked_todos_to_today_on_startup": "Automatically move unchecked TODOs\nfrom the last 7 days to next weekday",
106
+ "move_todos_include_weekends": "Allow moving unchecked TODOs to a weekend\nrather than next weekday",
106
107
  "insert_images": "Insert images",
107
108
  "images": "Images",
108
109
  "reopen_failed": "Re-open failed",
@@ -209,6 +210,7 @@
209
210
  "add_time_entry": "Add time entry",
210
211
  "time_period": "Time period",
211
212
  "dont_group": "Don't group",
213
+ "by_activity": "by activity",
212
214
  "by_day": "by day",
213
215
  "by_month": "by month",
214
216
  "by_week": "by week",
@@ -822,9 +822,13 @@ class MainWindow(QMainWindow):
822
822
  Given a 'new day' (system date), return the date we should move
823
823
  unfinished todos *to*.
824
824
 
825
- If the new day is Saturday or Sunday, we skip ahead to the next Monday.
826
- Otherwise we just return the same day.
825
+ By default, if the new day is Saturday or Sunday we skip ahead to the
826
+ next Monday (i.e., "next available weekday"). If the optional setting
827
+ `move_todos_include_weekends` is enabled, we move to the very next day
828
+ even if it's a weekend.
827
829
  """
830
+ if getattr(self.cfg, "move_todos_include_weekends", False):
831
+ return day
828
832
  # Qt: Monday=1 ... Sunday=7
829
833
  dow = day.dayOfWeek()
830
834
  if dow >= 6: # Saturday (6) or Sunday (7)
@@ -1566,6 +1570,11 @@ class MainWindow(QMainWindow):
1566
1570
  self.cfg.idle_minutes = getattr(new_cfg, "idle_minutes", self.cfg.idle_minutes)
1567
1571
  self.cfg.theme = getattr(new_cfg, "theme", self.cfg.theme)
1568
1572
  self.cfg.move_todos = getattr(new_cfg, "move_todos", self.cfg.move_todos)
1573
+ self.cfg.move_todos_include_weekends = getattr(
1574
+ new_cfg,
1575
+ "move_todos_include_weekends",
1576
+ getattr(self.cfg, "move_todos_include_weekends", False),
1577
+ )
1569
1578
  self.cfg.tags = getattr(new_cfg, "tags", self.cfg.tags)
1570
1579
  self.cfg.time_log = getattr(new_cfg, "time_log", self.cfg.time_log)
1571
1580
  self.cfg.reminders = getattr(new_cfg, "reminders", self.cfg.reminders)
@@ -42,6 +42,9 @@ def load_db_config() -> DBConfig:
42
42
  idle = s.value("ui/idle_minutes", 15, type=int)
43
43
  theme = s.value("ui/theme", "system", type=str)
44
44
  move_todos = s.value("ui/move_todos", False, type=bool)
45
+ move_todos_include_weekends = s.value(
46
+ "ui/move_todos_include_weekends", False, type=bool
47
+ )
45
48
  tags = s.value("ui/tags", True, type=bool)
46
49
  time_log = s.value("ui/time_log", True, type=bool)
47
50
  reminders = s.value("ui/reminders", True, type=bool)
@@ -57,6 +60,7 @@ def load_db_config() -> DBConfig:
57
60
  idle_minutes=idle,
58
61
  theme=theme,
59
62
  move_todos=move_todos,
63
+ move_todos_include_weekends=move_todos_include_weekends,
60
64
  tags=tags,
61
65
  time_log=time_log,
62
66
  reminders=reminders,
@@ -76,6 +80,7 @@ def save_db_config(cfg: DBConfig) -> None:
76
80
  s.setValue("ui/idle_minutes", str(cfg.idle_minutes))
77
81
  s.setValue("ui/theme", str(cfg.theme))
78
82
  s.setValue("ui/move_todos", str(cfg.move_todos))
83
+ s.setValue("ui/move_todos_include_weekends", str(cfg.move_todos_include_weekends))
79
84
  s.setValue("ui/tags", str(cfg.tags))
80
85
  s.setValue("ui/time_log", str(cfg.time_log))
81
86
  s.setValue("ui/reminders", str(cfg.reminders))
@@ -169,6 +169,25 @@ class SettingsDialog(QDialog):
169
169
  self.move_todos.setCursor(Qt.PointingHandCursor)
170
170
  features_layout.addWidget(self.move_todos)
171
171
 
172
+ # Optional: allow moving to the very next day even if it is a weekend.
173
+ self.move_todos_include_weekends = QCheckBox(
174
+ strings._("move_todos_include_weekends")
175
+ )
176
+ self.move_todos_include_weekends.setChecked(
177
+ getattr(self.current_settings, "move_todos_include_weekends", False)
178
+ )
179
+ self.move_todos_include_weekends.setCursor(Qt.PointingHandCursor)
180
+ self.move_todos_include_weekends.setEnabled(self.move_todos.isChecked())
181
+
182
+ move_todos_opts = QWidget()
183
+ move_todos_opts_layout = QVBoxLayout(move_todos_opts)
184
+ move_todos_opts_layout.setContentsMargins(24, 0, 0, 0)
185
+ move_todos_opts_layout.setSpacing(4)
186
+ move_todos_opts_layout.addWidget(self.move_todos_include_weekends)
187
+ features_layout.addWidget(move_todos_opts)
188
+
189
+ self.move_todos.toggled.connect(self.move_todos_include_weekends.setEnabled)
190
+
172
191
  self.tags = QCheckBox(strings._("enable_tags_feature"))
173
192
  self.tags.setChecked(self.current_settings.tags)
174
193
  self.tags.setCursor(Qt.PointingHandCursor)
@@ -441,6 +460,7 @@ class SettingsDialog(QDialog):
441
460
  idle_minutes=self.idle_spin.value(),
442
461
  theme=selected_theme.value,
443
462
  move_todos=self.move_todos.isChecked(),
463
+ move_todos_include_weekends=self.move_todos_include_weekends.isChecked(),
444
464
  tags=self.tags.isChecked(),
445
465
  time_log=self.time_log.isChecked(),
446
466
  reminders=self.reminders.isChecked(),
@@ -1083,6 +1083,7 @@ class TimeReportDialog(QDialog):
1083
1083
  self.granularity.addItem(strings._("by_day"), "day")
1084
1084
  self.granularity.addItem(strings._("by_week"), "week")
1085
1085
  self.granularity.addItem(strings._("by_month"), "month")
1086
+ self.granularity.addItem(strings._("by_activity"), "activity")
1086
1087
  form.addRow(strings._("group_by"), self.granularity)
1087
1088
 
1088
1089
  root.addLayout(form)
@@ -1161,6 +1162,20 @@ class TimeReportDialog(QDialog):
1161
1162
  header.setSectionResizeMode(2, QHeaderView.Stretch)
1162
1163
  header.setSectionResizeMode(3, QHeaderView.Stretch)
1163
1164
  header.setSectionResizeMode(4, QHeaderView.ResizeToContents)
1165
+ elif granularity == "activity":
1166
+ # Grouped by activity only: no time period, no note column
1167
+ self.table.setColumnCount(3)
1168
+ self.table.setHorizontalHeaderLabels(
1169
+ [
1170
+ strings._("project"),
1171
+ strings._("activity"),
1172
+ strings._("hours"),
1173
+ ]
1174
+ )
1175
+ header = self.table.horizontalHeader()
1176
+ header.setSectionResizeMode(0, QHeaderView.Stretch)
1177
+ header.setSectionResizeMode(1, QHeaderView.Stretch)
1178
+ header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
1164
1179
  else:
1165
1180
  # Grouped: no note column
1166
1181
  self.table.setColumnCount(4)
@@ -1272,16 +1287,21 @@ class TimeReportDialog(QDialog):
1272
1287
  rows_for_table
1273
1288
  ):
1274
1289
  hrs = minutes / 60.0
1275
- self.table.setItem(i, 0, QTableWidgetItem(project))
1276
- self.table.setItem(i, 1, QTableWidgetItem(time_period))
1277
- self.table.setItem(i, 2, QTableWidgetItem(activity_name))
1278
-
1279
- if self._last_gran == "none":
1280
- self.table.setItem(i, 3, QTableWidgetItem(note or ""))
1281
- self.table.setItem(i, 4, QTableWidgetItem(f"{hrs:.2f}"))
1290
+ if self._last_gran == "activity":
1291
+ self.table.setItem(i, 0, QTableWidgetItem(project))
1292
+ self.table.setItem(i, 1, QTableWidgetItem(activity_name))
1293
+ self.table.setItem(i, 2, QTableWidgetItem(f"{hrs:.2f}"))
1282
1294
  else:
1283
- # no note column
1284
- self.table.setItem(i, 3, QTableWidgetItem(f"{hrs:.2f}"))
1295
+ self.table.setItem(i, 0, QTableWidgetItem(project))
1296
+ self.table.setItem(i, 1, QTableWidgetItem(time_period))
1297
+ self.table.setItem(i, 2, QTableWidgetItem(activity_name))
1298
+
1299
+ if self._last_gran == "none":
1300
+ self.table.setItem(i, 3, QTableWidgetItem(note or ""))
1301
+ self.table.setItem(i, 4, QTableWidgetItem(f"{hrs:.2f}"))
1302
+ else:
1303
+ # no note column
1304
+ self.table.setItem(i, 3, QTableWidgetItem(f"{hrs:.2f}"))
1285
1305
 
1286
1306
  # Summary label - include per-project totals when in "all projects" mode
1287
1307
  total_hours = self._last_total_minutes / 60.0
@@ -1325,14 +1345,15 @@ class TimeReportDialog(QDialog):
1325
1345
  with open(filename, "w", newline="", encoding="utf-8") as f:
1326
1346
  writer = csv.writer(f)
1327
1347
 
1328
- show_note = getattr(self, "_last_gran", "day") == "none"
1348
+ gran = getattr(self, "_last_gran", "day")
1349
+ show_note = gran == "none"
1350
+ show_period = gran != "activity"
1329
1351
 
1330
1352
  # Header
1331
- header = [
1332
- strings._("project"),
1333
- strings._("time_period"),
1334
- strings._("activity"),
1335
- ]
1353
+ header: list[str] = [strings._("project")]
1354
+ if show_period:
1355
+ header.append(strings._("time_period"))
1356
+ header.append(strings._("activity"))
1336
1357
  if show_note:
1337
1358
  header.append(strings._("note"))
1338
1359
  header.append(strings._("hours"))
@@ -1347,16 +1368,22 @@ class TimeReportDialog(QDialog):
1347
1368
  minutes,
1348
1369
  ) in self._last_rows:
1349
1370
  hours = minutes / 60.0
1350
- row = [project, time_period, activity_name]
1371
+ row: list[str] = [project]
1372
+ if show_period:
1373
+ row.append(time_period)
1374
+ row.append(activity_name)
1351
1375
  if show_note:
1352
- row.append(note)
1376
+ row.append(note or "")
1353
1377
  row.append(f"{hours:.2f}")
1354
1378
  writer.writerow(row)
1355
1379
 
1356
1380
  # Blank line + total
1357
1381
  total_hours = self._last_total_minutes / 60.0
1358
1382
  writer.writerow([])
1359
- writer.writerow([strings._("total"), "", f"{total_hours:.2f}"])
1383
+ total_row = [""] * len(header)
1384
+ total_row[0] = strings._("total")
1385
+ total_row[-1] = f"{total_hours:.2f}"
1386
+ writer.writerow(total_row)
1360
1387
  except OSError as exc:
1361
1388
  QMessageBox.warning(
1362
1389
  self,
@@ -1384,17 +1411,20 @@ class TimeReportDialog(QDialog):
1384
1411
  if not filename.endswith(".pdf"):
1385
1412
  filename = f"{filename}.pdf"
1386
1413
 
1387
- # ---------- Build chart image (hours per period) ----------
1388
- per_period_minutes: dict[str, int] = defaultdict(int)
1389
- for _project, period, _activity, note, minutes in self._last_rows:
1390
- per_period_minutes[period] += minutes
1414
+ # ---------- Build chart image ----------
1415
+ # Default: hours per time period. If grouped by activity: hours per activity.
1416
+ gran = getattr(self, "_last_gran", "day")
1417
+ per_bucket_minutes: dict[str, int] = defaultdict(int)
1418
+ for _project, period, activity, _note, minutes in self._last_rows:
1419
+ bucket = activity if gran == "activity" else period
1420
+ per_bucket_minutes[bucket] += minutes
1391
1421
 
1392
- periods = sorted(per_period_minutes.keys())
1422
+ buckets = sorted(per_bucket_minutes.keys())
1393
1423
  chart_w, chart_h = 800, 220
1394
1424
  chart = QImage(chart_w, chart_h, QImage.Format_ARGB32)
1395
1425
  chart.fill(Qt.white)
1396
1426
 
1397
- if periods:
1427
+ if buckets:
1398
1428
  painter = QPainter(chart)
1399
1429
  try:
1400
1430
  painter.setRenderHint(QPainter.Antialiasing, True)
@@ -1422,9 +1452,9 @@ class TimeReportDialog(QDialog):
1422
1452
  # Border
1423
1453
  painter.drawRect(left, top, width, height)
1424
1454
 
1425
- max_hours = max(per_period_minutes[p] for p in periods) / 60.0
1455
+ max_hours = max(per_bucket_minutes[p] for p in buckets) / 60.0
1426
1456
  if max_hours > 0:
1427
- n = len(periods)
1457
+ n = len(buckets)
1428
1458
  bar_spacing = width / max(1, n)
1429
1459
  bar_width = bar_spacing * 0.6
1430
1460
 
@@ -1449,8 +1479,8 @@ class TimeReportDialog(QDialog):
1449
1479
  painter.setBrush(QColor(80, 140, 200))
1450
1480
  painter.setPen(Qt.NoPen)
1451
1481
 
1452
- for i, period in enumerate(periods):
1453
- hours = per_period_minutes[period] / 60.0
1482
+ for i, label in enumerate(buckets):
1483
+ hours = per_bucket_minutes[label] / 60.0
1454
1484
  bar_h = int((hours / max_hours) * (height - 10))
1455
1485
  if bar_h <= 0:
1456
1486
  continue # pragma: no cover
@@ -1463,7 +1493,7 @@ class TimeReportDialog(QDialog):
1463
1493
 
1464
1494
  # X labels after bars, in black
1465
1495
  painter.setPen(Qt.black)
1466
- for i, period in enumerate(periods):
1496
+ for i, label in enumerate(buckets):
1467
1497
  x_center = left + bar_spacing * (i + 0.5)
1468
1498
  x = int(x_center - bar_width / 2)
1469
1499
  painter.drawText(
@@ -1472,7 +1502,7 @@ class TimeReportDialog(QDialog):
1472
1502
  int(bar_width),
1473
1503
  20,
1474
1504
  Qt.AlignHCenter | Qt.AlignTop,
1475
- period,
1505
+ label,
1476
1506
  )
1477
1507
  finally:
1478
1508
  painter.end()
@@ -1481,23 +1511,53 @@ class TimeReportDialog(QDialog):
1481
1511
  project = html.escape(self._last_project_name or "")
1482
1512
  start = html.escape(self._last_start or "")
1483
1513
  end = html.escape(self._last_end or "")
1484
- gran = html.escape(self._last_gran_label or "")
1514
+ gran_key = getattr(self, "_last_gran", "day")
1515
+ gran_label = html.escape(self._last_gran_label or "")
1485
1516
 
1486
1517
  total_hours = self._last_total_minutes / 60.0
1487
1518
 
1488
- # Table rows (period, activity, hours)
1519
+ # Table rows
1489
1520
  row_html_parts: list[str] = []
1490
- for project, period, activity, note, minutes in self._last_rows:
1491
- hours = minutes / 60.0
1492
- row_html_parts.append(
1521
+ if gran_key == "activity":
1522
+ for project, _period, activity, _note, minutes in self._last_rows:
1523
+ hours = minutes / 60.0
1524
+ row_html_parts.append(
1525
+ "<tr>"
1526
+ f"<td>{html.escape(project)}</td>"
1527
+ f"<td>{html.escape(activity)}</td>"
1528
+ f"<td style='text-align:right'>{hours:.2f}</td>"
1529
+ "</tr>"
1530
+ )
1531
+ else:
1532
+ for project, period, activity, _note, minutes in self._last_rows:
1533
+ hours = minutes / 60.0
1534
+ row_html_parts.append(
1535
+ "<tr>"
1536
+ f"<td>{html.escape(project)}</td>"
1537
+ f"<td>{html.escape(period)}</td>"
1538
+ f"<td>{html.escape(activity)}</td>"
1539
+ f"<td style='text-align:right'>{hours:.2f}</td>"
1540
+ "</tr>"
1541
+ )
1542
+ rows_html = "\n".join(row_html_parts)
1543
+
1544
+ if gran_key == "activity":
1545
+ table_header_html = (
1493
1546
  "<tr>"
1494
- f"<td>{html.escape(project)}</td>"
1495
- f"<td>{html.escape(period)}</td>"
1496
- f"<td>{html.escape(activity)}</td>"
1497
- f"<td style='text-align:right'>{hours:.2f}</td>"
1547
+ f"<th>{html.escape(strings._('project'))}</th>"
1548
+ f"<th>{html.escape(strings._('activity'))}</th>"
1549
+ f"<th>{html.escape(strings._('hours'))}</th>"
1550
+ "</tr>"
1551
+ )
1552
+ else:
1553
+ table_header_html = (
1554
+ "<tr>"
1555
+ f"<th>{html.escape(strings._('project'))}</th>"
1556
+ f"<th>{html.escape(strings._('time_period'))}</th>"
1557
+ f"<th>{html.escape(strings._('activity'))}</th>"
1558
+ f"<th>{html.escape(strings._('hours'))}</th>"
1498
1559
  "</tr>"
1499
1560
  )
1500
- rows_html = "\n".join(row_html_parts)
1501
1561
 
1502
1562
  html_doc = f"""
1503
1563
  <!DOCTYPE html>
@@ -1544,16 +1604,11 @@ class TimeReportDialog(QDialog):
1544
1604
  <h1>{html.escape(strings._("time_log_report_title").format(project=project))}</h1>
1545
1605
  <p class="meta">
1546
1606
  {html.escape(strings._("time_log_report_meta").format(
1547
- start=start, end=end, granularity=gran))}
1607
+ start=start, end=end, granularity=gran_label))}
1548
1608
  </p>
1549
1609
  <p><img src="chart" class="chart" /></p>
1550
1610
  <table>
1551
- <tr>
1552
- <th>{html.escape(strings._("project"))}</th>
1553
- <th>{html.escape(strings._("time_period"))}</th>
1554
- <th>{html.escape(strings._("activity"))}</th>
1555
- <th>{html.escape(strings._("hours"))}</th>
1556
- </tr>
1611
+ {table_header_html}
1557
1612
  {rows_html}
1558
1613
  </table>
1559
1614
  <p><b>{html.escape(strings._("time_report_total").format(hours=total_hours))}</b></p>
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "bouquin"
3
- version = "0.7.2"
3
+ version = "0.7.3"
4
4
  description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
5
5
  authors = ["Miguel Jacq <mig@mig5.net>"]
6
6
  readme = "README.md"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes