mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-11-13 15:09:20 +01:00
Compare commits
3 Commits
7af6898301
...
f0971ebccc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0971ebccc | ||
|
|
18c8621095 | ||
|
|
b2eb3b3b40 |
@@ -1,2 +0,0 @@
|
||||
[//]: # (This file is used for the release CHANGELOG. Please use Markdown to describe changes.)
|
||||
|
||||
177
app/changelogManager.js
Normal file
177
app/changelogManager.js
Normal file
@@ -0,0 +1,177 @@
|
||||
const { app } = require("electron");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const https = require("https");
|
||||
const config = require("../config");
|
||||
const { logger } = require("../server/logger");
|
||||
|
||||
/** Maximum age of the cached release list before it must be refreshed. */
|
||||
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
/** Filename used to persist changelog data in the Electron user data folder. */
|
||||
const CACHE_FILE_NAME = "changelog-cache.json";
|
||||
|
||||
/** GitHub API endpoint containing the application's published releases. */
|
||||
const RELEASES_URL = "https://api.github.com/repos/MasterAcnolo/Freedom-Loader/releases";
|
||||
|
||||
/** Returns the path of the local changelog cache file. */
|
||||
function getCachePath() {
|
||||
return path.join(app.getPath("userData"), CACHE_FILE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the numeric major, minor, and patch parts from a version string.
|
||||
*
|
||||
* @param {string} version - Version string or release tag
|
||||
* @returns {number[]|null} Parsed version parts, or null when invalid
|
||||
*/
|
||||
function normalizeVersion(version) {
|
||||
const match = String(version || "").match(/(\d+)\.(\d+)\.(\d+)/);
|
||||
return match ? match.slice(1).map(Number) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two semantic version strings.
|
||||
*
|
||||
* @param {string} left - First version to compare
|
||||
* @param {string} right - Second version to compare
|
||||
* @returns {number} Negative, zero, or positive comparison result
|
||||
*/
|
||||
function compareVersions(left, right) {
|
||||
const leftParts = normalizeVersion(left) || [0, 0, 0];
|
||||
const rightParts = normalizeVersion(right) || [0, 0, 0];
|
||||
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
if (leftParts[index] !== rightParts[index]) {
|
||||
return leftParts[index] - rightParts[index];
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Reads and parses the cached changelog, if one is available. */
|
||||
function readCache() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(getCachePath(), "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Persists the fetched releases together with their cache timestamp. */
|
||||
function writeCache(releases) {
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
getCachePath(),
|
||||
JSON.stringify({ cachedAt: Date.now(), releases }, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
} catch (error) {
|
||||
logger.warn(`Unable to cache changelog: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches published releases from GitHub and maps them to the UI data shape.
|
||||
*
|
||||
* @returns {Promise<object[]>} Published releases
|
||||
*/
|
||||
function fetchReleases() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = https.get(
|
||||
RELEASES_URL,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
"User-Agent": "Freedom-Loader",
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
let body = "";
|
||||
response.setEncoding("utf8");
|
||||
response.on("data", (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
response.on("end", () => {
|
||||
// Do not attempt to parse an error response as a release list.
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`GitHub returned HTTP ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Draft releases are not visible to users and should not appear in the changelog.
|
||||
const releases = JSON.parse(body)
|
||||
.filter((release) => !release.draft)
|
||||
.map((release) => ({
|
||||
version: release.tag_name,
|
||||
name: release.name || release.tag_name,
|
||||
date: release.published_at || release.created_at,
|
||||
body: release.body || "No changelog available.",
|
||||
url: release.html_url,
|
||||
}));
|
||||
resolve(releases);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
request.setTimeout(10000, () => {
|
||||
request.destroy(new Error("Changelog request timed out"));
|
||||
});
|
||||
request.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns cached or freshly fetched releases and identifies unread versions.
|
||||
*
|
||||
* @returns {Promise<object>} Changelog data for the renderer
|
||||
*/
|
||||
async function getChangelog() {
|
||||
const cache = readCache();
|
||||
let releases = cache?.releases || [];
|
||||
const cacheIsFresh = cache && cache.releases?.length > 0 && Date.now() - cache.cachedAt < CACHE_TTL_MS;
|
||||
|
||||
// Refresh stale or missing data, while keeping the last cache as a fallback.
|
||||
if (!cacheIsFresh) {
|
||||
try {
|
||||
releases = await fetchReleases();
|
||||
writeCache(releases);
|
||||
} catch (error) {
|
||||
logger.warn(`Unable to retrieve changelog: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const lastSeenVersion = cache?.lastSeenVersion || null;
|
||||
|
||||
// Without a previous version, all available releases are considered unread.
|
||||
const unreadReleases = lastSeenVersion
|
||||
? releases.filter((release) => compareVersions(release.version, lastSeenVersion) > 0)
|
||||
: releases;
|
||||
|
||||
return {
|
||||
// An empty release list means that the fetch failed or returned no releases.
|
||||
error: releases.length === 0,
|
||||
currentVersion: config.version,
|
||||
lastSeenVersion,
|
||||
hasUnread: unreadReleases.length > 0,
|
||||
releases,
|
||||
unreadReleases,
|
||||
};
|
||||
}
|
||||
|
||||
/** Marks the current application version as seen in the local cache. */
|
||||
function markChangelogSeen() {
|
||||
const cache = readCache() || { releases: [] };
|
||||
cache.lastSeenVersion = config.version;
|
||||
cache.cachedAt = cache.cachedAt || 0 ;
|
||||
fs.mkdirSync(path.dirname(getCachePath()), { recursive: true });
|
||||
fs.writeFileSync(getCachePath(), JSON.stringify(cache, null, 2), "utf8");
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = { getChangelog, markChangelogSeen };
|
||||
@@ -21,6 +21,7 @@ const { userThemesPath } = require("../server/helpers/path.helpers");
|
||||
const { createSystemTray, destroyTray } = require("./tray");
|
||||
const {sendReport, checkReportService, getReportServiceStatus} = require("./sendReport");
|
||||
const { isUpdateAvailable, downloadUpdate, installUpdate } = require("./autoUpdater");
|
||||
const { getChangelog, markChangelogSeen } = require("./changelogManager");
|
||||
|
||||
/**
|
||||
* Security whitelist for feature flags that can be modified at runtime.
|
||||
@@ -71,6 +72,16 @@ function registerIpcHandlers(getMainWindow) {
|
||||
*/
|
||||
ipcMain.handle("version", () => config.version);
|
||||
|
||||
/**
|
||||
* Retrieves the changelog from cache or GitHub API.
|
||||
*/
|
||||
ipcMain.handle("get-changelog", () => getChangelog());
|
||||
|
||||
/**
|
||||
* Marks all changelog entries as seen and updates cache.
|
||||
*/
|
||||
ipcMain.handle("mark-changelog-seen", () => markChangelogSeen());
|
||||
|
||||
/**
|
||||
* Returns runtime feature configuration object.
|
||||
*/
|
||||
|
||||
10
preload.js
10
preload.js
@@ -69,6 +69,16 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
*/
|
||||
getVersion: () => ipcRenderer.invoke("version"),
|
||||
|
||||
/**
|
||||
* Returns the changelog data
|
||||
*/
|
||||
getChangelog: () => ipcRenderer.invoke("get-changelog"),
|
||||
|
||||
/**
|
||||
* Marks the current changelog version as seen by the user.
|
||||
*/
|
||||
markChangelogSeen: () => ipcRenderer.invoke("mark-changelog-seen"),
|
||||
|
||||
/**
|
||||
* Validates a download path before using it for file operations.
|
||||
*
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<button id="website-btn" title="Website">Website</button>
|
||||
<button id="wiki-btn" title="Wiki">Wiki</button>
|
||||
<button id="workshop-btn" title="Workshop">Theme Workshop</button>
|
||||
<button id="changelog-btn" title="Changelog">Changelog</button>
|
||||
<!-- <button id="theme-btn" title="Theme">Theme</button> -->
|
||||
</div>
|
||||
<div class="window-controls">
|
||||
@@ -303,6 +304,21 @@
|
||||
<!-- Toast Container -->
|
||||
<div id="toast-container"></div>
|
||||
|
||||
<div class="changelog-overlay" id="changelog-overlay" hidden>
|
||||
<section class="changelog-modal" role="dialog" aria-modal="true" aria-labelledby="changelog-title">
|
||||
<div class="changelog-header">
|
||||
<div>
|
||||
<span class="changelog-eyebrow">Freedom Loader</span>
|
||||
<h2 id="changelog-title">What's new</h2>
|
||||
</div>
|
||||
<button class="changelog-close" id="changelog-close" type="button" title="Close changelog" aria-label="Close changelog">×</button>
|
||||
</div>
|
||||
<div class="changelog-list" id="changelog-list">
|
||||
<p class="changelog-loading">Loading changelog...</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<h2 id="reference"> Because Why Not ? </h2>
|
||||
|
||||
<div id="updateAvailable" style="display:none">
|
||||
@@ -343,6 +359,7 @@
|
||||
<script src="script/customthemes.js"></script>
|
||||
<script src="script/reportBug.js"></script>
|
||||
<script src="script/autoUpdate.js"></script>
|
||||
<script type="module" src="script/changelog.js"></script>
|
||||
<script type="module" src="script/topbar.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
277
public/script/changelog.js
Normal file
277
public/script/changelog.js
Normal file
@@ -0,0 +1,277 @@
|
||||
/** DOM elements used by the changelog modal. */
|
||||
const UI = {
|
||||
overlay: document.getElementById("changelog-overlay"),
|
||||
list: document.getElementById("changelog-list"),
|
||||
closeBtn: document.getElementById("changelog-close"),
|
||||
openBtn: document.getElementById("changelog-btn")
|
||||
};
|
||||
|
||||
/** Changelog payload loaded from the Electron main process. */
|
||||
let changelogData = null;
|
||||
|
||||
/**
|
||||
* Formats an ISO release date for the current locale.
|
||||
*
|
||||
* @param {string} dateString - Release date returned by GitHub
|
||||
* @returns {string} Localized date or an empty string
|
||||
*/
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return "";
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}).format(new Date(dateString));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses inline Markdown and appends safe DOM nodes to a parent element.
|
||||
*
|
||||
* @param {HTMLElement} parent - Element receiving the parsed content
|
||||
* @param {string} value - Inline Markdown text
|
||||
*/
|
||||
function parseInline(parent, value) {
|
||||
const tokenPattern = /(\*\*|__)(.+?)\1|(?<!\*)\*([^*]+)\*(?!\*)|(?<!_)_([^_]+)_(?!_)|`([^`]+)`|~~([^~]+)~~|\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
while ((match = tokenPattern.exec(value)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
parent.appendChild(document.createTextNode(value.slice(lastIndex, match.index)));
|
||||
}
|
||||
|
||||
const element = match[7]
|
||||
? document.createElement("a")
|
||||
: document.createElement(match[1] ? "strong" : match[3] || match[4] ? "em" : match[5] ? "code" : "del");
|
||||
|
||||
element.textContent = match[7] || match[2] || match[3] || match[4] || match[5] || match[6];
|
||||
|
||||
if (match[7]) {
|
||||
element.href = match[8];
|
||||
element.target = "_blank";
|
||||
element.rel = "noopener noreferrer";
|
||||
}
|
||||
|
||||
parent.appendChild(element);
|
||||
lastIndex = tokenPattern.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < value.length) {
|
||||
parent.appendChild(document.createTextNode(value.slice(lastIndex)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses block-level Markdown such as headings, lists, quotes and code blocks.
|
||||
*
|
||||
* @param {HTMLElement} parent - Element receiving the parsed content
|
||||
* @param {string} markdown - Release body in Markdown format
|
||||
*/
|
||||
function parseBlock(parent, markdown) {
|
||||
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|
||||
let paragraph = [];
|
||||
let list = null;
|
||||
let codeBlock = null;
|
||||
|
||||
const flushParagraph = () => {
|
||||
if (!paragraph.length) return;
|
||||
const element = document.createElement("p");
|
||||
paragraph.forEach((line, index) => {
|
||||
if (index) element.appendChild(document.createElement("br"));
|
||||
parseInline(element, line);
|
||||
});
|
||||
parent.appendChild(element);
|
||||
paragraph = [];
|
||||
};
|
||||
|
||||
const closeList = () => {
|
||||
if (list) {
|
||||
parent.appendChild(list);
|
||||
list = null;
|
||||
}
|
||||
};
|
||||
|
||||
lines.forEach((line) => {
|
||||
if (line.trim().startsWith("```")) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
if (codeBlock) {
|
||||
const code = document.createElement("code");
|
||||
code.textContent = codeBlock.join("\n");
|
||||
const pre = document.createElement("pre");
|
||||
pre.appendChild(code);
|
||||
parent.appendChild(pre);
|
||||
codeBlock = null;
|
||||
} else {
|
||||
codeBlock = [];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (codeBlock) {
|
||||
codeBlock.push(line);
|
||||
return;
|
||||
}
|
||||
|
||||
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
||||
const unorderedItem = line.match(/^\s*[-*+]\s+(.+)$/);
|
||||
const orderedItem = line.match(/^\s*\d+[.]\s+(.+)$/);
|
||||
const quote = line.match(/^\s*>\s?(.*)$/);
|
||||
|
||||
if (!line.trim()) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
} else if (heading) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
const element = document.createElement(`h${heading[1].length}`);
|
||||
parseInline(element, heading[2]);
|
||||
parent.appendChild(element);
|
||||
} else if (unorderedItem || orderedItem) {
|
||||
flushParagraph();
|
||||
const isOrdered = Boolean(orderedItem);
|
||||
if (!list || list.tagName.toLowerCase() !== (isOrdered ? "ol" : "ul")) {
|
||||
closeList();
|
||||
list = document.createElement(isOrdered ? "ol" : "ul");
|
||||
}
|
||||
const item = document.createElement("li");
|
||||
parseInline(item, (unorderedItem || orderedItem)[1]);
|
||||
list.appendChild(item);
|
||||
} else if (quote) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
const element = document.createElement("blockquote");
|
||||
parseInline(element, quote[1]);
|
||||
parent.appendChild(element);
|
||||
} else if (/^\s*([-*_])\s*\1\s*\1\s*$/.test(line)) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
parent.appendChild(document.createElement("hr"));
|
||||
} else {
|
||||
paragraph.push(line);
|
||||
}
|
||||
});
|
||||
|
||||
flushParagraph();
|
||||
closeList();
|
||||
if (codeBlock) {
|
||||
const code = document.createElement("code");
|
||||
code.textContent = codeBlock.join("\n");
|
||||
const pre = document.createElement("pre");
|
||||
pre.appendChild(code);
|
||||
parent.appendChild(pre);
|
||||
}
|
||||
}
|
||||
|
||||
/** Renders the empty state when no releases are available. */
|
||||
function renderEmpty() {
|
||||
const emptyMessage = document.createElement("p");
|
||||
emptyMessage.className = "changelog-empty";
|
||||
emptyMessage.textContent = "No changelog available.";
|
||||
UI.list.appendChild(emptyMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders one release entry in the changelog list.
|
||||
*
|
||||
* @param {object} release - Release data from the backend
|
||||
*/
|
||||
function renderEntry(release) {
|
||||
const article = document.createElement("article");
|
||||
article.className = "changelog-entry";
|
||||
|
||||
const heading = document.createElement("div");
|
||||
heading.className = "changelog-entry-heading";
|
||||
|
||||
const title = document.createElement("h3");
|
||||
title.textContent = release.name;
|
||||
|
||||
const metadata = document.createElement("span");
|
||||
metadata.textContent = [release.version, formatDate(release.date)]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
|
||||
heading.append(title, metadata);
|
||||
|
||||
const body = document.createElement("div");
|
||||
body.className = "changelog-body";
|
||||
parseBlock(body, release.body);
|
||||
|
||||
article.append(heading, body);
|
||||
UI.list.appendChild(article);
|
||||
}
|
||||
|
||||
/** Renders all releases, replacing the current modal contents. */
|
||||
function renderReleases(releases) {
|
||||
UI.list.replaceChildren();
|
||||
if (!releases.length) {
|
||||
renderEmpty();
|
||||
return;
|
||||
}
|
||||
releases.forEach((release) => renderEntry(release));
|
||||
}
|
||||
|
||||
/** Opens the changelog modal and moves focus to its close button. */
|
||||
function openChangelog(releases) {
|
||||
renderReleases(releases);
|
||||
UI.overlay.hidden = false;
|
||||
UI.closeBtn?.focus();
|
||||
}
|
||||
|
||||
/** Closes the modal and records the current version as seen. */
|
||||
async function closeChangelog() {
|
||||
UI.overlay.hidden = true;
|
||||
await window.electronAPI.markChangelogSeen();
|
||||
}
|
||||
|
||||
/** Loads changelog data and opens the modal when unread releases exist. */
|
||||
async function loadChangelog() {
|
||||
try {
|
||||
changelogData = await window.electronAPI.getChangelog();
|
||||
if (!changelogData.error && changelogData.hasUnread) {
|
||||
openChangelog(changelogData.unreadReleases);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Unable to load changelog:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles a click on the button that opens the changelog. */
|
||||
async function handleOpenClick() {
|
||||
if (!changelogData || changelogData.error) {
|
||||
changelogData = await window.electronAPI.getChangelog();
|
||||
}
|
||||
|
||||
if (changelogData.error) {
|
||||
window.showError("Unable to load changelog. Please check your network connection.");
|
||||
return;
|
||||
}
|
||||
|
||||
openChangelog(changelogData.releases);
|
||||
}
|
||||
|
||||
/** Closes the modal when the user clicks on the overlay background. */
|
||||
function handleOverlayClick(event) {
|
||||
if (event.target === UI.overlay) closeChangelog();
|
||||
}
|
||||
|
||||
/** Closes the modal when Escape is pressed. */
|
||||
function handleKeydown(event) {
|
||||
if (event.key === "Escape" && !UI.overlay.hidden) closeChangelog();
|
||||
}
|
||||
|
||||
/** Registers all changelog modal event listeners. */
|
||||
function bindEvents() {
|
||||
UI.openBtn?.addEventListener("click", handleOpenClick);
|
||||
UI.closeBtn?.addEventListener("click", closeChangelog);
|
||||
UI.overlay?.addEventListener("click", handleOverlayClick);
|
||||
document.addEventListener("keydown", handleKeydown);
|
||||
}
|
||||
|
||||
/** Initializes the changelog data flow and modal interactions. */
|
||||
function init() {
|
||||
bindEvents();
|
||||
loadChangelog();
|
||||
}
|
||||
|
||||
init();
|
||||
229
public/styles/components/changelog.css
Normal file
229
public/styles/components/changelog.css
Normal file
@@ -0,0 +1,229 @@
|
||||
.changelog-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background: rgba(5, 10, 16, 0.72);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.changelog-overlay[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.changelog-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(680px, 100%);
|
||||
max-height: min(720px, 88vh);
|
||||
overflow: hidden;
|
||||
color: var(--settings-text-color, #f8fafc);
|
||||
background: var(--settings-modal-bg-color, #18212b);
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.changelog-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 24px 28px 18px;
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.changelog-eyebrow {
|
||||
color: var(--fill-accent, #0ea5e9);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.changelog-header h2 {
|
||||
margin: 5px 0 0;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.changelog-close {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.changelog-close:hover {
|
||||
background: rgba(148, 163, 184, 0.14);
|
||||
}
|
||||
|
||||
.changelog-list {
|
||||
overflow-y: auto;
|
||||
padding: 20px 28px 28px;
|
||||
scrollbar-color: var(--fill-accent, #0ea5e9) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.changelog-list::-webkit-scrollbar,
|
||||
.changelog-body pre::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
.changelog-list::-webkit-scrollbar-track,
|
||||
.changelog-body pre::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.changelog-list::-webkit-scrollbar-thumb,
|
||||
.changelog-body pre::-webkit-scrollbar-thumb {
|
||||
background: color-mix(in srgb, var(--fill-accent, #0ea5e9) 70%, transparent);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 999px;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.changelog-list::-webkit-scrollbar-thumb:hover,
|
||||
.changelog-body pre::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--fill-accent, #0ea5e9);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.changelog-entry {
|
||||
padding: 18px 0;
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.16);
|
||||
}
|
||||
|
||||
.changelog-entry:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.changelog-entry:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.changelog-entry-heading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.changelog-entry-heading h3 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.changelog-entry-heading span {
|
||||
flex-shrink: 0;
|
||||
color: var(--settings-subtitle-color, #94a3b8);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.changelog-body {
|
||||
margin: 12px 0 0;
|
||||
color: var(--settings-subtitle-color, #cbd5e1);
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.changelog-body p {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.changelog-body p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.changelog-body h1,
|
||||
.changelog-body h2,
|
||||
.changelog-body h3,
|
||||
.changelog-body h4,
|
||||
.changelog-body h5,
|
||||
.changelog-body h6 {
|
||||
margin: 14px 0 8px;
|
||||
color: var(--settings-text-color, #f8fafc);
|
||||
}
|
||||
|
||||
.changelog-body h1 { font-size: 20px; }
|
||||
.changelog-body h2 { font-size: 18px; }
|
||||
.changelog-body h3 { font-size: 16px; }
|
||||
.changelog-body h4,
|
||||
.changelog-body h5,
|
||||
.changelog-body h6 { font-size: 14px; }
|
||||
|
||||
.changelog-body ul,
|
||||
.changelog-body ol {
|
||||
margin: 8px 0 12px;
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.changelog-body li + li {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.changelog-body code {
|
||||
padding: 2px 5px;
|
||||
color: var(--settings-text-color, #f8fafc);
|
||||
background: rgba(148, 163, 184, 0.16);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.changelog-body pre {
|
||||
padding: 10px 12px;
|
||||
overflow-x: auto;
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.changelog-body pre code {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.changelog-body blockquote {
|
||||
margin: 10px 0;
|
||||
padding-left: 12px;
|
||||
color: var(--settings-subtitle-color, #cbd5e1);
|
||||
border-left: 3px solid var(--fill-accent, #0ea5e9);
|
||||
}
|
||||
|
||||
.changelog-body a {
|
||||
color: var(--fill-accent, #0ea5e9);
|
||||
}
|
||||
|
||||
.changelog-loading,
|
||||
.changelog-empty {
|
||||
margin: 0;
|
||||
color: var(--settings-subtitle-color, #cbd5e1);
|
||||
}
|
||||
|
||||
@media (max-width: 780px) {
|
||||
.changelog-overlay {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.changelog-header,
|
||||
.changelog-list {
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.changelog-entry-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@
|
||||
.custom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding-left: 12px;
|
||||
gap: 4px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
/* RIGHT - window buttons */
|
||||
@@ -43,11 +43,11 @@
|
||||
|
||||
/* Custom buttons (Logs / Wiki / etc.) */
|
||||
.custom-controls button {
|
||||
height: 32px;
|
||||
padding: 0 14px;
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
|
||||
font-size: 15px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.custom-controls button:hover {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
@import url("components/progressBar.css");
|
||||
@import url("components/toast.css");
|
||||
@import url("components/autoUpdate.css");
|
||||
@import url("components/changelog.css");
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
||||
|
||||
|
||||
21
scripts/RELEASE-CHANGELOG.md
Normal file
21
scripts/RELEASE-CHANGELOG.md
Normal file
@@ -0,0 +1,21 @@
|
||||
### Features
|
||||
- **In-App Feedback**: Added a built-in feedback form allowing users to send feedback, bug reports, and daily logs directly to the development team.
|
||||
- **Community Contributions**: GitHub issues are now better maintained on the repository. If you want to contribute to the project, feel free to drop by and check out the `good first issue` tags!
|
||||
|
||||
### Improvements
|
||||
- **Bundle Size**: Optimized assets and dependencies, reducing the overall application bundle size by approximately 50% on specific version.
|
||||
- **Updater UI**: Reworked the update notification for better clarity. Windows and AppImage users can download updates seamlessly in-app, while unsupported Linux package users are gracefully redirected to the manual download page. Flatpak (.rpm) and Snap (.snap) users are not informed, because that updates are handled by their respective package managers.
|
||||
- **Toast Notifications**: Refactored the style and overall UX of toast alerts.
|
||||
- **Code Quality**: Implemented a linter to enforce coding standards.
|
||||
|
||||
### Bug Fixes
|
||||
- **Auto-Updater**: Fixed a major issue causing 404 Not Found errors when attempting to download updates for `.AppImage` releases.
|
||||
- **Linux Updates**: Fixed update crashes on `.rpm` and `.snap` packages. In-app update flows are now disabled for these formats to let the OS natively handle the process.
|
||||
- **Windows Logs**: Fixed a file naming issue with the log transport system on Windows.
|
||||
- **Desktop Entry Name**: Fixed the desktop entry name for Linux users, ensuring it displays correctly in application menus.
|
||||
|
||||
### Chores
|
||||
- **Dependencies**: Removed `dotenv` from the project.
|
||||
- **URL**: A new URL for the website is now available ! [freedomloader.acnolo.fr](https://freedomloader.acnolo.fr)
|
||||
- **Dev**: Add new npm script. To check linux file quality
|
||||
|
||||
@@ -67,11 +67,11 @@ echo "Workspace cleaned (dist/ and srpm-out/ removed)"
|
||||
# ------------------------------------------------------------
|
||||
# Step 0.3 - Retrieve Changelog
|
||||
# ------------------------------------------------------------
|
||||
if [ ! -f "$ROOT_DIR/CHANGELOG.md" ]; then
|
||||
echo "Warning: CHANGELOG.md not found, release notes will be empty."
|
||||
if [ ! -f "$ROOT_DIR/scripts/RELEASE-CHANGELOG.md" ]; then
|
||||
echo "Warning: RELEASE-CHANGELOG.md not found, release notes will be empty."
|
||||
CHANGELOG=""
|
||||
else
|
||||
CHANGELOG="$(cat "$ROOT_DIR/CHANGELOG.md")"
|
||||
CHANGELOG="$(cat "$ROOT_DIR/scripts/RELEASE-CHANGELOG.md")"
|
||||
echo "Changelog retrieved successfully"
|
||||
fi
|
||||
# ------------------------------------------------------------
|
||||
@@ -132,7 +132,7 @@ if [ "$DRY_RUN" = false ]; then
|
||||
"$ROOT_DIR/dist/Freedom-Loader-Setup-${VERSION}.exe" \
|
||||
"$ROOT_DIR/dist/Freedom-Loader-Setup-${VERSION}.exe.blockmap" \
|
||||
"$ROOT_DIR/dist/freedom-loader-${VERSION}.x86_64.rpm" \
|
||||
"$ROOT_DIR/dist/freedom-loader_${VERSION}_amd64.deb" \
|
||||
"$ROOT_DIR/dist/Freedom-Loader_${VERSION}_amd64.deb" \
|
||||
"$ROOT_DIR/dist/Freedom-Loader-${VERSION}.AppImage" \
|
||||
"$ROOT_DIR/dist/freedom-loader_${VERSION}_amd64.snap" \
|
||||
"$ROOT_DIR/dist/latest.yml" \
|
||||
|
||||
Reference in New Issue
Block a user