Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 19 additions & 1 deletion frontend/cypress/e2e/auto-update.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,17 @@ describe('Auto Update Feature', () => {
delay: 2000,
}).as('downloadUpdate');

cy.intercept('GET', '/api/download-update/progress*', {
statusCode: 200,
body: {
state: 'downloading',
bytes_written: 5242880,
total_bytes: 10485760,
percentage: 50,
indeterminate: false,
},
}).as('downloadProgress');

cy.visit('/');
cy.wait('@getFeeds');

Expand All @@ -389,6 +400,11 @@ describe('Auto Update Feature', () => {

// Verify progress bar exists
cy.get('[data-testid="update-download-progress"]').should('be.visible');
cy.wait('@downloadProgress');
cy.contains('50%').should('be.visible');
cy.wait('@downloadUpdate')
.its('request.body.request_id')
.should('match', /^[A-Za-z0-9_-]+$/);
});

it('should show installing message after download completes', () => {
Expand Down Expand Up @@ -464,7 +480,7 @@ describe('Auto Update Feature', () => {
// Mock download failure
cy.intercept('POST', '/api/download-update', {
statusCode: 500,
body: { error: 'Download failed' },
body: { success: false, error_code: 'download_network_error' },
}).as('downloadUpdate');

cy.visit('/');
Expand All @@ -482,6 +498,8 @@ describe('Auto Update Feature', () => {

// Verify error message is shown (via toast notification)
cy.get('.toast-container').should('exist');
cy.contains(/release page|发布页/i).should('be.visible');
cy.contains(/network|网络/i).should('be.visible');
});

it('should handle update check failure gracefully', () => {
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ const {
downloadingUpdate,
installingUpdate,
downloadProgress,
downloadProgressKnown,
downloadBytesWritten,
downloadTotalBytes,
downloadErrorCode,
} = useAppUpdates();

// Update dialog state
Expand Down Expand Up @@ -365,6 +369,10 @@ function onFeedUpdated(): void {
:downloading-update="downloadingUpdate"
:installing-update="installingUpdate"
:download-progress="downloadProgress"
:download-progress-known="downloadProgressKnown"
:download-bytes-written="downloadBytesWritten"
:download-total-bytes="downloadTotalBytes"
:download-error-code="downloadErrorCode"
@close="showUpdateDialog = false"
@update="downloadAndInstallUpdate"
/>
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/components/modals/SettingsModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ const {
downloadingUpdate,
installingUpdate,
downloadProgress,
downloadProgressKnown,
downloadBytesWritten,
downloadTotalBytes,
downloadErrorCode,
checkForUpdates: handleCheckUpdates,
downloadAndInstallUpdate: handleDownloadInstallUpdate,
} = useAppUpdates();
Expand Down Expand Up @@ -270,6 +274,10 @@ function handleDiscoverAll() {
:downloading-update="downloadingUpdate"
:installing-update="installingUpdate"
:download-progress="downloadProgress"
:download-progress-known="downloadProgressKnown"
:download-bytes-written="downloadBytesWritten"
:download-total-bytes="downloadTotalBytes"
:download-error-code="downloadErrorCode"
@check-updates="handleCheckUpdates"
@download-install-update="handleDownloadInstallUpdate"
/>
Expand Down
33 changes: 33 additions & 0 deletions frontend/src/components/modals/settings/about/AboutTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ interface Props {
downloadingUpdate?: boolean;
installingUpdate?: boolean;
downloadProgress?: number;
downloadProgressKnown?: boolean;
downloadBytesWritten?: number;
downloadTotalBytes?: number;
downloadErrorCode?: string;
}

withDefaults(defineProps<Props>(), {
Expand All @@ -36,6 +40,10 @@ withDefaults(defineProps<Props>(), {
downloadingUpdate: false,
installingUpdate: false,
downloadProgress: 0,
downloadProgressKnown: false,
downloadBytesWritten: 0,
downloadTotalBytes: 0,
downloadErrorCode: '',
});

const emit = defineEmits<{
Expand Down Expand Up @@ -80,6 +88,11 @@ function openGitHubRepo() {
function openGitHubRelease() {
openInBrowser('https://github.com/DevXDojo/MrRSS/releases/latest');
}

function formatBytes(bytes: number): string {
if (bytes <= 0) return '0 MB';
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
</script>

<template>
Expand Down Expand Up @@ -153,6 +166,26 @@ function openGitHubRelease() {
<span v-else-if="installingUpdate">{{ t('setting.update.installingUpdate') }}</span>
<span v-else>{{ t('modal.update.downloadUpdate') }}</span>
</button>
<div v-if="downloadingUpdate" class="mt-2 text-xs text-text-secondary">
<span v-if="downloadProgressKnown">
{{ Math.round(downloadProgress) }}% · {{ formatBytes(downloadBytesWritten) }} /
{{ formatBytes(downloadTotalBytes) }}
</span>
<span v-else>{{ formatBytes(downloadBytesWritten) }}</span>
</div>
<div
v-if="downloadErrorCode"
class="mt-2 rounded-md border border-red-500/30 bg-red-500/10 p-2 text-xs text-text-secondary"
>
<p>{{ t('setting.update.downloadFailedHelp') }}</p>
<button
type="button"
class="mt-1 text-accent hover:underline"
@click="openGitHubRelease"
>
{{ t('setting.update.downloadManually') }}
</button>
</div>
</div>

<!-- Fallback to GitHub if no download URL -->
Expand Down
75 changes: 73 additions & 2 deletions frontend/src/components/modals/update/UpdateAvailableDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n';
import { PhArrowCircleUp, PhDownloadSimple, PhCircleNotch, PhGear } from '@phosphor-icons/vue';
import BaseModal from '@/components/common/BaseModal.vue';
import ModalFooter from '@/components/common/ModalFooter.vue';
import { openInBrowser } from '@/utils/browser';

interface UpdateInfo {
has_update: boolean;
Expand All @@ -18,12 +19,20 @@ interface Props {
downloadingUpdate?: boolean;
installingUpdate?: boolean;
downloadProgress?: number;
downloadProgressKnown?: boolean;
downloadBytesWritten?: number;
downloadTotalBytes?: number;
downloadErrorCode?: string;
}

const props = withDefaults(defineProps<Props>(), {
downloadingUpdate: false,
installingUpdate: false,
downloadProgress: 0,
downloadProgressKnown: false,
downloadBytesWritten: 0,
downloadTotalBytes: 0,
downloadErrorCode: '',
});

const emit = defineEmits<{
Expand Down Expand Up @@ -56,6 +65,37 @@ const normalizedDownloadProgress = computed(() =>
Math.min(100, Math.max(0, props.downloadProgress))
);

function formatBytes(bytes: number): string {
if (bytes <= 0) return '0 MB';
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}

const downloadSizeLabel = computed(() => {
if (props.downloadProgressKnown && props.downloadTotalBytes > 0) {
return `${formatBytes(props.downloadBytesWritten)} / ${formatBytes(props.downloadTotalBytes)}`;
}
return formatBytes(props.downloadBytesWritten);
});

const downloadErrorMessage = computed(() => {
switch (props.downloadErrorCode) {
case 'download_proxy_error':
return t('setting.update.downloadProxyError');
case 'download_timeout':
return t('setting.update.downloadTimeout');
case 'download_network_error':
return t('setting.update.downloadNetworkError');
case 'download_server_error':
return t('setting.update.downloadServerError');
default:
return t('setting.update.downloadFailedHelp');
}
});

function openReleasePage() {
openInBrowser('https://github.com/DevXDojo/MrRSS/releases/latest');
}

function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Enter') {
e.preventDefault();
Expand Down Expand Up @@ -113,15 +153,33 @@ onUnmounted(() => {
<div v-if="props.downloadingUpdate" class="mt-4" data-testid="update-download-progress">
<div class="mb-1 flex items-center justify-between text-xs text-text-secondary">
<span>{{ t('common.action.downloading') }}</span>
<span>{{ Math.round(normalizedDownloadProgress) }}%</span>
<span>
{{
props.downloadProgressKnown
? `${Math.round(normalizedDownloadProgress)}%`
: downloadSizeLabel
}}
</span>
</div>
<progress
class="download-progress block h-2 w-full overflow-hidden rounded-full"
:value="normalizedDownloadProgress"
:value="props.downloadProgressKnown ? normalizedDownloadProgress : undefined"
max="100"
></progress>
<div v-if="props.downloadProgressKnown" class="mt-1 text-right text-xs text-text-secondary">
{{ downloadSizeLabel }}
</div>
</div>

<div
v-if="props.downloadErrorCode"
class="mt-4 rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-xs text-text-secondary"
>
<p>{{ downloadErrorMessage }}</p>
<button type="button" class="mt-2 text-accent hover:underline" @click="openReleasePage">
{{ t('setting.update.downloadManually') }}
</button>
</div>
<p v-if="!updateInfo.download_url" class="text-text-secondary text-xs mt-4">
{{ t('setting.update.noInstallerAvailable') }}
<a
Expand Down Expand Up @@ -185,6 +243,10 @@ onUnmounted(() => {
background: var(--accent-color);
}

.download-progress:indeterminate {
animation: progress-pulse 1.2s ease-in-out infinite;
}

.animate-spin {
animation: spin 1s linear infinite;
}
Expand All @@ -196,4 +258,13 @@ onUnmounted(() => {
transform: rotate(360deg);
}
}
@keyframes progress-pulse {
0%,
100% {
opacity: 0.45;
}
50% {
opacity: 1;
}
}
</style>
Loading