From: PhiTux Date: Mon, 8 Sep 2025 14:26:03 +0000 (+0200) Subject: added statistics-tab to settings X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=b887b64ef64c0476986bc89a9ec595e17b26edf8;p=DailyTxT.git added statistics-tab to settings --- diff --git a/backend/handlers/additional.go b/backend/handlers/additional.go index 33871c4..cf391b2 100644 --- a/backend/handlers/additional.go +++ b/backend/handlers/additional.go @@ -2625,159 +2625,129 @@ func renderMarkdownToHTML(text string) string { return string(htmlBytes) } -/* -// BACKUP: Old custom markdown renderer - kept for reference -// renderMarkdownToHTML converts simple markdown to HTML -func renderMarkdownToHTML_OLD(text string) string { - lines := strings.Split(text, "\n") - var result strings.Builder - var inCodeBlock bool - var codeBlockContent strings.Builder - var codeLanguage string - - for _, line := range lines { - // Handle code blocks - if strings.HasPrefix(line, "```") { - if !inCodeBlock { - // Starting a code block - inCodeBlock = true - codeLanguage = strings.TrimSpace(line[3:]) - codeBlockContent.Reset() - continue - } else { - // Ending a code block - inCodeBlock = false - codeContent := codeBlockContent.String() - if codeLanguage != "" { - result.WriteString(fmt.Sprintf(`
%s
`, - htmlpkg.EscapeString(codeLanguage), htmlpkg.EscapeString(codeContent))) - } else { - result.WriteString(fmt.Sprintf(`
%s
`, - htmlpkg.EscapeString(codeContent))) - } - codeLanguage = "" - continue - } - } - - // If we're in a code block, accumulate the content - if inCodeBlock { - if codeBlockContent.Len() > 0 { - codeBlockContent.WriteString("\n") - } - codeBlockContent.WriteString(line) - continue - } - - // Normal line processing (when not in code block) - line = strings.TrimSpace(line) - - if line == "" { - result.WriteString("
") - continue - } - - // Handle headings - if strings.HasPrefix(line, "###") { - content := strings.TrimSpace(line[3:]) - result.WriteString(fmt.Sprintf("

%s

", htmlpkg.EscapeString(content))) - continue - } - if strings.HasPrefix(line, "##") { - content := strings.TrimSpace(line[2:]) - result.WriteString(fmt.Sprintf("

%s

", htmlpkg.EscapeString(content))) - continue - } - if strings.HasPrefix(line, "#") { - content := strings.TrimSpace(line[1:]) - result.WriteString(fmt.Sprintf("

%s

", htmlpkg.EscapeString(content))) - continue - } - - // Handle list items - if strings.HasPrefix(line, "- ") || strings.HasPrefix(line, "* ") { - content := strings.TrimSpace(line[2:]) - content = renderInlineMarkdownToHTML(content) - result.WriteString(fmt.Sprintf("", content)) - continue - } - - // Handle blockquotes - if strings.HasPrefix(line, "> ") { - content := strings.TrimSpace(line[2:]) - content = renderInlineMarkdownToHTML(content) - result.WriteString(fmt.Sprintf("
%s
", content)) - continue - } +// Load user statistics: +// - each logged day with amount of words for each day +// - amount of files for each day +// - tags for each day +func GetStatistics(w http.ResponseWriter, r *http.Request) { + // Get user ID and derived key from context + userID, ok := r.Context().Value(utils.UserIDKey).(int) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } - // Regular paragraph with inline formatting - line = renderInlineMarkdownToHTML(line) - result.WriteString(fmt.Sprintf("

%s

", line)) + // Prepare encryption key for decrypting texts and filenames + encKey, err := utils.GetEncryptionKey(userID, derivedKey) + if err != nil { + http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError) + return } - // Handle unclosed code block - if inCodeBlock { - codeContent := codeBlockContent.String() - if codeLanguage != "" { - result.WriteString(fmt.Sprintf(`
%s
`, - htmlpkg.EscapeString(codeLanguage), htmlpkg.EscapeString(codeContent))) - } else { - result.WriteString(fmt.Sprintf(`
%s
`, - htmlpkg.EscapeString(codeContent))) - } + // Define response structure (per day only) + type DayStat struct { + Year int `json:"year"` + Month int `json:"month"` + Day int `json:"day"` + WordCount int `json:"wordCount"` + FileCount int `json:"fileCount"` + Tags []int `json:"tags"` + IsBookmarked bool `json:"isBookmarked"` } - return result.String() -} + dayStats := []DayStat{} -// renderInlineMarkdownToHTML processes inline markdown formatting -func renderInlineMarkdownToHTML_OLD(text string) string { - // Escape HTML first - text = htmlpkg.EscapeString(text) - - // Links with title: [text](url "title") - linkWithTitleRegex := regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\s+"([^"]+)"\)`) - text = linkWithTitleRegex.ReplaceAllStringFunc(text, func(match string) string { - matches := linkWithTitleRegex.FindStringSubmatch(match) - if len(matches) == 4 { - linkText := matches[1] - url := matches[2] - title := matches[3] - return fmt.Sprintf(`%s`, url, title, linkText) - } - return match - }) + // Get all years + years, err := utils.GetYears(userID) + if err != nil { + http.Error(w, fmt.Sprintf("Error retrieving years: %v", err), http.StatusInternalServerError) + return + } - // Links without title: [text](url) - linkRegex := regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`) - text = linkRegex.ReplaceAllStringFunc(text, func(match string) string { - matches := linkRegex.FindStringSubmatch(match) - if len(matches) == 3 { - linkText := matches[1] - url := matches[2] - return fmt.Sprintf(`%s`, url, linkText) + // Iterate years and months + for _, yearStr := range years { + yearInt, _ := strconv.Atoi(yearStr) + months, err := utils.GetMonths(userID, yearStr) + if err != nil { + continue // skip problematic year } - return match - }) + for _, monthStr := range months { + monthInt, _ := strconv.Atoi(monthStr) + content, err := utils.GetMonth(userID, yearInt, monthInt) + if err != nil { + continue + } + daysArr, ok := content["days"].([]any) + if !ok { + continue + } + for _, dayInterface := range daysArr { + dayMap, ok := dayInterface.(map[string]any) + if !ok { + continue + } + dayNumFloat, ok := dayMap["day"].(float64) + if !ok { + continue + } + dayNum := int(dayNumFloat) + + // Word count (decrypt text if present) + wordCount := 0 + if encText, ok := dayMap["text"].(string); ok && encText != "" { + if decrypted, err := utils.DecryptText(encText, encKey); err == nil { + // Count words using Fields (splits on any whitespace) + words := strings.Fields(decrypted) + wordCount = len(words) + } + } - // Bold (**text** or __text__) - boldRegex := regexp.MustCompile(`\*\*(.*?)\*\*`) - text = boldRegex.ReplaceAllString(text, "$1") + // File count (filenames stored decrypted in memory? If encrypted we try decrypt to validate) + fileCount := 0 + if filesAny, ok := dayMap["files"].([]any); ok { + fileCount = len(filesAny) + } - boldRegex2 := regexp.MustCompile(`__(.*?)__`) - text = boldRegex2.ReplaceAllString(text, "$1") + // Tags (IDs are numeric) + var tagIDs []int + if tagsAny, ok := dayMap["tags"].([]any); ok { + for _, t := range tagsAny { + if tf, ok := t.(float64); ok { + tagIDs = append(tagIDs, int(tf)) + } + } + } - // Italic (*text* or _text_) - italicRegex := regexp.MustCompile(`\*(.*?)\*`) - text = italicRegex.ReplaceAllString(text, "$1") + // Bookmark flag + isBookmarked := false + if bmRaw, ok := dayMap["isBookmarked"]; ok { + if b, ok2 := bmRaw.(bool); ok2 { + isBookmarked = b + } else if f, ok2 := bmRaw.(float64); ok2 { // if stored as number + isBookmarked = f != 0 + } + } - italicRegex2 := regexp.MustCompile(`_(.*?)_`) - text = italicRegex2.ReplaceAllString(text, "$1") + dayStats = append(dayStats, DayStat{ + Year: yearInt, + Month: monthInt, + Day: dayNum, + WordCount: wordCount, + FileCount: fileCount, + Tags: tagIDs, + IsBookmarked: isBookmarked, + }) + } + } + } - // Code (`text`) - codeRegex := regexp.MustCompile("`(.*?)`") - text = codeRegex.ReplaceAllString(text, "$1") + // Sort days by date descending (latest first) if desired; currently ascending by traversal. Keep ascending. - return text + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(dayStats) } -*/ diff --git a/backend/main.go b/backend/main.go index e430cde..ea344c5 100644 --- a/backend/main.go +++ b/backend/main.go @@ -65,6 +65,7 @@ func main() { mux.HandleFunc("POST /users/changePassword", middleware.RequireAuth(handlers.ChangePassword)) mux.HandleFunc("POST /users/deleteAccount", middleware.RequireAuth(handlers.DeleteAccount)) mux.HandleFunc("POST /users/createBackupCodes", middleware.RequireAuth(handlers.CreateBackupCodes)) + mux.HandleFunc("GET /users/statistics", middleware.RequireAuth(handlers.GetStatistics)) mux.HandleFunc("POST /logs/saveLog", middleware.RequireAuth(handlers.SaveLog)) mux.HandleFunc("GET /logs/getLog", middleware.RequireAuth(handlers.GetLog)) diff --git a/frontend/src/lib/SettingsNav.svelte b/frontend/src/lib/SettingsNav.svelte new file mode 100644 index 0000000..e44e5cf --- /dev/null +++ b/frontend/src/lib/SettingsNav.svelte @@ -0,0 +1,13 @@ + diff --git a/frontend/src/lib/settings/Admin.svelte b/frontend/src/lib/settings/Admin.svelte new file mode 100644 index 0000000..2576bf7 --- /dev/null +++ b/frontend/src/lib/settings/Admin.svelte @@ -0,0 +1,15 @@ + + +
+

{$t('settings.admin.title')}

+

{$t('settings.admin.placeholder')}

+
+ + diff --git a/frontend/src/lib/settings/Statistics.svelte b/frontend/src/lib/settings/Statistics.svelte new file mode 100644 index 0000000..8a7a8b8 --- /dev/null +++ b/frontend/src/lib/settings/Statistics.svelte @@ -0,0 +1,582 @@ + + +
+

{$t('settings.statistics.title')}

+ + {#if years.length === 0} +
+ Loading... +
+ {:else} +
+ + + +
+ {$t('settings.statistics.legend')} +
+ {#each legendRanges as seg} + + {/each} +
+
+
+ +
+
+ {#each weeks as week, wi} +
+ {#each week as day, di} + {#if day === null || day.empty} + + {:else} +
+ {#if day.isBookmarked} + ★ + {/if} +
+ {/if} + {/each} +
+ {/each} +
+
+ {/if} +
+ + diff --git a/frontend/src/routes/(authed)/+layout.svelte b/frontend/src/routes/(authed)/+layout.svelte index ee35fb5..0fd2b7b 100644 --- a/frontend/src/routes/(authed)/+layout.svelte +++ b/frontend/src/routes/(authed)/+layout.svelte @@ -31,6 +31,8 @@ import axios from 'axios'; import { page } from '$app/state'; import { blur, slide, fade } from 'svelte/transition'; + import Statistics from '$lib/settings/Statistics.svelte'; + import Admin from '$lib/settings/Admin.svelte'; import { T, getTranslate, getTolgee } from '@tolgee/svelte'; const { t } = getTranslate(); @@ -40,6 +42,9 @@ let inDuration = 150; let outDuration = 150; + // Active sub-view of settings modal: 'settings' | 'stats' | 'admin' + let activeSettingsView = $state('settings'); + $effect(() => { if ($readingMode === true && page.url.pathname !== '/read') { goto('/read'); @@ -170,6 +175,7 @@ } function openSettingsModal() { + activeSettingsView = 'settings'; $tempSettings = JSON.parse(JSON.stringify($settings)); aLookBackYears = $settings.aLookBackYears.toString(); @@ -887,490 +893,559 @@ >