Compare commits
9 Commits
6d4dd4de06
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
5c092cad17 | ||
|
f38165d8d2 | ||
|
49a800fb4b | ||
|
45cbcaf95f | ||
|
8fab1a2c74 | ||
|
17e8bda099 | ||
|
ea19698036 | ||
|
0eb69d777d | ||
|
65943e33b9 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "comic_viewer",
|
"name": "comic_viewer",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.3",
|
"version": "0.2.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
11
src-tauri/Cargo.lock
generated
11
src-tauri/Cargo.lock
generated
@@ -326,7 +326,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "comic_viewer"
|
name = "comic_viewer"
|
||||||
version = "0.2.3"
|
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"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "comic_viewer"
|
name = "comic_viewer"
|
||||||
version = "0.2.3"
|
version = "0.2.7"
|
||||||
description = "漫画、条漫简易阅读器"
|
description = "漫画、条漫简易阅读器"
|
||||||
authors = ["Khadgar"]
|
authors = ["Khadgar"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -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
|
||||||
|
@@ -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()
|
||||||
@@ -89,7 +125,7 @@ pub async fn scan_directory(target: String) -> Result<Vec<FileItem>, String> {
|
|||||||
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)
|
||||||
}
|
}
|
||||||
@@ -190,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(())
|
||||||
|
}
|
||||||
|
@@ -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))?;
|
||||||
|
@@ -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();
|
||||||
|
@@ -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 });
|
||||||
|
@@ -123,6 +123,7 @@ export const DirTree: FC = () => {
|
|||||||
const roots = useDirTreeStore(currentRootsSelector());
|
const roots = useDirTreeStore(currentRootsSelector());
|
||||||
const { focused, focus, unfocus, selected, foldDir } = 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 ebus = useContext<EventEmitter>(EventBusContext);
|
||||||
|
|
||||||
const handleFocusAction = useCallback(() => {
|
const handleFocusAction = useCallback(() => {
|
||||||
@@ -141,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}
|
||||||
|
@@ -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}>
|
||||||
|
{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>
|
<Text truncate>{files[index].filename}</Text>
|
||||||
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
Reference in New Issue
Block a user