Skip to content

Commit 3fdcb75

Browse files
committed
Improve sharing user experience
1 parent 3a11c63 commit 3fdcb75

10 files changed

Lines changed: 398 additions & 22 deletions

File tree

.github/workflows/release.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ jobs:
157157
"$updater_asset" \
158158
"$updater_asset.sig" \
159159
"release-assets/${{ matrix.latest_name }}" \
160-
"解决下载速度问题"
160+
"提升用户体验"
161161
162162
- name: Prepare Windows updater metadata
163163
if: runner.os == 'Windows'
@@ -172,7 +172,7 @@ jobs:
172172
"$updater_asset" \
173173
"$updater_asset.sig" \
174174
"release-assets/${{ matrix.latest_name }}" \
175-
"解决下载速度问题"
175+
"提升用户体验"
176176
177177
- name: Upload workflow artifact
178178
uses: actions/upload-artifact@v4
@@ -187,5 +187,5 @@ jobs:
187187
with:
188188
files: ${{ matrix.release_artifact_path || matrix.artifact_path }}
189189
body: |
190-
解决下载速度问题
190+
提升用户体验
191191
prerelease: ${{ contains(github.ref_name, 'rc') }}

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fileshare",
3-
"version": "1.0.4",
3+
"version": "1.0.5",
44
"private": true,
55
"type": "module",
66
"scripts": {

public/app.js

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
const state = {
33
role: 'client',
44
items: [],
5+
downloadStats: {},
56
apiBase: '',
67
events: null,
8+
downloadEvents: null,
79
isTauri: false,
810
serverRunning: false,
911
shareInfo: null,
@@ -24,6 +26,11 @@
2426
return `${(size / 1024 / 1024).toFixed(1)} MB`;
2527
}
2628

29+
function formatSpeed(size) {
30+
if (!Number.isFinite(size) || size <= 0) return '0 B/s';
31+
return `${formatSize(size)}/s`;
32+
}
33+
2734
function formatTime(value) {
2835
return new Date(value).toLocaleString('zh-CN', { hour12: false });
2936
}
@@ -64,24 +71,33 @@
6471

6572
root.innerHTML = state.items.map((item) => {
6673
const isText = item.kind === 'text';
74+
const isMissing = item.kind !== 'text' && item.exists === false;
6775
const badge = isText ? '文本' : '文件';
6876
const title = isText
6977
? escapeHtml(previewText(item.content || item.title, 40))
7078
: escapeHtml(item.title);
7179
const titleAction = isText
7280
? ''
7381
: `<button class="inline-icon-button" data-action="copy-link" data-id="${item.id}" aria-label="复制文件链接" title="复制文件链接">🔗</button>`;
82+
const downloadStat = state.role === 'admin' && !isText ? state.downloadStats[item.id] : null;
83+
const downloadStatus = downloadStat
84+
? `<span class="download-status">⬇ ${formatSpeed(downloadStat.speedBps)}</span>`
85+
: '';
7486
const description = isText
7587
? `<div class="meta">${formatTextLength(item.content || item.title)} · ${formatTime(item.createdAt)}</div>`
76-
: `<div class="meta">${formatSize(item.size)} · ${formatTime(item.createdAt)}</div>`;
88+
: `<div class="meta">${formatSize(item.size)} · ${formatTime(item.createdAt)}${downloadStatus}</div>`;
7789
const primaryAction = isText
7890
? `<button class="secondary" data-action="copy" data-id="${item.id}">复制</button>`
79-
: `<button class="secondary" data-action="download" data-id="${item.id}">下载</button>`;
91+
: (isMissing
92+
? `<button class="secondary" disabled type="button">已失效</button>`
93+
: (state.role === 'admin'
94+
? `<button class="secondary" data-action="reveal" data-id="${item.id}">查看</button>`
95+
: `<button class="secondary" data-action="download" data-id="${item.id}">下载</button>`));
8096
const actions = state.role === 'admin'
81-
? `${primaryAction}<button class="danger" data-action="delete" data-id="${item.id}">删除</button>`
97+
? `${primaryAction}<button class="secondary remove-action" data-action="delete" data-id="${item.id}">移除</button>`
8298
: primaryAction;
8399
return `
84-
<article class="item">
100+
<article class="item${isMissing ? ' item-missing' : ''}">
85101
<div class="item-main">
86102
<div class="item-title">
87103
<span class="badge">${badge}</span>
@@ -189,10 +205,15 @@
189205
state.serverRunning = false;
190206
state.apiBase = '';
191207
state.items = [];
208+
state.downloadStats = {};
192209
if (state.events) {
193210
state.events.close();
194211
state.events = null;
195212
}
213+
if (state.downloadEvents) {
214+
state.downloadEvents.close();
215+
state.downloadEvents = null;
216+
}
196217
const root = $('items');
197218
if (root) root.innerHTML = '';
198219
document.body.classList.add('server-stopped');
@@ -311,6 +332,28 @@
311332
state.items = JSON.parse(event.data);
312333
render();
313334
};
335+
if (state.role === 'admin') {
336+
connectDownloadEvents();
337+
}
338+
}
339+
340+
function connectDownloadEvents() {
341+
if (state.downloadEvents) state.downloadEvents.close();
342+
const events = new EventSource(`${state.apiBase}/api/download-events`);
343+
state.downloadEvents = events;
344+
events.onerror = () => {
345+
state.downloadStats = {};
346+
render();
347+
};
348+
events.onmessage = (event) => {
349+
const stats = JSON.parse(event.data);
350+
state.downloadStats = Object.fromEntries(
351+
stats
352+
.filter((item) => item.activeCount > 0)
353+
.map((item) => [item.itemId, item])
354+
);
355+
render();
356+
};
314357
}
315358

316359
async function addAdminLocalFiles(paths) {
@@ -420,6 +463,20 @@
420463
}, 1600);
421464
}
422465

466+
function showStatusMessage(message) {
467+
const status = $('status');
468+
if (status) {
469+
status.textContent = message;
470+
status.classList.add('status-error');
471+
window.setTimeout(() => {
472+
status.classList.remove('status-error');
473+
if (state.serverRunning) {
474+
status.textContent = '实时同步';
475+
}
476+
}, 1800);
477+
}
478+
}
479+
423480
async function bindAdminFileDrop() {
424481
if (state.role !== 'admin' || state.adminDragDropBound) return;
425482

@@ -664,10 +721,28 @@
664721
$('items').addEventListener('click', async (event) => {
665722
const button = event.target.closest('button[data-action]');
666723
if (!button) return;
724+
if (button.disabled) return;
667725
const { action, id } = button.dataset;
726+
if (action === 'reveal' && state.role === 'admin') {
727+
const item = state.items.find((entry) => entry.id === id);
728+
if (!item) return;
729+
if (state.isTauri) {
730+
try {
731+
await window.__TAURI__.core.invoke('reveal_admin_file', { id });
732+
} catch (error) {
733+
const message = error?.message || String(error) || '无法打开文件位置';
734+
showStatusMessage(`打开失败:${message}`);
735+
window.alert(`打开失败:${message}`);
736+
}
737+
return;
738+
}
739+
window.open(getDownloadUrl(item), '_blank');
740+
}
668741
if (action === 'download') {
669-
if (state.role === 'admin' && state.isTauri) {
670-
await window.__TAURI__.core.invoke('download_admin_file', { id });
742+
const item = state.items.find((entry) => entry.id === id);
743+
if (!item) return;
744+
if (item.exists === false) {
745+
alert('源文件已不存在');
671746
return;
672747
}
673748
window.location.href = `${state.apiBase}/api/items/${id}/download`;

public/styles.css

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,11 @@ button.danger:hover {
704704
font-size: 14px;
705705
}
706706

707+
#status.status-error {
708+
color: var(--danger);
709+
font-weight: 600;
710+
}
711+
707712
.items {
708713
display: grid;
709714
gap: 8px;
@@ -726,6 +731,15 @@ button.danger:hover {
726731
box-shadow: 0 4px 14px color-mix(in srgb, var(--accent) 12%, transparent);
727732
}
728733

734+
.item-missing {
735+
opacity: 0.62;
736+
}
737+
738+
.item-missing .item-title strong,
739+
.item-missing .meta {
740+
text-decoration: line-through;
741+
}
742+
729743
.item-title {
730744
display: flex;
731745
align-items: center;
@@ -785,6 +799,36 @@ button.danger:hover {
785799
line-height: 1.25;
786800
}
787801

802+
.download-status {
803+
display: inline;
804+
align-items: center;
805+
margin-left: 8px;
806+
padding: 1px 5px;
807+
border: 1px solid color-mix(in srgb, var(--success-soft-border) 70%, transparent);
808+
border-radius: 999px;
809+
background: color-mix(in srgb, var(--surface-success) 72%, transparent);
810+
color: var(--success);
811+
font-size: 10px;
812+
line-height: 1;
813+
}
814+
815+
button.secondary:disabled {
816+
opacity: 0.75;
817+
cursor: not-allowed;
818+
}
819+
820+
button.remove-action {
821+
border-color: var(--line);
822+
background: var(--surface-soft);
823+
color: color-mix(in srgb, var(--text-soft) 70%, transparent);
824+
}
825+
826+
button.remove-action:hover {
827+
border-color: var(--line);
828+
background: var(--surface-soft);
829+
color: var(--text-soft);
830+
}
831+
788832
pre {
789833
max-height: 140px;
790834
overflow: auto;

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "fileshare"
3-
version = "1.0.4"
3+
version = "1.0.5"
44
description = "LAN file sharing desktop app"
55
authors = ["FileShare"]
66
edition = "2021"

src-tauri/src/main.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,50 @@ async fn download_admin_file(id: String) -> Result<(), String> {
7575
server::copy_item_to_path(&id, &target_path).await
7676
}
7777

78+
#[tauri::command]
79+
async fn reveal_admin_file(id: String) -> Result<(), String> {
80+
let path = server::item_file_path(&id).await?;
81+
reveal_file(&path)
82+
}
83+
84+
fn reveal_file(path: &PathBuf) -> Result<(), String> {
85+
if !path.exists() {
86+
return Err("源文件已不存在".to_string());
87+
}
88+
89+
#[cfg(target_os = "macos")]
90+
{
91+
std::process::Command::new("open")
92+
.arg("-R")
93+
.arg(path)
94+
.status()
95+
.map_err(|error| error.to_string())?;
96+
return Ok(());
97+
}
98+
99+
#[cfg(target_os = "windows")]
100+
{
101+
std::process::Command::new("explorer")
102+
.arg(format!("/select,{}", path.display()))
103+
.status()
104+
.map_err(|error| error.to_string())?;
105+
return Ok(());
106+
}
107+
108+
#[cfg(target_os = "linux")]
109+
{
110+
let directory = path.parent().unwrap_or(path.as_path());
111+
std::process::Command::new("xdg-open")
112+
.arg(directory)
113+
.status()
114+
.map_err(|error| error.to_string())?;
115+
return Ok(());
116+
}
117+
118+
#[allow(unreachable_code)]
119+
Err("当前系统不支持打开文件位置".to_string())
120+
}
121+
78122
#[tauri::command]
79123
async fn start_server(
80124
port: u16,
@@ -132,6 +176,7 @@ fn main() {
132176
.invoke_handler(tauri::generate_handler![
133177
pick_admin_files,
134178
download_admin_file,
179+
reveal_admin_file,
135180
start_server,
136181
stop_server,
137182
server_status,

0 commit comments

Comments
 (0)