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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

The version number is defined in `frontend/src/enum/version.ts` and must stay in sync with `const Version` in `backend-go/config/config.go`.

## 1.5.2

### UI

- **Folder view**: The list or grid view is saved per folder and inherited by its subfolders, instead of one global view for every folder (frontend).

### Fixes

- **Folder state**: The sort mode is saved per folder and inherited by subfolders (frontend).

## 1.5.1

### UI
Expand Down
42 changes: 32 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,39 @@

![File Lite](docs/screenshots/00-main.webp)

## Screenshots

| Screenshot | Feature |
| --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ![Tabs and split view](docs/screenshots/01-tabs-split.webp) | **Tabs & split view**: Chrome-like tabs with a Total Commander-style multi-pane view, horizontal and vertical, where files can be dragged straight from one pane into the other. |
| ![Transfers and background tasks](docs/screenshots/02-transfers-tasks.webp) | **Transfers & background tasks**: Uploads and downloads, while copy, move and delete run on the server — with progress and cancel, gathered in the top-right panel. |
| ![Endless Gallery](docs/screenshots/03-gallery.webp) | **Endless Gallery**: A short-video-style vertical feed of the images, videos and audio in the current folder, with touch, wheel and keyboard navigation, and a collect list. |
| ![Music player](docs/screenshots/04-music-player.webp) | **Music player**: Playlist, embedded cover art and synced lyrics. |
| ![Video player](docs/screenshots/05-video-player.webp) | **Video player**: ArtPlayer or the native `<video>` element, switchable from the app menu, remembering the last opened file. |
| ![Previews and thumbnails](docs/screenshots/06-thumbnails.webp) | **Previews & thumbnails**: Image previews, video first frames (ffmpeg) and audio cover art are all generated by the server and cached in the browser. |
| ![Text editor](docs/screenshots/07-text-editor.webp) | **Text editor**: Edit and save text files right in the browser. |
| ![Properties window](docs/screenshots/08-properties.webp) | **Windows-style properties**: Built-in apps support multi-window interaction and can be minimised. |
**Tabs & split view**: Chrome-like tabs with a Total Commander-style multi-pane view, horizontal and vertical, where files can be dragged straight from one pane into the other.

![Tabs and split view](docs/screenshots/01-tabs-split.webp)

**Transfers & background tasks**: Uploads and downloads, while copy, move and delete run on the server — with progress and cancel, gathered in the top-right panel.

![Transfers and background tasks](docs/screenshots/02-transfers-tasks.webp)

**Endless Gallery**: A short-video-style vertical feed of the images, videos and audio in the current folder, with touch, wheel and keyboard navigation, and a collect list.

![Endless Gallery](docs/screenshots/03-gallery.webp)

**Music player**: Playlist, embedded cover art and synced lyrics.

![Music player](docs/screenshots/04-music-player.webp)

**Video player**: ArtPlayer or the native `<video>` element, switchable from the app menu, remembering the last opened file.

![Video player](docs/screenshots/05-video-player.webp)

**Previews & thumbnails**: Image previews, video first frames (ffmpeg) and audio cover art are all generated by the server and cached in the browser.

![Previews and thumbnails](docs/screenshots/06-thumbnails.webp)

**Text editor**: Edit and save text files right in the browser.

![Text editor](docs/screenshots/07-text-editor.webp)

**Multiple windows**: Built-in apps can open several windows at once, and those windows can be stacked or minimised.

![Multiple windows](docs/screenshots/08-properties.webp)


- **Backend**: a single Go (Echo) server, shipped as one static binary with the UI embedded
Expand Down
2 changes: 1 addition & 1 deletion backend-go/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ type Cfg struct {
}

const PkgName = "file-lite-go"
const Version = "1.5.1"
const Version = "1.5.2"

var cfg Cfg
var dataBaseDir string
Expand Down
Binary file modified docs/screenshots/08-properties.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 33 additions & 10 deletions docs/zh-CN/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,39 @@

![File Lite](../screenshots/00-main.webp)

| 截图 | 功能 |
| :-: | :-- |
| ![标签页与分栏](../screenshots/01-tabs-split.webp) | **标签页与分栏**:类似 Chrome 的标签页,加上 Total Commander 式的多栏视图,横竖都可以,文件能从一个栏直接拖到另一个栏。 |
| ![传输与后台任务](../screenshots/02-transfers-tasks.webp) | **传输与后台任务**:上传和下载;复制、移动、删除在服务端进行,带进度和取消,集中在右上角面板。 |
| ![Endless Gallery](../screenshots/03-gallery.webp) | **Endless Gallery**:把当前文件夹里的图片、视频和音频排成短视频式的竖向信息流,支持触摸、滚轮和键盘,并有收藏列表。 |
| ![音乐播放器](../screenshots/04-music-player.webp) | **音乐播放器**:播放列表、内嵌封面和同步歌词。 |
| ![视频播放器](../screenshots/05-video-player.webp) | **视频播放器**:ArtPlayer 或原生 `<video>`,可在应用菜单里切换,并记住上次打开的文件。 |
| ![预览与缩略图](../screenshots/06-thumbnails.webp) | **预览与缩略图**:图片预览、视频首帧(ffmpeg)和音频封面都由服务端生成,并缓存在浏览器里。 |
| ![文本编辑器](../screenshots/07-text-editor.webp) | **文本编辑器**:在浏览器里直接编辑并保存文本文件。 |
| ![属性窗口](../screenshots/08-properties.webp) | **Windows 风格的属性**:内置应用支持多窗口,也可以最小化。 |
## 截图

**标签页与分栏**:类似 Chrome 的标签页,加上 Total Commander 式的多栏视图,横竖都可以,文件能从一个栏直接拖到另一个栏。

![标签页与分栏](../screenshots/01-tabs-split.webp)

**传输与后台任务**:上传和下载;复制、移动、删除在服务端进行,带进度和取消,集中在右上角面板。

![传输与后台任务](../screenshots/02-transfers-tasks.webp)

**Endless Gallery**:把当前文件夹里的图片、视频和音频排成短视频式的竖向信息流,支持触摸、滚轮和键盘,并有收藏列表。

![Endless Gallery](../screenshots/03-gallery.webp)

**音乐播放器**:播放列表、内嵌封面和同步歌词。

![音乐播放器](../screenshots/04-music-player.webp)

**视频播放器**:ArtPlayer 或原生 `<video>`,可在应用菜单里切换,并记住上次打开的文件。

![视频播放器](../screenshots/05-video-player.webp)

**预览与缩略图**:图片预览、视频首帧(ffmpeg)和音频封面都由服务端生成,并缓存在浏览器里。

![预览与缩略图](../screenshots/06-thumbnails.webp)

**文本编辑器**:在浏览器里直接编辑并保存文本文件。

![文本编辑器](../screenshots/07-text-editor.webp)

**多窗口**:内置应用可以同时打开多个窗口,能叠放,也能最小化。

![多窗口](../screenshots/08-properties.webp)

- **后端**:单个 Go (Echo) 服务,界面内嵌,打成一个静态二进制
- **体积**:大约 **10MB**
Expand Down
4 changes: 2 additions & 2 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,13 @@ upload/ a.txt(与服务端同名)、fresh.txt
`.gitignore`,这样新克隆出来的仓库里**有一个空的 `screenshots/`**,
测试直接往里写即可,也不会留下未跟踪的空目录。

> 与 README 功能表格用的截图要分清:那些是人工挑选的展示图,由
> 与 README 展示用的截图要分清:那些是人工挑选的展示图,由
> `bun run docs:screenshots` 生成到 `../docs/screenshots/`,**仍然提交**(只提交
> `.webp`;同目录的 `.png` 是压缩前的中间产物,见下)。

## README 截图

README 功能表格里的截图由另一个脚本产出,输出到 `../docs/screenshots/`:
README 里的功能截图由另一个脚本产出,输出到 `../docs/screenshots/`:

```sh
cd e2e
Expand Down
16 changes: 11 additions & 5 deletions e2e/scripts/capture-docs.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// 生成 README 里那张功能表格用的截图:起一台演示服务器,把上面列出的功能逐个截下来,
// 生成 README 展示用的功能截图:起一台演示服务器,把下面列出的功能逐个截下来,
// 输出到 docs/screenshots/(webp,宽度 1280)。00-main.webp 是手工精修的主图,这里不碰。
//
// cd e2e && bun run docs:screenshots # 首次会下载演示素材,之后复用缓存
Expand Down Expand Up @@ -234,7 +234,13 @@ async function placeWindow(window, x, y) {
}, { x, y })
}

/** 08 属性窗口:每个选中项各开一扇小窗,可同时摆开。 */
/**
* 08 属性窗口:每个选中项各开一扇小窗,可同时摆开。
*
* 三扇窗排成一条向下的阶梯:起始 y 落在整个文件列表下方,标签栏、地址栏、
* 工具栏和文件列表都露在窗口上方,不会“挡住背后的 UI”;每扇窗只压住前一扇的
* 右下角,标题栏依次可见,前后层叠关系一眼能看出来。
*/
async function shotProperties(page) {
await goToRoot(page)
const names = ['Pictures', 'Documents', 'Music']
Expand All @@ -257,9 +263,9 @@ async function shotProperties(page) {
const windows = page.locator('.app-window')
await expect(windows).toHaveCount(3)
const placements = [
[28, 56],
[490, 150],
[940, 250],
[130, 370],
[480, 445],
[840, 520],
]
for (const [index, win] of (await windows.all()).entries()) {
await expect(win.locator('.properties-row', { hasText: 'Contains' }).locator('.properties-value'))
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/enum/version.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const PKG_NAME = 'file-lite'
// also update config.go: const Version = "
// and CHANGELOG.md
export const VERSION = '1.5.1'
export const VERSION = '1.5.2'
2 changes: 1 addition & 1 deletion frontend/src/i18n/locales/en-US/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
"filter_by_key_scope_or_descripti": "Filter by key, scope or description",
"filter_name": "Filter name",
"folder": "Folder",
"folder_state": "Folder state (scroll position & sort mode)",
"folder_state": "Folder state",
"forward": "Forward",
"fullscreen": "Fullscreen",
"gigantic_4_gb": "Gigantic",
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/i18n/locales/ja-JP/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
"filter_by_key_scope_or_descripti": "キー、スコープ、または説明でフィルター",
"filter_name": "フィルター名",
"folder": "フォルダ",
"folder_state": "フォルダの状態(スクロール位置と並べ替えモード)",
"folder_state": "フォルダの状態",
"forward": "進む",
"fullscreen": "全画面表示",
"gigantic_4_gb": "巨大",
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/i18n/locales/zh-CN/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
"filter_by_key_scope_or_descripti": "按键、作用域或描述筛选",
"filter_name": "筛选名称",
"folder": "文件夹",
"folder_state": "文件夹状态(滚动位置和排序方式)",
"folder_state": "文件夹状态",
"forward": "前进",
"fullscreen": "全屏",
"gigantic_4_gb": "超大",
Expand Down
1 change: 0 additions & 1 deletion frontend/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ function createDefaultLocalSettingsStore() {
showHidden: true,
/** 排序后是否把文件夹排在文件前面 */
sortFoldersFirst: true,
isGridView: false,
iconSizeList: 16,
iconSizeGrid: 48,
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ async function onFileChange(event: Event) {
:disabled="busy"
@click="pickFile"
>
{{ busy ? 'Uploading…' : 'Upload' }}
{{ busy ? 'Uploading' : 'Upload...' }}
</button>
<input ref="fileInput" class="translucent-background__file" type="file" accept="image/*" @change="onFileChange">
</div>
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/views/FileManager/ExplorerPane.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const props = withDefaults(
active?: boolean
// 本面板是不是当前聚焦的面板:拆分项里两个面板只有一个是
focused?: boolean
// 面板自己的视图偏好(list/grid、图标大小);不传则用全局设置
// 面板自己的图标大小偏好;不传则用全局设置
view?: ExplorerPaneView
}>(),
{
Expand Down Expand Up @@ -95,7 +95,6 @@ function syncBranchViewProp(enabled: boolean) {
if (Boolean(props.view?.branch) === enabled)
return
const base = props.view ?? {
grid: localSettingsStore.value.isGridView,
iconSizeList: localSettingsStore.value.iconSizeList,
iconSizeGrid: localSettingsStore.value.iconSizeGrid,
}
Expand Down
37 changes: 14 additions & 23 deletions frontend/src/views/FileManager/ExplorerUI/FileList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { ExplorerEvents, useExplorerBusOn } from '../utils/bus'
import { GROUP_HEADER_HEIGHT, groupEntries } from '../utils/group'
import { composeSortMode, parseSortMode } from '../utils/sort'
import { acceptDirDrag, beginEntryDrag, dragSession, dropIntoDir, endEntryDrag, isExternalFileDrag, isInternalDrag, useDragEnabled } from './entry-drag'
import { explorerStateMap, groupDescRef, groupFieldRef, sortModeRef } from './explorer-state'
import { explorerStateMap, gridRef, groupDescRef, groupFieldRef, scrollPositionMap, sortModeRef } from './explorer-state'
import { createDefaultFileFilter, isFileFilterActive } from './file-filter'
import { getEntryTypeLabel } from './file-type'
import FileGridItem from './FileGridItem.vue'
Expand Down Expand Up @@ -58,7 +58,7 @@ const props = withDefaults(
* 否则后台面板一加载完就会把活动面板抢走。
*/
focused?: boolean
/** 面板自己的视图偏好(list/grid、图标大小);不传则用全局设置 */
/** 面板自己的图标大小偏好;不传则用全局设置 */
view?: ExplorerPaneView
/**
* 上一次列目录失败的原因;非空时列表区显示错误空状态而不是「This folder is empty」。
Expand Down Expand Up @@ -101,14 +101,15 @@ function clearCollapsedGroups() {
}

/**
* 视图偏好(list/grid、图标大小)属于**面板**:拆分视图里两个面板各看各的,
* 图标大小偏好属于**面板**:拆分视图里两个面板各看各的,
* 改一个不会带着另一个一起变。面板还没设置过时回落到全局设置。
*
* 状态由外壳持有(挂在标签 store 的面板上,跟着标签一起持久化),这里只按值收发:
* 外壳没有接线(选择器窗口)时也不会丢,它会把这一份写回全局设置。
*
* 列表/图标视图(grid)不在面板上:它按目录持久化并沿祖先继承(见 `gridRef`)。
*/
const paneView = computed<ExplorerPaneView>(() => props.view ?? {
grid: localSettingsStore.value.isGridView,
iconSizeList: localSettingsStore.value.iconSizeList,
iconSizeGrid: localSettingsStore.value.iconSizeGrid,
})
Expand All @@ -117,10 +118,8 @@ function updatePaneView(patch: Partial<ExplorerPaneView>) {
emit('update:view', { ...paneView.value, ...patch })
}

const isGridView = computed({
get: () => paneView.value.grid,
set: (val: boolean) => updatePaneView({ grid: val }),
})
/** 列表/图标视图:目录自己的设置 > 祖先继承 > 默认列表。 */
const isGridView = gridRef(basePath)
const isBranchView = computed({
get: () => Boolean(paneView.value.branch) && !selectFileMode.value,
set: (val: boolean) => {
Expand Down Expand Up @@ -1588,17 +1587,16 @@ useShortcut({
})

// 缓存滚动位置
function getSetScrollPosition(action: 'get' | 'set', value = 0) {
function getSetScrollPosition(action: 'get' | 'set', value = 0): number {
const el = explorerContentRef.value
if (!el) {
return 0
}
if (action === 'get') {
return el.scrollTop
}
else if (action === 'set') {
el.scrollTop = value
}
el.scrollTop = value
return value
}

function scrollToFile(name: string) {
Expand Down Expand Up @@ -1668,10 +1666,10 @@ function selectAndReveal(name: string) {
* 恢复本目录上次的滚动位置。
*
* 保活的标签被重新显示时调用:`display:none` 期间容器尺寸为 0,虚拟列表的可视区是空的,
* 需要重新量一次并滚回去(位置按 path 存在 explorerStateMap 里)。
* 需要重新量一次并滚回去(位置按 path 存在内存里的 scrollPositionMap 中,刷新即失效)。
*/
function restoreViewport() {
const position = explorerStateMap.value[basePath.value]?.position || 0
const position = scrollPositionMap[basePath.value] ?? 0
nextTick(() => {
virtualList.refresh()
virtualGrid.refresh()
Expand All @@ -1686,20 +1684,13 @@ function restoreViewport() {
}

watchDebounced(files, () => {
if (explorerStateMap.value[basePath.value]) {
if (scrollPositionMap[basePath.value] !== undefined) {
restoreViewport()
}
}, { debounce: 100, maxWait: 1000 })
const debounceHandleScroll = useDebounceFn(() => {
const position = getSetScrollPosition('get')
if (!explorerStateMap.value[basePath.value]) {
explorerStateMap.value[basePath.value] = { position }
}
else {
explorerStateMap.value[basePath.value].position = position
}

// console.log('save', basePath.value, position)
scrollPositionMap[basePath.value] = position
}, 500)
useEventListener(() => explorerContentRef.value, 'scroll', debounceHandleScroll)

Expand Down
19 changes: 19 additions & 0 deletions frontend/src/views/FileManager/ExplorerUI/explorer-state.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { SortField } from '../utils/sort'
import type { PathState } from './path-state'
import { useStorage } from '@vueuse/core'
import { reactive } from 'vue'
import { LsKeys } from '@/enum'
import { SortType } from '@/types/server'
import { normalizeListingPath } from '../utils'
Expand All @@ -20,6 +21,11 @@ export const explorerStateMap = useStorage<Record<string, PathState>>(
{ listenToStorageChanges: false },
)

/**
* 滚动位置只在当前页面内存里保留(键为 listing 路径),刷新页面即失效,不持久化。
*/
export const scrollPositionMap = reactive<Record<string, number>>({})

/** 只写某个目录**自己**的状态,不碰继承链 */
function writeOwnPathState<K extends keyof PathState>(path: string, key: K, val: NonNullable<PathState[K]>) {
const entry = normalizeListingPath(path)
Expand Down Expand Up @@ -48,6 +54,11 @@ export function getPathGroupDesc(path: string): boolean {
return resolveInheritedPathState(explorerStateMap.value, path, 'groupDesc', mountPaths()) ?? false
}

/** 有效列表/图标视图:目录自己的手动设置 > 最近一个手动设置过的祖先 > 默认列表。 */
export function getPathGrid(path: string): boolean {
return resolveInheritedPathState(explorerStateMap.value, path, 'grid', mountPaths()) ?? false
}

/** 排序 ref:读走继承解析,写落在当前目录自己的记录上 */
export function sortModeRef(path: Ref<string>) {
return computed({
Expand All @@ -71,3 +82,11 @@ export function groupDescRef(path: Ref<string>) {
set: (val: boolean) => writeOwnPathState(path.value, 'groupDesc', val),
})
}

/** 列表/图标视图 ref,读写语义同 `sortModeRef` */
export function gridRef(path: Ref<string>) {
return computed({
get: () => getPathGrid(path.value),
set: (val: boolean) => writeOwnPathState(path.value, 'grid', val),
})
}
Loading
Loading