Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
09da83b
release: Update version to v1.3.30
WCY-dt Sep 5, 2026
0ba95a8
fix(articles): preserve position after relative mark read (#1055)
WCY-dt Sep 5, 2026
4d75096
fix(summary): collapse card height with content (#1056)
WCY-dt Sep 5, 2026
a796244
fix(summary): stabilize heading during generation (#1068)
WCY-dt Sep 5, 2026
cd4c18d
feat(reader): show article reading progress (#1058)
WCY-dt Sep 5, 2026
bbde59e
feat(reader): add back-to-top control (#1057)
WCY-dt Sep 5, 2026
d16dca1
style(reader): refine article measure and spacing (#1059)
WCY-dt Sep 5, 2026
71d9945
style(ai): normalize dark mode text colors (#1060)
WCY-dt Sep 5, 2026
3b96b5d
fix(settings): anchor AI toggles during expansion (#1061)
WCY-dt Sep 5, 2026
e5f9591
fix(i18n): localize AI and feed status messages (#1062)
WCY-dt Sep 5, 2026
92b1624
feat(settings): make bulk read confirmation optional (#1063)
WCY-dt Sep 5, 2026
f04180b
style(articles): clarify empty unread state (#1064)
WCY-dt Sep 5, 2026
825b70d
style(update): compact up-to-date status card (#1065)
WCY-dt Sep 5, 2026
ab043a5
style(articles): show exact time on hover (#1066)
WCY-dt Sep 5, 2026
3894e66
feat(settings): add searchable settings navigation (#1067)
WCY-dt Sep 5, 2026
120cbcb
style(articles): standardize read state icons (#1070)
WCY-dt Sep 5, 2026
f837726
style(ui): unify shared interface primitives (#1071)
WCY-dt Sep 5, 2026
66f92c8
fix(settings): make settings search direct and predictable (#1067)
WCY-dt Sep 5, 2026
268fb99
style(sidebar): integrate feed sorting with navigation (#1071)
WCY-dt Sep 5, 2026
b55dbb8
doc: Update changelog
WCY-dt Sep 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@ All notable changes to MrRSS will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.3.30] - 2026-09-06

### Added

- Add bilingual settings search with ranked, keyboard-accessible results that open the exact setting without hiding navigation. (#1067)
- Add an opt-out confirmation setting for bulk mark-as-read actions across article and sidebar menus. (#1063)
- Add a floating back-to-top button for long articles. (#1057)
- Show a live reading-progress bar while scrolling through an article. (#1058)

### Changed

- Replace the sidebar's boxed native sort field with a lightweight menu aligned with the surrounding navigation controls. (#1071)
- Unify shared button, form-control, dropdown, settings-card and keyboard-focus styling. (#1071)
- Standardize read and unread controls on hollow and filled circle icons across article views. (#1070)
- Show publication timestamps to minute precision when hovering article dates. (#1066)
- Let the up-to-date status card shrink to its content when no download action is present. (#1065)
- Replace the misleading empty unread-list error copy with a clear all-caught-up state. (#1064)
- Center article bodies at a readable 68-character line width with more consistent paragraph and list spacing. (#1059)

### Fixed

- Localize AI chat failures and feed update-status tooltips instead of exposing English backend values. (#1062)
- Keep AI feature toggles anchored while their dependent settings expand or collapse. (#1061)
- Use the theme's neutral primary text color consistently in AI profile forms and article chat. (#1060)
- Keep the article-summary heading stationary when generated content and action buttons appear. (#1068)
- Let collapsed article-summary cards shrink to their header instead of leaving a large blank area. (#1056)
- Keep the article list anchored at the selected item after marking newer or older articles as read. (#1055)

## [1.3.29] - 2026-09-05

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<strong>English</strong> | <a href="README_zh.md">简体中文</a>
</p>

[![Version](https://img.shields.io/badge/version-1.3.29-blue.svg)](https://github.com/DevXDojo/MrRSS/releases)
[![Version](https://img.shields.io/badge/version-1.3.30-blue.svg)](https://github.com/DevXDojo/MrRSS/releases)
[![License](https://img.shields.io/badge/license-GPLv3-green.svg)](LICENSE)
[![Go](https://img.shields.io/badge/Go-1.27+-00ADD8?logo=go)](https://go.dev/)
[![Wails](https://img.shields.io/badge/Wails-v3%20alpha-red)](https://wails.io/)
Expand Down
2 changes: 1 addition & 1 deletion README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<a href="README.md">English</a> | <strong>简体中文</strong>
</p>

[![Version](https://img.shields.io/badge/version-1.3.29-blue.svg)](https://github.com/DevXDojo/MrRSS/releases)
[![Version](https://img.shields.io/badge/version-1.3.30-blue.svg)](https://github.com/DevXDojo/MrRSS/releases)
[![License](https://img.shields.io/badge/license-GPLv3-green.svg)](LICENSE)
[![Go](https://img.shields.io/badge/Go-1.27+-00ADD8?logo=go)](https://go.dev/)
[![Wails](https://img.shields.io/badge/Wails-v3%20alpha-red)](https://wails.io/)
Expand Down
4 changes: 2 additions & 2 deletions build/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ info:
description: "A modern, standalone RSS reader" # The application description
copyright: "Copyright © 2026" # Copyright text
comments: "Built with Wails" # Comments
version: "1.3.29" # The application version
version: "1.3.30" # The application version

# iOS build configuration (uncomment to customise iOS project generation)
# Note: Keys under `ios` OVERRIDE values under `info` when set.
Expand All @@ -21,7 +21,7 @@ info:
# # The display name shown under the app icon (CFBundleDisplayName/CFBundleName)
# displayName: "MrRSS"
# # The app version to embed in Info.plist (CFBundleShortVersionString/CFBundleVersion)
# version: "1.3.29"
# version: "1.3.30"
# # The company/organisation name for templates and project settings
# company: "Ch3nyang"
# # Additional comments to embed in Info.plist metadata
Expand Down
2 changes: 1 addition & 1 deletion build/linux/Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ tasks:
cmds:
- cd {{.BIN_DIR}} && tar -czf {{.APP_NAME}}-{{.VERSION}}-linux-{{.ARCH}}.tar.gz {{.APP_NAME}}
vars:
VERSION: '1.3.29'
VERSION: '1.3.30'
ARCH: '{{.ARCH | default "amd64"}}'

generate:dotdesktop:
Expand Down
2 changes: 1 addition & 1 deletion build/linux/nfpm/nfpm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
name: "MrRSS"
arch: ${GOARCH}
platform: "linux"
version: "1.3.29"
version: "1.3.30"
section: "default"
priority: "extra"
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
Expand Down
1 change: 1 addition & 0 deletions config/defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"baidu_app_id": "",
"baidu_secret_key": "",
"close_to_tray": true,
"confirm_mark_as_read": true,
"content_font_family": "system",
"content_font_size": 16,
"content_line_height": "1.6",
Expand Down
2 changes: 1 addition & 1 deletion docs/SERVER_MODE/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"name": "GPL-3.0",
"url": "https://www.gnu.org/licenses/gpl-3.0.en.html"
},
"version": "1.3.29"
"version": "1.3.30"
},
"host": "localhost:1234",
"basePath": "/api",
Expand Down
4 changes: 2 additions & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "mrrss-frontend",
"private": true,
"version": "1.3.29",
"version": "1.3.30",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/components/article/ArticleCardItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { useI18n } from 'vue-i18n';
import { PhEyeSlash, PhStar, PhClockCountdown } from '@phosphor-icons/vue';
import type { Article } from '@/types/models';
import { formatDate as formatDateUtil } from '@/utils/date';
import { formatDate as formatDateUtil, formatExactDateTime } from '@/utils/date';
import { getProxiedMediaUrl, isMediaCacheEnabled } from '@/utils/mediaProxy';
import { useAppStore } from '@/stores/app';
import { imageCache } from '@/utils/imageCache';
Expand Down Expand Up @@ -205,7 +205,9 @@ function handleImageError(event: Event) {
<!-- Meta info -->
<div class="card-meta">
<span class="feed-name">{{ article.feed_title }}</span>
<span class="publish-date">{{ formatDateWithI18n(article.published_at) }}</span>
<span class="publish-date" :title="formatExactDateTime(article.published_at, locale)">
{{ formatDateWithI18n(article.published_at) }}
</span>
</div>
</div>
</div>
Expand Down
33 changes: 9 additions & 24 deletions frontend/src/components/article/ArticleChatPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
PhPencil,
} from '@phosphor-icons/vue';
import type { Article } from '@/types/models';
import { readAIError } from '@/utils/aiError';

interface ChatMessage {
id: number;
Expand Down Expand Up @@ -292,36 +293,20 @@ async function sendMessage() {

isFirstMessage.value = false;
} else {
const errorText = await response.text();
console.error('AI chat request failed:', response.status);

let errorMessage = t('article.chat.aiChatError');
let persistedSessionID = 0;
try {
const errorData = JSON.parse(errorText);
persistedSessionID = Number(errorData.session_id || 0);
// Extract error message from various possible formats
if (typeof errorData.error === 'string') {
errorMessage = errorData.error;
} else if (errorData.error?.message) {
errorMessage = errorData.error.message;
} else if (errorData.message) {
errorMessage = errorData.message;
} else if (typeof errorData === 'string') {
errorMessage = errorData;
} else {
errorMessage = t('article.chat.aiChatError');
}
} catch {
errorMessage = t('article.chat.aiChatError');
}
const chatError = await readAIError(response);
const errorPayload =
chatError.payload !== null && typeof chatError.payload === 'object'
? (chatError.payload as Record<string, unknown>)
: null;
const persistedSessionID = Number(errorPayload?.session_id || 0);

if (persistedSessionID > 0) {
currentSessionId.value = persistedSessionID;
await loadSessions();
await selectSession(persistedSessionID, true);
}
window.showToast(errorMessage, 'error');
window.showToast(chatError.message, 'error');
}
} catch (e) {
console.error('AI chat error:', e);
Expand Down Expand Up @@ -361,7 +346,7 @@ const currentSessionTitle = computed(() => {
<div
v-if="isOpen"
ref="panelElement"
class="chat-panel fixed bottom-10 right-4 md:bottom-14 md:right-6 w-[500px] h-[600px] bg-bg-primary border border-border rounded-xl shadow-2xl flex flex-col z-50"
class="chat-panel fixed bottom-10 right-4 md:bottom-14 md:right-6 w-[500px] h-[600px] bg-bg-primary text-text-primary border border-border rounded-xl shadow-2xl flex flex-col z-50"
:class="{ 'select-none': isResizing }"
>
<!-- Header -->
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/article/ArticleContent.css
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@
}

.prose p {
margin-top: 1.25em;
margin-bottom: 1.25em;
margin-top: 1em;
margin-bottom: 1em;
line-height: var(--content-line-height, 1.75);
/* Prevent overflow from long words/links */
overflow-wrap: break-word;
Expand Down Expand Up @@ -365,8 +365,8 @@

.prose ul,
.prose ol {
margin-top: 1.25em;
margin-bottom: 1.25em;
margin-top: 1em;
margin-bottom: 1em;
padding-left: 1.625em;
}

Expand Down
67 changes: 65 additions & 2 deletions frontend/src/components/article/ArticleContent.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount, computed, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { PhSpinnerGap, PhArticleNyTimes } from '@phosphor-icons/vue';
import { PhSpinnerGap, PhArticleNyTimes, PhArrowUp } from '@phosphor-icons/vue';
import type { Article } from '@/types/models';
import ArticleTitle from './parts/ArticleTitle.vue';
import ArticleSummary from './parts/ArticleSummary.vue';
Expand Down Expand Up @@ -76,6 +76,8 @@ const { settings: appSettings, fetchSettings } = useSettings();
const store = useAppStore();
const isChatPanelOpen = ref(false);
const articleScrollContainer = ref<HTMLElement | null>(null);
const readingProgress = ref(0);
const showBackToTop = ref(false);
const { onContextMenu: onTextContextMenu } = useArticleSelectionMenu(articleScrollContainer);
const ARTICLE_SCROLL_POSITIONS_KEY = 'mrrssArticleScrollPositions';
let scrollSaveTimer: ReturnType<typeof setTimeout> | null = null;
Expand Down Expand Up @@ -246,6 +248,30 @@ function scheduleSaveArticleScrollPosition() {
}, 200);
}

function updateReadingProgress() {
const container = articleScrollContainer.value;
if (!container) {
readingProgress.value = 0;
return;
}

const scrollableHeight = container.scrollHeight - container.clientHeight;
showBackToTop.value = container.scrollTop > 480;
readingProgress.value =
scrollableHeight > 0
? Math.min(100, Math.max(0, (container.scrollTop / scrollableHeight) * 100))
: 100;
}

function handleArticleScroll() {
updateReadingProgress();
scheduleSaveArticleScrollPosition();
}

function scrollToArticleTop() {
articleScrollContainer.value?.scrollTo({ top: 0, behavior: 'smooth' });
}

function restoreArticleScrollPosition(articleId: number | null | undefined = props.article?.id) {
const container = articleScrollContainer.value;
if (!container || !articleId) return;
Expand Down Expand Up @@ -902,6 +928,8 @@ watch(
if (articleScrollContainer.value) {
articleScrollContainer.value.scrollTop = 0;
}
readingProgress.value = 0;
showBackToTop.value = false;
pendingScrollRestoreArticleId = newId ?? null;
pendingScrollRestoreAttempts = 0;

Expand Down Expand Up @@ -1019,6 +1047,8 @@ watch(

onMounted(async () => {
await loadSettings();
await nextTick();
updateReadingProgress();
if (props.article) {
Comment on lines 1048 to 1052
pendingScrollRestoreArticleId = props.article.id;
pendingScrollRestoreAttempts = 0;
Expand Down Expand Up @@ -1117,12 +1147,25 @@ onBeforeUnmount(() => {

<template>
<div class="relative flex-1 overflow-hidden bg-bg-primary">
<div
class="pointer-events-none absolute inset-x-0 top-0 z-20 h-0.5 bg-border/40"
role="progressbar"
:aria-label="t('article.content.readingProgress')"
aria-valuemin="0"
aria-valuemax="100"
:aria-valuenow="Math.round(readingProgress)"
>
<div
class="h-full bg-accent transition-[width] duration-100 ease-out"
:style="{ width: `${readingProgress}%` }"
></div>
</div>
<div
ref="articleScrollContainer"
class="h-full overflow-y-scroll p-3 sm:p-6 scroll-smooth"
@click="handleContainerClick"
@contextmenu="onTextContextMenu"
@scroll="scheduleSaveArticleScrollPosition"
@scroll="handleArticleScroll"
>
<div
class="max-w-3xl mx-auto bg-bg-primary [container-type:inline-size]"
Expand Down Expand Up @@ -1207,6 +1250,26 @@ onBeforeUnmount(() => {
:scroll-container="articleScrollContainer"
/>

<Transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="translate-y-2 opacity-0"
enter-to-class="translate-y-0 opacity-100"
leave-active-class="transition duration-150 ease-in"
leave-from-class="translate-y-0 opacity-100"
leave-to-class="translate-y-2 opacity-0"
>
<button
v-if="showBackToTop"
class="absolute bottom-6 z-30 flex h-10 w-10 items-center justify-center rounded-full border border-border bg-bg-secondary/95 text-text-secondary shadow-lg backdrop-blur-sm transition-colors hover:bg-bg-tertiary hover:text-text-primary"
:class="showChatButton && !isChatPanelOpen ? 'right-24' : 'right-6'"
:title="t('article.action.backToTop')"
:aria-label="t('article.action.backToTop')"
@click="scrollToArticleTop"
>
<PhArrowUp :size="20" />
</button>
</Transition>

<!-- Chat Button (shown when content is loaded and chat is enabled) -->
<ArticleChatButton v-if="showChatButton && !isChatPanelOpen" @click="isChatPanelOpen = true" />

Expand Down
8 changes: 6 additions & 2 deletions frontend/src/components/article/ArticleItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { PhEyeSlash, PhStar, PhClockCountdown } from '@phosphor-icons/vue';
import type { Article } from '@/types/models';
import { formatDate as formatDateUtil } from '@/utils/date';
import { formatDate as formatDateUtil, formatExactDateTime } from '@/utils/date';
import { getProxiedMediaUrl, isMediaCacheEnabled } from '@/utils/mediaProxy';
import { useShowPreviewImages } from '@/composables/ui/useShowPreviewImages';
import { useAppStore } from '@/stores/app';
Expand Down Expand Up @@ -381,7 +381,11 @@ onUnmounted(() => {
alt="RSSHub"
/>
</template>
<span class="whitespace-nowrap">{{ formatDateWithI18n(article.published_at) }}</span>
<span
class="whitespace-nowrap"
:title="formatExactDateTime(article.published_at, locale)"
>{{ formatDateWithI18n(article.published_at) }}</span
>
</div>
</div>
</div>
Expand Down
Loading