Compare commits

...

16 Commits

Author SHA1 Message Date
徐涛
5c092cad17 fix(list):调整文件夹树布局。 2023-03-27 18:47:15 +08:00
徐涛
f38165d8d2 fix(app):修复自动设置窗口标题的功能。 2023-03-24 09:29:20 +08:00
徐涛
49a800fb4b build(version):更改发布版本号。 2023-03-23 16:02:03 +08:00
徐涛
45cbcaf95f enhance(list):文件夹列表与文件列表改用自然数字排序。 2023-03-23 16:01:10 +08:00
徐涛
8fab1a2c74 build(deps):增加正则表达式支持库。 2023-03-23 15:58:57 +08:00
徐涛
17e8bda099 build(version):更新版本号。 2023-03-23 10:01:24 +08:00
徐涛
ea19698036 fix(list):调整文件列表样式,使之可以列出文件列表的最后一项。 2023-03-23 09:58:52 +08:00
徐涛
0eb69d777d feat(file):增加修改文件名称的功能。 2023-03-23 09:52:26 +08:00
徐涛
65943e33b9 enhance(view):调整虚拟列表滚动设置,避免卡顿。 2023-03-23 08:47:03 +08:00
徐涛
6d4dd4de06 build(config):修改Tauri配置。 2023-03-22 14:41:48 +08:00
徐涛
a6f043e0d1 build(action):更新自动编译Action. 2023-03-22 14:30:01 +08:00
徐涛
50ba85afa3 build(action):测试精简的Actions. 2023-03-22 14:13:50 +08:00
徐涛
7331f42e56 build(version):更新版本号。 2023-03-22 14:06:00 +08:00
徐涛
36e0c7714c enhance(list):聚焦时可以自动展开被聚焦的文件夹。 2023-03-22 14:01:01 +08:00
徐涛
c63da5222e enhance(fs):优化文件夹扫描。 2023-03-22 09:25:21 +08:00
徐涛
32e797f35d enhance(debug):去掉一些不再使用的调试语句。 2023-03-22 08:40:30 +08:00
13 changed files with 225 additions and 177 deletions

View File

@@ -3,39 +3,7 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
create-release:
permissions:
contents: write
runs-on: ubuntu-20.04
outputs:
release_id: ${{ steps.create-release.outputs.result }}
steps:
- uses: actions/checkout@v3
- name: setup node
uses: actions/setup-node@v3
with:
node-version: 16
- name: get version
run: echo "PACKAGE_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
- name: create release
id: create-release
uses: actions/github-script@v6
with:
script: |
const { data } = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: `app-v${process.env.PACKAGE_VERSION}-alpha`,
name: `漫画阅读器 v${process.env.PACKAGE_VERSION}`,
body: '从附件中下载对应平台的安装包以及应用。',
draft: true,
prerelease: true
})
return data.id
build-tauri: build-tauri:
needs: create-release
permissions: permissions:
contents: write contents: write
strategy: strategy:
@@ -45,48 +13,44 @@ jobs:
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/checkout@v3 - name: Checkout repository
uses: actions/checkout@v3
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev
- name: setup node - name: setup node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 16
- name: install Rust stable
- name: install Rust nightly
uses: dtolnay/rust-toolchain@nightly uses: dtolnay/rust-toolchain@nightly
- name: install dependencies (ubuntu only) - name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-20.04' if: matrix.platform == 'ubuntu-20.04'
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: latest-7 version: latest-7
- name: install frontend dependencies - name: install frontend dependencies
run: pnpm install # change this to npm or pnpm depending on which one you use run: pnpm install # change this to npm or pnpm depending on which one you use
- uses: tauri-apps/tauri-action@v0
- name: build app
uses: tauri-apps/tauri-action@v0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
tauriScript: pnpm tauri tauriScript: pnpm tauri
releaseId: ${{ needs.create-release.outputs.release_id }} tagName: ${{ github.ref_name }} # This only works if your workflow triggers on new tags.
releaseName: '漫画阅读器 v__VERSION__' # tauri-action replaces \_\_VERSION\_\_ with the app version.
publish-release: releaseBody: '从附件中下载对应平台的安装包以及应用。'
permissions: releaseDraft: true
contents: write prerelease: false
runs-on: ubuntu-20.04
needs: [create-release, build-tauri]
steps:
- name: publish release
id: publish-release
uses: actions/github-script@v6
env:
release_id: ${{ needs.create-release.outputs.release_id }}
with:
script: |
github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: process.env.release_id,
draft: true,
prerelease: true
})

View File

@@ -5,39 +5,7 @@ on:
- 'v*' - 'v*'
jobs: jobs:
create-release:
permissions:
contents: write
runs-on: ubuntu-20.04
outputs:
release_id: ${{ steps.create-release.outputs.result }}
steps:
- uses: actions/checkout@v3
- name: setup node
uses: actions/setup-node@v3
with:
node-version: 16
- name: get version
run: echo "PACKAGE_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
- name: create release
id: create-release
uses: actions/github-script@v6
with:
script: |
const { data } = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: `app-v${process.env.PACKAGE_VERSION}`,
name: `漫画阅读器 v${process.env.PACKAGE_VERSION}`,
body: '从附件中下载对应平台的安装包以及应用。',
draft: true,
prerelease: false
})
return data.id
build-tauri: build-tauri:
needs: create-release
permissions: permissions:
contents: write contents: write
strategy: strategy:
@@ -47,48 +15,44 @@ jobs:
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/checkout@v3 - name: Checkout repository
uses: actions/checkout@v3
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev
- name: setup node - name: setup node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 16
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable - name: install Rust nightly
uses: dtolnay/rust-toolchain@nightly
- name: install dependencies (ubuntu only) - name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-20.04' if: matrix.platform == 'ubuntu-20.04'
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: latest-7 version: latest-7
- name: install frontend dependencies - name: install frontend dependencies
run: pnpm install # change this to npm or pnpm depending on which one you use run: pnpm install # change this to npm or pnpm depending on which one you use
- uses: tauri-apps/tauri-action@v0
- name: build app
uses: tauri-apps/tauri-action@v0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
tauriScript: pnpm tauri tauriScript: pnpm tauri
releaseId: ${{ needs.create-release.outputs.release_id }} tagName: ${{ github.ref_name }} # This only works if your workflow triggers on new tags.
releaseName: '漫画阅读器 v__VERSION__' # tauri-action replaces \_\_VERSION\_\_ with the app version.
publish-release: releaseBody: '从附件中下载对应平台的安装包以及应用。'
permissions: releaseDraft: true
contents: write prerelease: false
runs-on: ubuntu-20.04
needs: [create-release, build-tauri]
steps:
- name: publish release
id: publish-release
uses: actions/github-script@v6
env:
release_id: ${{ needs.create-release.outputs.release_id }}
with:
script: |
github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: process.env.release_id,
draft: false,
prerelease: false
})

View File

@@ -1,7 +1,7 @@
{ {
"name": "comic_viewer", "name": "comic_viewer",
"private": true, "private": true,
"version": "0.2.0", "version": "0.2.7",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

11
src-tauri/Cargo.lock generated
View File

@@ -326,7 +326,7 @@ dependencies = [
[[package]] [[package]]
name = "comic_viewer" name = "comic_viewer"
version = "0.2.0" version = "0.2.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -335,6 +335,7 @@ dependencies = [
"mime_guess", "mime_guess",
"mountpoints", "mountpoints",
"once_cell", "once_cell",
"regex",
"serde", "serde",
"serde_json", "serde_json",
"serde_repr", "serde_repr",
@@ -2285,9 +2286,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.7.1" version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" checksum = "cce168fea28d3e05f158bda4576cf0c844d5045bc2cc3620fa0292ed5bb5814c"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -2305,9 +2306,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.28" version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]] [[package]]
name = "rfd" name = "rfd"

View File

@@ -1,10 +1,10 @@
[package] [package]
name = "comic_viewer" name = "comic_viewer"
version = "0.2.0" version = "0.2.7"
description = "A Tauri App" description = "漫画、条漫简易阅读器"
authors = ["you"] authors = ["Khadgar"]
license = "" license = "MIT"
repository = "" repository = "https://github.com/vixalie/comic_viewer"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -28,6 +28,7 @@ mountpoints = "0.2.1"
md-5 = "0.10.5" md-5 = "0.10.5"
urlencoding = "2.1.2" urlencoding = "2.1.2"
mime_guess = "2.0.4" mime_guess = "2.0.4"
regex = "1.7.2"
[features] [features]
# this feature is used for production builds or when `devPath` points to the filesystem # this feature is used for production builds or when `devPath` points to the filesystem

View File

@@ -1,12 +1,24 @@
use std::{fs::DirEntry, path::Path}; use std::{
fs::{self, DirEntry},
path::{Path, PathBuf},
};
use mountpoints::mountinfos; use mountpoints::mountinfos;
use regex::Regex;
use serde::Serialize; use serde::Serialize;
use tauri::Runtime; use tauri::Runtime;
use crate::utils; use crate::utils;
#[derive(Debug, Clone, Serialize)] fn compute_all_digits(text: &str) -> usize {
let re = Regex::new(r#"\d+"#).unwrap();
re.find_iter(&["a", text, "0"].join(" "))
.map(|b| b.as_str())
.map(|b| usize::from_str_radix(b, 10).unwrap_or(0))
.sum::<usize>()
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct FileItem { pub struct FileItem {
pub id: String, pub id: String,
pub filename: String, pub filename: String,
@@ -15,7 +27,19 @@ pub struct FileItem {
pub width: u32, pub width: u32,
} }
#[derive(Debug, Clone, Serialize)] impl PartialOrd for FileItem {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
let this_digit = compute_all_digits(&self.filename);
let other_digit = compute_all_digits(&other.filename);
if this_digit == other_digit {
self.filename.partial_cmp(&other.filename)
} else {
this_digit.partial_cmp(&other_digit)
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct DirItem { pub struct DirItem {
pub id: String, pub id: String,
pub dirname: String, pub dirname: String,
@@ -23,6 +47,18 @@ pub struct DirItem {
pub root: bool, pub root: bool,
} }
impl PartialOrd for DirItem {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
let this_digit = compute_all_digits(&self.dirname);
let other_digit = compute_all_digits(&other.dirname);
if this_digit == other_digit {
self.dirname.partial_cmp(&other.dirname)
} else {
this_digit.partial_cmp(&other_digit)
}
}
}
fn is_hidden(entry: &DirEntry) -> bool { fn is_hidden(entry: &DirEntry) -> bool {
entry entry
.file_name() .file_name()
@@ -48,8 +84,12 @@ pub async fn scan_directory(target: String) -> Result<Vec<FileItem>, String> {
if !entry.path().is_file() { if !entry.path().is_file() {
continue; continue;
} }
let (width, height) = image::image_dimensions(entry.path()) let image_detect_result = image::image_dimensions(entry.path());
.map_err(|_e| format!("读取图片文件 {} 元信息失败。", entry.path().display()))?; if image_detect_result.is_err() {
println!("读取图片文件 {} 元信息失败。", entry.path().display());
continue;
}
let (width, height) = image_detect_result.unwrap();
let file_hash_id = entry let file_hash_id = entry
.path() .path()
.to_str() .to_str()
@@ -60,25 +100,32 @@ pub async fn scan_directory(target: String) -> Result<Vec<FileItem>, String> {
let filename = entry let filename = entry
.path() .path()
.file_name() .file_name()
.ok_or(String::from("不能获取到文件名。"))? .ok_or(String::from("不能获取到文件名。"))
.to_owned() .map(ToOwned::to_owned)
.into_string() .map(|s| s.into_string().unwrap());
.unwrap(); if filename.is_err() {
println!("不能获取到文件 {} 文件名。", entry.path().display());
continue;
}
let path = entry let path = entry
.path() .path()
.clone() .clone()
.to_str() .to_str()
.ok_or(String::from("不能获取到文件路径。"))? .ok_or(String::from("不能获取到文件路径。"))
.to_string(); .map(ToString::to_string);
if path.is_err() {
println!("不能获取到文件 {} 路径。", entry.path().display());
continue;
}
file_items.push(FileItem { file_items.push(FileItem {
id: file_hash_id, id: file_hash_id,
filename, filename: filename.unwrap(),
path, path: path.unwrap(),
height, height,
width, width,
}); });
} }
file_items.sort_by(|a, b| a.filename.partial_cmp(&b.filename).unwrap()); file_items.sort_by(|a, b| a.partial_cmp(&b).unwrap());
Ok(file_items) Ok(file_items)
} }
@@ -179,6 +226,20 @@ pub async fn scan_for_child_dirs<R: Runtime>(
root: false, root: false,
}); });
} }
child_dirs.sort_by(|a, b| a.dirname.partial_cmp(&b.dirname).unwrap()); child_dirs.sort_by(|a, b| a.partial_cmp(&b).unwrap());
Ok(child_dirs) Ok(child_dirs)
} }
#[tauri::command]
pub async fn rename_file<R: Runtime>(
_app: tauri::AppHandle<R>,
_window: tauri::Window<R>,
store_path: String,
origin_name: String,
new_name: String,
) -> Result<(), String> {
let origin_file = PathBuf::from(store_path.clone()).join(origin_name);
let new_file = PathBuf::from(store_path).join(new_name);
fs::rename(origin_file, new_file).map_err(|e| format!("重命名问文件失败,{}", e))?;
Ok(())
}

View File

@@ -20,8 +20,8 @@ pub fn update_window_title_with_app<R: Runtime>(
ext: Option<String>, ext: Option<String>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let app_name = match app { let app_name = match app {
AppHold::Instance(app) => app.package_info().name.clone(), AppHold::Instance(app) => app.config().tauri.windows[0].title.clone(),
AppHold::Handle(app) => app.package_info().name.clone(), AppHold::Handle(app) => app.config().tauri.windows[0].title.clone(),
}; };
if let Some(ext) = ext { if let Some(ext) = ext {
window.set_title(&format!("{} - {}", app_name, ext))?; window.set_title(&format!("{} - {}", app_name, ext))?;

View File

@@ -15,7 +15,8 @@ fn main() {
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
commands::prelude::scan_directory, commands::prelude::scan_directory,
commands::prelude::show_drives, commands::prelude::show_drives,
commands::prelude::scan_for_child_dirs commands::prelude::scan_for_child_dirs,
commands::prelude::rename_file
]) ])
.setup(|app| { .setup(|app| {
let main_window = app.get_window("main").unwrap(); let main_window = app.get_window("main").unwrap();

View File

@@ -7,8 +7,8 @@
"withGlobalTauri": false "withGlobalTauri": false
}, },
"package": { "package": {
"productName": "漫画阅读器", "productName": "comic_viewer",
"version": "0.2.0" "version": "../package.json"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {

View File

@@ -25,7 +25,7 @@ export const ContinuationView: FC = () => {
useEffect(() => { useEffect(() => {
ebus?.addListener('navigate_offset', ({ filename }) => { ebus?.addListener('navigate_offset', ({ filename }) => {
let index = indexOf(filename, pluck('filename', files)); let index = indexOf(filename, pluck('filename', files));
virtualListRef.current?.scrollToIndex({ index, align: 'start', behavior: 'smooth' }); virtualListRef.current?.scrollToIndex({ index, align: 'start' });
}); });
ebus?.addListener('reset_views', () => { ebus?.addListener('reset_views', () => {
virtualListRef.current?.scrollTo({ top: 0 }); virtualListRef.current?.scrollTo({ top: 0 });

View File

@@ -4,16 +4,17 @@ import { ActionIcon, Box, Flex, Stack, Text, Tooltip } from '@mantine/core';
import { notifications } from '@mantine/notifications'; import { notifications } from '@mantine/notifications';
import { IconEye, IconSquareMinus, IconSquarePlus } from '@tabler/icons-react'; import { IconEye, IconSquareMinus, IconSquarePlus } from '@tabler/icons-react';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import { equals, isEmpty, isNil, length, map, not } from 'ramda'; import EventEmitter from 'events';
import { FC, PropsWithChildren, useCallback, useContext, useState } from 'react'; import { equals, isEmpty, isNil, map, not } from 'ramda';
import { useMeasure, useMount } from 'react-use'; import { FC, PropsWithChildren, useCallback, useContext } from 'react';
import { useLifecycles, useMeasure } from 'react-use';
import { EventBusContext } from '../EventBus'; import { EventBusContext } from '../EventBus';
import { DirItem } from '../models'; import { DirItem } from '../models';
import { loadSubDirectories } from '../queries/directories'; import { loadSubDirectories } from '../queries/directories';
import { import {
currentRootsSelector, currentRootsSelector,
isExpandedSelector, isExpandedSelector,
selectDirectories, subDirectoriesSelector,
useDirTreeStore useDirTreeStore
} from '../states/dirs'; } from '../states/dirs';
import { useFileListStore } from '../states/files'; import { useFileListStore } from '../states/files';
@@ -30,12 +31,9 @@ const Tree = styled.ul`
} }
`; `;
const Branch: FC<PropsWithChildren<{ current: DirItem; expanded: boolean }>> = ({ const Branch: FC<PropsWithChildren<{ current: DirItem }>> = ({ children, current }) => {
children,
current
}) => {
const { directories: allSubDirs } = useDirTreeStore(); const { directories: allSubDirs } = useDirTreeStore();
const [subDirs, setSubDirs] = useState<DirItem[]>([]); const subDirs = useDirTreeStore(subDirectoriesSelector(current.id));
const isCurrentExpanded = useDirTreeStore(isExpandedSelector(current.id)); const isCurrentExpanded = useDirTreeStore(isExpandedSelector(current.id));
const expend = useDirTreeStore.use.expandDir(); const expend = useDirTreeStore.use.expandDir();
const fold = useDirTreeStore.use.foldDir(); const fold = useDirTreeStore.use.foldDir();
@@ -44,18 +42,24 @@ const Branch: FC<PropsWithChildren<{ current: DirItem; expanded: boolean }>> = (
const storeFiles = useFileListStore.use.updateFiles(); const storeFiles = useFileListStore.use.updateFiles();
const ebus = useContext<EventEmitter>(EventBusContext); const ebus = useContext<EventEmitter>(EventBusContext);
useMount(() => { useLifecycles(
if (isCurrentExpanded) { () => {
setSubDirs(selectDirectories(current.id)(useDirTreeStore.getState().directories)); ebus.addListener(`expand:${current.id}`, () => {
expend(current.id);
loadSubDirectories(current);
});
},
() => {
ebus.removeAllListeners(`expand:${current.id}`);
} }
}); );
const handleExpandAction = useCallback(async () => { const handleExpandAction = useCallback(async () => {
try { try {
if (isCurrentExpanded) { if (isCurrentExpanded) {
fold(current.id); fold(current.id);
} else { } else {
await loadSubDirectories(current); await loadSubDirectories(current);
setSubDirs(selectDirectories(current.id)(useDirTreeStore.getState().directories));
expend(current.id); expend(current.id);
} }
} catch (e) { } catch (e) {
@@ -69,7 +73,6 @@ const Branch: FC<PropsWithChildren<{ current: DirItem; expanded: boolean }>> = (
try { try {
selectDir(current.id); selectDir(current.id);
const files = await invoke('scan_directory', { target: current.path }); const files = await invoke('scan_directory', { target: current.path });
console.log('[debug]获取到文件个数:', length(files));
storeFiles(files); storeFiles(files);
ebus.emit('reset_views'); ebus.emit('reset_views');
} catch (e) { } catch (e) {
@@ -118,14 +121,17 @@ const Branch: FC<PropsWithChildren<{ current: DirItem; expanded: boolean }>> = (
export const DirTree: FC = () => { export const DirTree: FC = () => {
const roots = useDirTreeStore(currentRootsSelector()); const roots = useDirTreeStore(currentRootsSelector());
const { focused, focus, unfocus, selected } = useDirTreeStore(); const { focused, focus, unfocus, selected, foldDir } = useDirTreeStore();
const [viewRef, { width }] = useMeasure(); const [viewRef, { width }] = useMeasure();
const storeFiles = useFileListStore.use.updateFiles();
const ebus = useContext<EventEmitter>(EventBusContext);
const handleFocusAction = useCallback(() => { const handleFocusAction = useCallback(() => {
console.log('[debug]focus action: ', focused, selected);
if (isNil(focused) && not(isNil(selected))) { if (isNil(focused) && not(isNil(selected))) {
ebus.emit(`expand:${selected}`);
focus(selected); focus(selected);
} else { } else {
foldDir(focused.id);
unfocus(); unfocus();
} }
}, [focused, selected]); }, [focused, selected]);
@@ -136,7 +142,8 @@ export const DirTree: FC = () => {
h="100%" h="100%"
spacing={8} spacing={8}
px={4} px={4}
py={4} pt={4}
pb={40}
pos="relative" pos="relative"
sx={{ overflow: 'auto' }} sx={{ overflow: 'auto' }}
ref={viewRef} ref={viewRef}

View File

@@ -1,18 +1,34 @@
//@ts-nocheck //@ts-nocheck
import { Box, Center, Text, Tooltip } from '@mantine/core'; import { Box, Center, Group, Text, TextInput, Tooltip } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { invoke } from '@tauri-apps/api';
import EventEmitter from 'events'; import EventEmitter from 'events';
import { head, includes, indexOf, isEmpty, length, not, pluck } from 'ramda'; import { equals, find, head, includes, indexOf, isEmpty, length, not, pluck, propEq } from 'ramda';
import { FC, useCallback, useContext, useLayoutEffect, useMemo, useRef } from 'react'; import {
FC,
KeyboardEvent,
useCallback,
useContext,
useLayoutEffect,
useMemo,
useRef,
useState
} from 'react';
import { useMeasure } from 'react-use'; import { useMeasure } from 'react-use';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'; import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { EventBusContext } from '../EventBus'; import { EventBusContext } from '../EventBus';
import { useDirTreeStore } from '../states/dirs';
import { sortedFilesSelector, useFileListStore } from '../states/files'; import { sortedFilesSelector, useFileListStore } from '../states/files';
export const FileList: FC = () => { export const FileList: FC = () => {
const files = useFileListStore(sortedFilesSelector()); const files = useFileListStore(sortedFilesSelector());
const storeFiles = useFileListStore.use.updateFiles();
const activeFiles = useFileListStore.use.actives(); const activeFiles = useFileListStore.use.actives();
const cwd = useDirTreeStore.use.selected();
const directories = useDirTreeStore.use.directories();
const ebus = useContext<EventEmitter>(EventBusContext); const ebus = useContext<EventEmitter>(EventBusContext);
const filesCount = useMemo(() => length(files), [files]); const filesCount = useMemo(() => length(files), [files]);
const [editingFile, setEditing] = useState<string | null>(null);
const [parentRef, { height: parentHeight }] = useMeasure(); const [parentRef, { height: parentHeight }] = useMeasure();
const listRef = useRef<VirtuosoHandle | null>(null); const listRef = useRef<VirtuosoHandle | null>(null);
const handleFileSelectAction = useCallback( const handleFileSelectAction = useCallback(
@@ -21,28 +37,50 @@ export const FileList: FC = () => {
}, },
[ebus] [ebus]
); );
const handleFileRenameAction = useCallback(
async (fileId: string, event: KeyboardEvent<HTMLInputElement>) => {
if (equals(event.key, 'Enter')) {
const newFileName = event.target.value;
const originalFile = find(propEq('id', fileId), files);
const storeDirectory = find(propEq('id', cwd), directories);
try {
await invoke('rename_file', {
storePath: storeDirectory?.path,
originName: originalFile?.filename,
newName: newFileName
});
const refreshedFiles = await invoke('scan_directory', { target: storeDirectory?.path });
storeFiles(refreshedFiles);
ebus.emit('reset_views');
} catch (e) {
console.error('[debug]重命名文件:', e);
notifications.show({ message: `重命名文件失败,${e}`, color: 'red' });
}
setEditing(null);
}
},
[files, cwd, directories]
);
useLayoutEffect(() => { useLayoutEffect(() => {
let firstActived = head(activeFiles); let firstActived = head(activeFiles);
let firstActivedIndex = indexOf(firstActived, pluck('filename', files)); let firstActivedIndex = indexOf(firstActived, pluck('filename', files));
listRef.current?.scrollToIndex({ listRef.current?.scrollToIndex({
index: firstActivedIndex, index: firstActivedIndex,
align: 'center', align: 'center'
behavior: 'auto'
}); });
}, [activeFiles]); }, [activeFiles]);
return ( return (
<Box <Box
w="100%" w="100%"
h="100%"
pl={4} pl={4}
sx={{ flexGrow: 1, overflowY: 'auto', contain: 'strict', overflowX: 'hidden' }} sx={{ flexGrow: 1, overflowY: 'auto', contain: 'strict', overflowX: 'hidden' }}
ref={parentRef} ref={parentRef}
> >
{!isEmpty(files) && ( {!isEmpty(files) && (
<Virtuoso <Virtuoso
style={{ height: parentHeight }} style={{ height: parentHeight - 36 }}
totalCount={filesCount} totalCount={filesCount}
ref={listRef} ref={listRef}
itemContent={index => ( itemContent={index => (
@@ -52,6 +90,7 @@ export const FileList: FC = () => {
px={4} px={4}
py={2} py={2}
onClick={() => handleFileSelectAction(files[index].filename)} onClick={() => handleFileSelectAction(files[index].filename)}
onDoubleClick={() => setEditing(files[index].id)}
sx={theme => ({ sx={theme => ({
cursor: 'pointer', cursor: 'pointer',
'&:hover': { '&:hover': {
@@ -60,7 +99,18 @@ export const FileList: FC = () => {
})} })}
> >
<Tooltip label={files[index].filename} zIndex={999}> <Tooltip label={files[index].filename} zIndex={999}>
<Text truncate>{files[index].filename}</Text> {propEq('id', editingFile)(files[index]) ? (
<Group>
<TextInput
defaultValue={files[index].filename}
size="xs"
variant="unstyled"
onKeyDown={event => handleFileRenameAction(files[index].id, event)}
/>
</Group>
) : (
<Text truncate>{files[index].filename}</Text>
)}
</Tooltip> </Tooltip>
</Box> </Box>
)} )}

View File

@@ -92,7 +92,6 @@ export const useDirTreeStore = createStoreHook<DirsStates & DirsActions>((set, g
}, },
focus(specifiedDirId) { focus(specifiedDirId) {
const requestedDir = find(propEq('id', specifiedDirId), get().directories); const requestedDir = find(propEq('id', specifiedDirId), get().directories);
console.log('[debug]focus search: ', specifiedDirId, requestedDir);
if (not(isNil(requestedDir))) { if (not(isNil(requestedDir))) {
set(df => { set(df => {
df.focused = requestedDir; df.focused = requestedDir;