From: PhiTux Date: Wed, 10 Sep 2025 23:02:47 +0000 (+0200) Subject: added about-section and update-notification X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=b359b1fe039e11a9b52d2257fd64d51afcbdecef;p=DailyTxT.git added about-section and update-notification --- diff --git a/backend/handlers/additional.go b/backend/handlers/additional.go index 9d50c12..3ff9e5a 100644 --- a/backend/handlers/additional.go +++ b/backend/handlers/additional.go @@ -2770,3 +2770,14 @@ func GetStatistics(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(dayStats) } + +// GetVersionInfo returns the current application version (public endpoint, no auth required) +func GetVersionInfo(w http.ResponseWriter, r *http.Request) { + latest_stable, latest_overall := utils.GetLatestVersion() + + utils.JSONResponse(w, http.StatusOK, map[string]string{ + "current_version": utils.GetVersion(), + "latest_stable_version": latest_stable, + "latest_overall_version": latest_overall, + }) +} diff --git a/backend/handlers/users.go b/backend/handlers/users.go index c35bbbe..3b98b20 100644 --- a/backend/handlers/users.go +++ b/backend/handlers/users.go @@ -437,6 +437,8 @@ func GetDefaultSettings() map[string]any { "useDarkMode": false, "background": "gradient", "monochromeBackgroundColor": "#ececec", + "checkForUpdates": true, + "includeTestVersions": false, } } diff --git a/backend/main.go b/backend/main.go index 5ffeaba..c7aae83 100644 --- a/backend/main.go +++ b/backend/main.go @@ -14,6 +14,9 @@ import ( "github.com/phitux/dailytxt/backend/utils" ) +// Application version - UPDATE THIS FOR NEW RELEASES +const AppVersion = "2.0.0-testing.1" + // longTimeoutEndpoints defines endpoints that need extended timeouts var longTimeoutEndpoints = map[string]bool{ "/logs/uploadFile": true, @@ -42,6 +45,10 @@ func main() { logger := log.New(os.Stdout, "dailytxt: ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile) logger.Println("Server starting...") + // Set application version + utils.SetVersion(AppVersion) + logger.Printf("DailyTxT version: %s", AppVersion) + // Load settings if err := utils.InitSettings(); err != nil { logger.Fatalf("Failed to initialize settings: %v", err) @@ -53,6 +60,9 @@ func main() { // Create a new router mux := http.NewServeMux() + // Public routes (no authentication required) + mux.HandleFunc("GET /version", handlers.GetVersionInfo) + // Register routes mux.HandleFunc("POST /users/login", handlers.Login) mux.HandleFunc("GET /users/migrationProgress", handlers.GetMigrationProgress) diff --git a/backend/utils/helpers.go b/backend/utils/helpers.go index c13402b..c3a020f 100644 --- a/backend/utils/helpers.go +++ b/backend/utils/helpers.go @@ -7,12 +7,17 @@ import ( "log" "net/http" "os" + "strconv" "strings" + "time" ) // Global logger var Logger *log.Logger +// Application version (separate from AppSettings) +var AppVersion string + func init() { // Initialize logger Logger = log.New(os.Stdout, "dailytxt: ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile) @@ -42,6 +47,16 @@ type AppSettings struct { // Global settings var Settings AppSettings +// SetVersion sets the application version +func SetVersion(version string) { + AppVersion = version +} + +// GetVersion returns the current application version +func GetVersion() string { + return AppVersion +} + // InitSettings loads the application settings func InitSettings() error { // Default settings @@ -406,3 +421,184 @@ func GetUsernameByID(userID int) string { fmt.Printf("user not found with ID: %d\n", userID) return "" } + +// Docker Hub API structures +type DockerHubTag struct { + Name string `json:"name"` +} + +type DockerHubTagsResponse struct { + //Count int `json:"count"` + Results []DockerHubTag `json:"results"` +} + +// Version cache +var ( + lastVersionCheck time.Time + cachedLatestVersion string + cachedLatestWithTest string + versionCacheDuration = time.Hour +) + +// parseVersion parses a semver string and returns major, minor, patch as integers +// Returns -1, -1, -1 if parsing fails +func parseVersion(version string) (int, int, int) { + // Remove 'v' prefix if present + version = strings.TrimPrefix(version, "v") + + // Split by '-' to separate version from pre-release identifiers + parts := strings.Split(version, "-") + if len(parts) == 0 { + return -1, -1, -1 + } + + // Parse the main version part (e.g., "2.3.1") + versionPart := parts[0] + versionNumbers := strings.Split(versionPart, ".") + + if len(versionNumbers) != 3 { + return -1, -1, -1 + } + + major, err1 := strconv.Atoi(versionNumbers[0]) + minor, err2 := strconv.Atoi(versionNumbers[1]) + patch, err3 := strconv.Atoi(versionNumbers[2]) + + if err1 != nil || err2 != nil || err3 != nil { + return -1, -1, -1 + } + + return major, minor, patch +} + +// compareVersions compares two version strings +// Returns 1 if v1 > v2, -1 if v1 < v2, 0 if equal +func compareVersions(v1, v2 string) int { + maj1, min1, pat1 := parseVersion(v1) + maj2, min2, pat2 := parseVersion(v2) + + // If either version is invalid, treat it as lower + if maj1 == -1 { + if maj2 == -1 { + return 0 + } + return -1 + } + if maj2 == -1 { + return 1 + } + + // Compare major version + if maj1 != maj2 { + if maj1 > maj2 { + return 1 + } + return -1 + } + + // Compare minor version + if min1 != min2 { + if min1 > min2 { + return 1 + } + return -1 + } + + // Compare patch version + if pat1 != pat2 { + if pat1 > pat2 { + return 1 + } + return -1 + } + + return 0 +} + +// isStableVersion checks if a version is stable (no pre-release identifiers like "testing") +func isStableVersion(version string) bool { + // Remove 'v' prefix if present + version = strings.TrimPrefix(version, "v") + + // Convert to lowercase for case-insensitive search + lowerVersion := strings.ToLower(version) + + // Check if the version contains "test" (part of "testing", "test", etc.) + if strings.Contains(lowerVersion, "test") { + return false + } + + return true +} + +// GetLatestVersion fetches the latest version information from Docker Hub +// Returns (latest_stable_version, latest_version_including_testing) +func GetLatestVersion() (string, string) { + // Check if we have cached data that's still fresh + if time.Since(lastVersionCheck) < versionCacheDuration && cachedLatestVersion != "" { + return cachedLatestVersion, cachedLatestWithTest + } + + // Fetch tags from Docker Hub + resp, err := http.Get("https://hub.docker.com/v2/repositories/phitux/dailytxt/tags") + if err != nil { + Logger.Printf("Error fetching Docker Hub tags: %v", err) + // Return cached values if available, otherwise empty + return cachedLatestVersion, cachedLatestWithTest + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + Logger.Printf("Docker Hub API returned status %d", resp.StatusCode) + return cachedLatestVersion, cachedLatestWithTest + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + Logger.Printf("Error reading Docker Hub response: %v", err) + return cachedLatestVersion, cachedLatestWithTest + } + + var tagsResponse DockerHubTagsResponse + if err := json.Unmarshal(body, &tagsResponse); err != nil { + Logger.Printf("Error parsing Docker Hub response: %v", err) + return cachedLatestVersion, cachedLatestWithTest + } + + var latestStable, latestOverall string + + // Process all tags + for _, tag := range tagsResponse.Results { + tagName := tag.Name + + // Skip non-version tags like "latest" + if !strings.Contains(tagName, ".") { + continue + } + + // Check if this is a valid semver-like version + maj, min, pat := parseVersion(tagName) + if maj == -1 || min == -1 || pat == -1 { + continue + } + + // Update latest overall version + if latestOverall == "" || compareVersions(tagName, latestOverall) > 0 { + latestOverall = tagName + } + + // Update latest stable version (only if it's stable) + if isStableVersion(tagName) { + if latestStable == "" || compareVersions(tagName, latestStable) > 0 { + latestStable = tagName + } + } + } + + // Update cache + lastVersionCheck = time.Now() + cachedLatestVersion = latestStable + cachedLatestWithTest = latestOverall + + return latestStable, latestOverall +} diff --git a/frontend/src/routes/(authed)/+layout.svelte b/frontend/src/routes/(authed)/+layout.svelte index ad8c698..5a5e387 100644 --- a/frontend/src/routes/(authed)/+layout.svelte +++ b/frontend/src/routes/(authed)/+layout.svelte @@ -24,7 +24,8 @@ faCopy, faCheck, faSun, - faMoon + faMoon, + faCircleUp } from '@fortawesome/free-solid-svg-icons'; import Tag from '$lib/Tag.svelte'; import SelectTimezone from '$lib/SelectTimezone.svelte'; @@ -34,6 +35,8 @@ import Statistics from '$lib/settings/Statistics.svelte'; import Admin from '$lib/settings/Admin.svelte'; import { T, getTranslate, getTolgee } from '@tolgee/svelte'; + import github from '$lib/assets/GitHub-Logo.png'; + import donate from '$lib/assets/bmc-button.png'; const { t } = getTranslate(); const tolgee = getTolgee(['language']); @@ -42,9 +45,65 @@ let inDuration = 150; let outDuration = 150; + let current_version = $state(''); + let latest_stable_version = $state(''); + let latest_overall_version = $state(''); + let updateAvailable = $state(false); + // Active sub-view of settings modal: 'settings' | 'stats' | 'admin' let activeSettingsView = $state('settings'); + // Function to compare version strings (semver-like) + function compareVersions(v1, v2) { + if (!v1 || !v2) return 0; + + const parseVersion = (version) => { + const cleaned = version.replace(/^v/, ''); + const parts = cleaned.split('-')[0].split('.'); + return parts.map((part) => parseInt(part) || 0); + }; + + const version1 = parseVersion(v1); + const version2 = parseVersion(v2); + + for (let i = 0; i < Math.max(version1.length, version2.length); i++) { + const v1Part = version1[i] || 0; + const v2Part = version2[i] || 0; + + if (v1Part > v2Part) return 1; + if (v1Part < v2Part) return -1; + } + + // if both have the same semver-number, check the testing-number (like 2.3.1-testing.3) + // if one does not have anything on the right of "-", then this is the "stable" version + const testingVersion1 = v1.split('-')[1] || ''; + const testingVersion2 = v2.split('-')[1] || ''; + + if (testingVersion1 === '') return 1; + if (testingVersion2 === '') return -1; + + return testingVersion1.localeCompare(testingVersion2) > 0; + } + + // Function to check if updates are available + function checkForUpdates() { + if (!$settings.checkForUpdates) { + updateAvailable = false; + return; + } + + const latestVersion = $settings.includeTestVersions + ? latest_overall_version + : latest_stable_version; + + updateAvailable = compareVersions(latestVersion, current_version) > 0; + } + + // React to changes in settings or version info + $effect(() => { + checkForUpdates(); + }); + $effect(() => { if ($readingMode === true && page.url.pathname !== '/read') { goto('/read'); @@ -56,6 +115,7 @@ onMount(() => { getUserSettings(); getTemplates(); + getVersionInfo(); if (page.url.pathname === '/read') { $readingMode = true; @@ -907,6 +967,21 @@ isExporting = false; }); } + + function getVersionInfo() { + axios + .get(API_URL + '/version') + .then((response) => { + current_version = response.data.current_version; + latest_stable_version = response.data.latest_stable_version; + latest_overall_version = response.data.latest_overall_version; + // Trigger update check after loading version info + checkForUpdates(); + }) + .catch((error) => { + console.error('Error fetching version info:', error); + }); + }
@@ -944,9 +1019,19 @@
- + + {#if updateAvailable} + + {/if} + @@ -1085,8 +1170,13 @@ class="nav-link mb-1 text-start {activeSettingsSection === 'about' ? 'active' : ''}" - onclick={() => scrollToSection('about')}>{$t('settings.about')} scrollToSection('about')} > + {$t('settings.about')} + {#if updateAvailable} + + {/if} +
@@ -2017,9 +2107,108 @@

💡 {$t('settings.about')}

- Version:
- Changelog:
- Link zu github + + {@html $t('settings.about.made_by', { + creator: + 'PhiTux / Marco Kümmel' + })} +
+ + {$t('settings.about.current_version')}: + {current_version}
+ {$t('settings.about.latest_version')}: + {#if !updateAvailable} + {$settings.includeTestVersions + ? latest_overall_version + : latest_stable_version} + {:else} + {$settings.includeTestVersions + ? latest_overall_version + : latest_stable_version} + {/if} + +
+ + {#if updateAvailable} +

+ + {$t('settings.about.update_available')} +

+ {/if} + + {$t('settings.about.version_info')}
+ + + {$t('settings.about.changelog')} + + +
+ {#if $tempSettings.checkForUpdates !== $settings.checkForUpdates || $tempSettings.includeTestVersions !== $settings.includeTestVersions} + {@render unsavedChanges()} + {/if} + +
{$t('settings.about.update_notification')}
+
+ + +
+ +
+ + +
+
+ +
+ + + {$t('settings.about.source_code')}: + + +
+ + {@html $t('settings.about.donate')} + + +