Compare commits
32 Commits
39accb3cb7
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
5c092cad17 | ||
|
f38165d8d2 | ||
|
49a800fb4b | ||
|
45cbcaf95f | ||
|
8fab1a2c74 | ||
|
17e8bda099 | ||
|
ea19698036 | ||
|
0eb69d777d | ||
|
65943e33b9 | ||
|
6d4dd4de06 | ||
|
a6f043e0d1 | ||
|
50ba85afa3 | ||
|
7331f42e56 | ||
|
36e0c7714c | ||
|
c63da5222e | ||
|
32e797f35d | ||
|
d7aeb00212 | ||
|
45844453b4 | ||
|
a6ac162bfd | ||
|
02f8e70a71 | ||
|
32ad1017ad | ||
|
17a4b3fda1 | ||
|
0697450604 | ||
|
fed1ed9265 | ||
|
fea1c9e6e5 | ||
|
0469f18ef6 | ||
|
8024738334 | ||
|
2fc2bd7185 | ||
|
3e6b89ab39 | ||
|
925b055318 | ||
|
0685e7f4e1 | ||
|
a80e8eb74f |
56
.github/workflows/alpha-release.yml
vendored
Normal file
56
.github/workflows/alpha-release.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Alpha Release CI
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-tauri:
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [macos-latest, windows-latest]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- 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
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: install Rust nightly
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
|
||||
- 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 libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: latest-7
|
||||
|
||||
- name: install frontend dependencies
|
||||
run: pnpm install # change this to npm or pnpm depending on which one you use
|
||||
|
||||
- name: build app
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tauriScript: pnpm tauri
|
||||
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.
|
||||
releaseBody: '从附件中下载对应平台的安装包以及应用。'
|
||||
releaseDraft: true
|
||||
prerelease: false
|
82
.github/workflows/release.yml
vendored
82
.github/workflows/release.yml
vendored
@@ -5,39 +5,7 @@ on:
|
||||
- 'v*'
|
||||
|
||||
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:
|
||||
needs: create-release
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
@@ -47,48 +15,44 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
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
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
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)
|
||||
if: matrix.platform == 'ubuntu-20.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: latest-7
|
||||
|
||||
- name: install frontend dependencies
|
||||
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:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tauriScript: pnpm tauri
|
||||
releaseId: ${{ needs.create-release.outputs.release_id }}
|
||||
|
||||
publish-release:
|
||||
permissions:
|
||||
contents: write
|
||||
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
|
||||
})
|
||||
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.
|
||||
releaseBody: '从附件中下载对应平台的安装包以及应用。'
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,3 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
**/src-tauri/.cargo/
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "comic_viewer",
|
||||
"private": true,
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -29,7 +29,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-use": "^17.4.0",
|
||||
"react-window": "^1.8.8",
|
||||
"react-virtuoso": "^4.1.0",
|
||||
"use-immer": "^0.8.1",
|
||||
"zustand": "^4.2.0"
|
||||
},
|
||||
|
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@@ -28,7 +28,7 @@ specifiers:
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
react-use: ^17.4.0
|
||||
react-window: ^1.8.8
|
||||
react-virtuoso: ^4.1.0
|
||||
typescript: ^4.6.4
|
||||
use-immer: ^0.8.1
|
||||
vite: ^4.0.0
|
||||
@@ -53,7 +53,7 @@ dependencies:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
react-use: 17.4.0_biqbaboplfbrettd7655fr4n2y
|
||||
react-window: 1.8.8_biqbaboplfbrettd7655fr4n2y
|
||||
react-virtuoso: 4.1.0_biqbaboplfbrettd7655fr4n2y
|
||||
use-immer: 0.8.1_immer@9.0.19+react@18.2.0
|
||||
zustand: 4.3.6_immer@9.0.19+react@18.2.0
|
||||
|
||||
@@ -1526,10 +1526,6 @@ packages:
|
||||
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
|
||||
dev: false
|
||||
|
||||
/memoize-one/5.2.1:
|
||||
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
|
||||
dev: false
|
||||
|
||||
/ms/2.1.2:
|
||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||
dev: true
|
||||
@@ -1750,15 +1746,13 @@ packages:
|
||||
tslib: 2.5.0
|
||||
dev: false
|
||||
|
||||
/react-window/1.8.8_biqbaboplfbrettd7655fr4n2y:
|
||||
resolution: {integrity: sha512-D4IiBeRtGXziZ1n0XklnFGu7h9gU684zepqyKzgPNzrsrk7xOCxni+TCckjg2Nr/DiaEEGVVmnhYSlT2rB47dQ==}
|
||||
engines: {node: '>8.0.0'}
|
||||
/react-virtuoso/4.1.0_biqbaboplfbrettd7655fr4n2y:
|
||||
resolution: {integrity: sha512-Vcq5WXn18PvPT55kdeGQ8BN3K95XyPe7hum8zG6Tx7g1CtUYVsQKN7fouMxBSy+XymEDB5ynGy8JWhuqyLLtPw==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
react: '>=16 || >=17 || >= 18'
|
||||
react-dom: '>=16 || >=17 || >= 18'
|
||||
dependencies:
|
||||
'@babel/runtime': 7.21.0
|
||||
memoize-one: 5.2.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
dev: false
|
||||
|
@@ -1,8 +0,0 @@
|
||||
[target.'cfg(target_os = "linux")']
|
||||
rustflags = ["-C", "link-arg=-nostartfiles"]
|
||||
|
||||
[target.'cfg(target_os = "windows")']
|
||||
rustflags = ["-C", "link-args=/ENTRY:_start /SUBSYSTEM:console"]
|
||||
|
||||
[target.'cfg(target_os = "macos")']
|
||||
rustflags = ["-C", "link-args=-e __start -static -nostartfiles"]
|
11
src-tauri/Cargo.lock
generated
11
src-tauri/Cargo.lock
generated
@@ -326,7 +326,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "comic_viewer"
|
||||
version = "0.2.0"
|
||||
version = "0.2.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -335,6 +335,7 @@ dependencies = [
|
||||
"mime_guess",
|
||||
"mountpoints",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
@@ -2285,9 +2286,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.7.1"
|
||||
version = "1.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
|
||||
checksum = "cce168fea28d3e05f158bda4576cf0c844d5045bc2cc3620fa0292ed5bb5814c"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -2305,9 +2306,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.28"
|
||||
version = "0.6.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
|
||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
|
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "comic_viewer"
|
||||
version = "0.2.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
version = "0.2.7"
|
||||
description = "漫画、条漫简易阅读器"
|
||||
authors = ["Khadgar"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/vixalie/comic_viewer"
|
||||
edition = "2021"
|
||||
|
||||
# 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"
|
||||
urlencoding = "2.1.2"
|
||||
mime_guess = "2.0.4"
|
||||
regex = "1.7.2"
|
||||
|
||||
[features]
|
||||
# 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 regex::Regex;
|
||||
use serde::Serialize;
|
||||
use tauri::Runtime;
|
||||
|
||||
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 id: String,
|
||||
pub filename: String,
|
||||
@@ -15,7 +27,19 @@ pub struct FileItem {
|
||||
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 id: String,
|
||||
pub dirname: String,
|
||||
@@ -23,6 +47,18 @@ pub struct DirItem {
|
||||
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 {
|
||||
entry
|
||||
.file_name()
|
||||
@@ -48,8 +84,12 @@ pub async fn scan_directory(target: String) -> Result<Vec<FileItem>, String> {
|
||||
if !entry.path().is_file() {
|
||||
continue;
|
||||
}
|
||||
let (width, height) = image::image_dimensions(entry.path())
|
||||
.map_err(|_e| format!("读取图片文件 {} 元信息失败。", entry.path().display()))?;
|
||||
let image_detect_result = image::image_dimensions(entry.path());
|
||||
if image_detect_result.is_err() {
|
||||
println!("读取图片文件 {} 元信息失败。", entry.path().display());
|
||||
continue;
|
||||
}
|
||||
let (width, height) = image_detect_result.unwrap();
|
||||
let file_hash_id = entry
|
||||
.path()
|
||||
.to_str()
|
||||
@@ -60,25 +100,32 @@ pub async fn scan_directory(target: String) -> Result<Vec<FileItem>, String> {
|
||||
let filename = entry
|
||||
.path()
|
||||
.file_name()
|
||||
.ok_or(String::from("不能获取到文件名。"))?
|
||||
.to_owned()
|
||||
.into_string()
|
||||
.unwrap();
|
||||
.ok_or(String::from("不能获取到文件名。"))
|
||||
.map(ToOwned::to_owned)
|
||||
.map(|s| s.into_string().unwrap());
|
||||
if filename.is_err() {
|
||||
println!("不能获取到文件 {} 文件名。", entry.path().display());
|
||||
continue;
|
||||
}
|
||||
let path = entry
|
||||
.path()
|
||||
.clone()
|
||||
.to_str()
|
||||
.ok_or(String::from("不能获取到文件路径。"))?
|
||||
.to_string();
|
||||
.ok_or(String::from("不能获取到文件路径。"))
|
||||
.map(ToString::to_string);
|
||||
if path.is_err() {
|
||||
println!("不能获取到文件 {} 路径。", entry.path().display());
|
||||
continue;
|
||||
}
|
||||
file_items.push(FileItem {
|
||||
id: file_hash_id,
|
||||
filename,
|
||||
path,
|
||||
filename: filename.unwrap(),
|
||||
path: path.unwrap(),
|
||||
height,
|
||||
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)
|
||||
}
|
||||
@@ -96,6 +143,7 @@ pub async fn show_drives<R: Runtime>(
|
||||
.iter()
|
||||
.filter(|m| !m.path.starts_with("/System") && !m.path.starts_with("/dev"));
|
||||
for mount in mounts {
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
let dirname = mount
|
||||
.path
|
||||
.as_path()
|
||||
@@ -104,6 +152,8 @@ pub async fn show_drives<R: Runtime>(
|
||||
.to_os_string()
|
||||
.into_string()
|
||||
.unwrap();
|
||||
#[cfg(target_os = "windows")]
|
||||
let dirname = mount.path.display().to_string();
|
||||
let dirname = if dirname.len() == 0 {
|
||||
String::from("/")
|
||||
} else {
|
||||
@@ -146,7 +196,7 @@ pub async fn scan_for_child_dirs<R: Runtime>(
|
||||
for entry in std::fs::read_dir(target).map_err(|e| format!("无法读取指定文件夹,{}", e))?
|
||||
{
|
||||
let entry = entry.map_err(|e| format!("无法获取指定文件夹信息,{}", e))?;
|
||||
if is_hidden(&entry) || is_root(&entry) {
|
||||
if is_hidden(&entry) || is_root(&entry) || entry.path().is_file() {
|
||||
continue;
|
||||
}
|
||||
let dir_hash_id = entry
|
||||
@@ -176,6 +226,20 @@ pub async fn scan_for_child_dirs<R: Runtime>(
|
||||
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)
|
||||
}
|
||||
|
||||
#[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>,
|
||||
) -> anyhow::Result<()> {
|
||||
let app_name = match app {
|
||||
AppHold::Instance(app) => app.package_info().name.clone(),
|
||||
AppHold::Handle(app) => app.package_info().name.clone(),
|
||||
AppHold::Instance(app) => app.config().tauri.windows[0].title.clone(),
|
||||
AppHold::Handle(app) => app.config().tauri.windows[0].title.clone(),
|
||||
};
|
||||
if let Some(ext) = ext {
|
||||
window.set_title(&format!("{} - {}", app_name, ext))?;
|
||||
|
@@ -15,7 +15,8 @@ fn main() {
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::prelude::scan_directory,
|
||||
commands::prelude::show_drives,
|
||||
commands::prelude::scan_for_child_dirs
|
||||
commands::prelude::scan_for_child_dirs,
|
||||
commands::prelude::rename_file
|
||||
])
|
||||
.setup(|app| {
|
||||
let main_window = app.get_window("main").unwrap();
|
||||
|
@@ -7,8 +7,8 @@
|
||||
"withGlobalTauri": false
|
||||
},
|
||||
"package": {
|
||||
"productName": "漫画阅读器",
|
||||
"version": "0.2.0"
|
||||
"productName": "comic_viewer",
|
||||
"version": "../package.json"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
@@ -72,7 +72,7 @@
|
||||
"fullscreen": false,
|
||||
"resizable": true,
|
||||
"title": "漫画阅读器",
|
||||
"width": 1000,
|
||||
"width": 1200,
|
||||
"height": 800
|
||||
}
|
||||
]
|
||||
|
19
src/App.tsx
19
src/App.tsx
@@ -1,15 +1,16 @@
|
||||
import { useState } from "react";
|
||||
import reactLogo from "./assets/react.svg";
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
import "./App.css";
|
||||
//@ts-nocheck
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
import { useState } from 'react';
|
||||
import './App.css';
|
||||
import reactLogo from './assets/react.svg';
|
||||
|
||||
function App() {
|
||||
const [greetMsg, setGreetMsg] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [greetMsg, setGreetMsg] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
|
||||
async function greet() {
|
||||
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
||||
setGreetMsg(await invoke("greet", { name }));
|
||||
setGreetMsg(await invoke('greet', { name }));
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -32,14 +33,14 @@ function App() {
|
||||
|
||||
<div className="row">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
greet();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
id="greet-input"
|
||||
onChange={(e) => setName(e.currentTarget.value)}
|
||||
onChange={e => setName(e.currentTarget.value)}
|
||||
placeholder="Enter a name..."
|
||||
/>
|
||||
<button type="submit">Greet</button>
|
||||
|
@@ -1,3 +1,4 @@
|
||||
//@ts-nocheck
|
||||
import { EventEmitter } from 'events';
|
||||
import { createContext } from 'react';
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
//@ts-nocheck
|
||||
import { Group, Stack } from '@mantine/core';
|
||||
import { FC } from 'react';
|
||||
import { ComicView } from './components/ComicView';
|
||||
|
@@ -33,7 +33,8 @@ export const NavMenu: FC = () => {
|
||||
return (
|
||||
<Stack
|
||||
spacing={8}
|
||||
miw={220}
|
||||
w={300}
|
||||
mw={200}
|
||||
h="inherit"
|
||||
sx={theme => ({
|
||||
flexGrow: 1,
|
||||
|
@@ -13,12 +13,16 @@ export const ComicView: FC = () => {
|
||||
const files = useFileListStore(sortedFilesSelector());
|
||||
const viewMode = useZoomState.use.viewMode();
|
||||
const updateViewHeight = useZoomState.use.updateViewHeight();
|
||||
const [containerRef, { height }] = useMeasure();
|
||||
const updateViewWidth = useZoomState.use.updateViewWidth();
|
||||
const [containerRef, { height, width }] = useMeasure();
|
||||
const firstFileId = useMemo(() => head(files)?.id ?? '', [files, files.length]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
updateViewHeight(height);
|
||||
}, [height]);
|
||||
useLayoutEffect(() => {
|
||||
updateViewWidth(width);
|
||||
}, [width]);
|
||||
|
||||
return (
|
||||
<Box w="100%" h="100%" sx={{ overflow: 'hidden' }} ref={containerRef}>
|
||||
|
@@ -1,35 +1,34 @@
|
||||
//@ts-nocheck
|
||||
import EventEmitter from 'events';
|
||||
import { indexOf, isEmpty, length, map, mergeLeft, pluck, range } from 'ramda';
|
||||
import { indexOf, isEmpty, length, map, pluck, range } from 'ramda';
|
||||
import { FC, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
||||
import { VariableSizeList } from 'react-window';
|
||||
import { ListRange, Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
import { EventBusContext } from '../EventBus';
|
||||
import { sortedFilesSelector, useFileListStore } from '../states/files';
|
||||
import { useZoomState } from '../states/zoom';
|
||||
|
||||
export const ContinuationView: FC = () => {
|
||||
const files = useFileListStore(sortedFilesSelector());
|
||||
const zoom = useZoomState.use.currentZoom();
|
||||
const viewHeight = useZoomState.use.viewHeight();
|
||||
const { currentZoom: zoom, viewWidth, viewHeight } = useZoomState();
|
||||
const updateActives = useFileListStore.use.updateActiveFiles();
|
||||
const fileCount = useMemo(() => length(files), [files]);
|
||||
const ebus = useContext<EventEmitter>(EventBusContext);
|
||||
const virtualListRef = useRef<VariableSizeList | null>();
|
||||
const virtualListRef = useRef<VirtuosoHandle | null>(null);
|
||||
const handleOnRenderAction = useCallback(
|
||||
({ visibleStartIndex, visibleStopIndex }) => {
|
||||
updateActives(map(i => files[i].filename, range(visibleStartIndex, visibleStopIndex + 1)));
|
||||
({ startIndex, endIndex }: ListRange) => {
|
||||
updateActives(map(i => files[i].filename, range(startIndex, endIndex + 1)));
|
||||
},
|
||||
[files]
|
||||
);
|
||||
const fileHeights = useMemo(() => map(item => item.height * (zoom / 100), files), [files, zoom]);
|
||||
const maxImageWidth = useMemo(() => Math.floor(viewWidth * (zoom / 100)), [viewWidth, zoom]);
|
||||
|
||||
useEffect(() => {
|
||||
ebus?.addListener('navigate_offset', ({ filename }) => {
|
||||
let index = indexOf(filename, pluck('filename', files));
|
||||
virtualListRef.current?.scrollToItem(index);
|
||||
virtualListRef.current?.scrollToIndex({ index, align: 'start' });
|
||||
});
|
||||
ebus?.addListener('reset_views', () => {
|
||||
virtualListRef.current?.scrollTo(0);
|
||||
virtualListRef.current?.scrollTo({ top: 0 });
|
||||
});
|
||||
return () => {
|
||||
ebus?.removeAllListeners('navigate_offset');
|
||||
@@ -46,29 +45,27 @@ export const ContinuationView: FC = () => {
|
||||
}}
|
||||
>
|
||||
{!isEmpty(files) && (
|
||||
<VariableSizeList
|
||||
itemData={files}
|
||||
itemCount={fileCount}
|
||||
itemSize={index => fileHeights[index]}
|
||||
itemKey={index => files[index].id}
|
||||
height={viewHeight}
|
||||
width="100%"
|
||||
<Virtuoso
|
||||
style={{ height: viewHeight }}
|
||||
ref={virtualListRef}
|
||||
onItemsRendered={handleOnRenderAction}
|
||||
>
|
||||
{({ index, style, data }) => (
|
||||
totalCount={fileCount}
|
||||
computeItemKey={index => files[index].id}
|
||||
rangeChanged={handleOnRenderAction}
|
||||
itemContent={index => (
|
||||
<div
|
||||
style={mergeLeft(style, {
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start'
|
||||
})}
|
||||
alignItems: 'flex-start',
|
||||
width: '100%',
|
||||
height: Math.round(files[index].height * (maxImageWidth / files[index].width))
|
||||
}}
|
||||
>
|
||||
<img src={data[index].path} style={{ width: data[index].width * (zoom / 100) }} />
|
||||
<img src={files[index].path} style={{ width: maxImageWidth }} />
|
||||
</div>
|
||||
)}
|
||||
</VariableSizeList>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@@ -1,18 +1,20 @@
|
||||
//@ts-nocheck
|
||||
import styled from '@emotion/styled';
|
||||
import { ActionIcon, Flex, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { ActionIcon, Box, Flex, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconSquareMinus, IconSquarePlus } from '@tabler/icons-react';
|
||||
import { IconEye, IconSquareMinus, IconSquarePlus } from '@tabler/icons-react';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import EventEmitter from 'events';
|
||||
import { equals, isEmpty, length, map, not } from 'ramda';
|
||||
import { FC, PropsWithChildren, useCallback, useContext, useState } from 'react';
|
||||
import { equals, isEmpty, isNil, map, not } from 'ramda';
|
||||
import { FC, PropsWithChildren, useCallback, useContext } from 'react';
|
||||
import { useLifecycles, useMeasure } from 'react-use';
|
||||
import { EventBusContext } from '../EventBus';
|
||||
import { DirItem } from '../models';
|
||||
import { loadSubDirectories } from '../queries/directories';
|
||||
import {
|
||||
currentRootsSelector,
|
||||
isExpandedSelector,
|
||||
selectDirectories,
|
||||
subDirectoriesSelector,
|
||||
useDirTreeStore
|
||||
} from '../states/dirs';
|
||||
import { useFileListStore } from '../states/files';
|
||||
@@ -29,12 +31,9 @@ const Tree = styled.ul`
|
||||
}
|
||||
`;
|
||||
|
||||
const Branch: FC<PropsWithChildren<{ current: DirItem; expanded: boolean }>> = ({
|
||||
children,
|
||||
current
|
||||
}) => {
|
||||
const Branch: FC<PropsWithChildren<{ current: DirItem }>> = ({ children, current }) => {
|
||||
const { directories: allSubDirs } = useDirTreeStore();
|
||||
const [subDirs, setSubDirs] = useState<DirItem[]>([]);
|
||||
const subDirs = useDirTreeStore(subDirectoriesSelector(current.id));
|
||||
const isCurrentExpanded = useDirTreeStore(isExpandedSelector(current.id));
|
||||
const expend = useDirTreeStore.use.expandDir();
|
||||
const fold = useDirTreeStore.use.foldDir();
|
||||
@@ -43,13 +42,24 @@ const Branch: FC<PropsWithChildren<{ current: DirItem; expanded: boolean }>> = (
|
||||
const storeFiles = useFileListStore.use.updateFiles();
|
||||
const ebus = useContext<EventEmitter>(EventBusContext);
|
||||
|
||||
useLifecycles(
|
||||
() => {
|
||||
ebus.addListener(`expand:${current.id}`, () => {
|
||||
expend(current.id);
|
||||
loadSubDirectories(current);
|
||||
});
|
||||
},
|
||||
() => {
|
||||
ebus.removeAllListeners(`expand:${current.id}`);
|
||||
}
|
||||
);
|
||||
|
||||
const handleExpandAction = useCallback(async () => {
|
||||
try {
|
||||
if (isCurrentExpanded) {
|
||||
fold(current.id);
|
||||
} else {
|
||||
await loadSubDirectories(current);
|
||||
setSubDirs(selectDirectories(current.id)(useDirTreeStore.getState().directories));
|
||||
expend(current.id);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -63,7 +73,6 @@ const Branch: FC<PropsWithChildren<{ current: DirItem; expanded: boolean }>> = (
|
||||
try {
|
||||
selectDir(current.id);
|
||||
const files = await invoke('scan_directory', { target: current.path });
|
||||
console.log('[debug]获取到文件个数:', length(files));
|
||||
storeFiles(files);
|
||||
ebus.emit('reset_views');
|
||||
} catch (e) {
|
||||
@@ -112,9 +121,45 @@ const Branch: FC<PropsWithChildren<{ current: DirItem; expanded: boolean }>> = (
|
||||
|
||||
export const DirTree: FC = () => {
|
||||
const roots = useDirTreeStore(currentRootsSelector());
|
||||
const { focused, focus, unfocus, selected, foldDir } = useDirTreeStore();
|
||||
const [viewRef, { width }] = useMeasure();
|
||||
const storeFiles = useFileListStore.use.updateFiles();
|
||||
const ebus = useContext<EventEmitter>(EventBusContext);
|
||||
|
||||
const handleFocusAction = useCallback(() => {
|
||||
if (isNil(focused) && not(isNil(selected))) {
|
||||
ebus.emit(`expand:${selected}`);
|
||||
focus(selected);
|
||||
} else {
|
||||
foldDir(focused.id);
|
||||
unfocus();
|
||||
}
|
||||
}, [focused, selected]);
|
||||
|
||||
return (
|
||||
<Stack w="auto" h="100%" spacing={8} px={4} py={4} sx={{ overflow: 'auto' }}>
|
||||
<Stack
|
||||
w="auto"
|
||||
h="100%"
|
||||
spacing={8}
|
||||
px={4}
|
||||
pt={4}
|
||||
pb={40}
|
||||
pos="relative"
|
||||
sx={{ overflow: 'auto' }}
|
||||
ref={viewRef}
|
||||
>
|
||||
<Box pos="fixed" style={{ transform: `translateX(${width * 0.85}px)` }}>
|
||||
<Tooltip label={isNil(focused) ? '聚焦当前选择的目录' : '解除聚焦'}>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color={isNil(focused) ? 'grape' : 'green'}
|
||||
disabled={isNil(selected)}
|
||||
onClick={handleFocusAction}
|
||||
>
|
||||
<IconEye stroke={1.5} size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Tree>
|
||||
{map(
|
||||
item => (
|
||||
|
@@ -1,63 +1,120 @@
|
||||
//@ts-nocheck
|
||||
import { Box, Center, Text } 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 { head, includes, indexOf, isEmpty, length, mergeLeft, not, pluck } from 'ramda';
|
||||
import { FC, useCallback, useContext, useLayoutEffect, useMemo, useRef } from 'react';
|
||||
import { equals, find, head, includes, indexOf, isEmpty, length, not, pluck, propEq } from 'ramda';
|
||||
import {
|
||||
FC,
|
||||
KeyboardEvent,
|
||||
useCallback,
|
||||
useContext,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react';
|
||||
import { useMeasure } from 'react-use';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
import { EventBusContext } from '../EventBus';
|
||||
import { useDirTreeStore } from '../states/dirs';
|
||||
import { sortedFilesSelector, useFileListStore } from '../states/files';
|
||||
|
||||
export const FileList: FC = () => {
|
||||
const files = useFileListStore(sortedFilesSelector());
|
||||
const storeFiles = useFileListStore.use.updateFiles();
|
||||
const activeFiles = useFileListStore.use.actives();
|
||||
const cwd = useDirTreeStore.use.selected();
|
||||
const directories = useDirTreeStore.use.directories();
|
||||
const ebus = useContext<EventEmitter>(EventBusContext);
|
||||
const filesCount = useMemo(() => length(files), [files]);
|
||||
const [editingFile, setEditing] = useState<string | null>(null);
|
||||
const [parentRef, { height: parentHeight }] = useMeasure();
|
||||
const listRef = useRef<FixedSizeList | null>();
|
||||
const listRef = useRef<VirtuosoHandle | null>(null);
|
||||
const handleFileSelectAction = useCallback(
|
||||
(filename: string) => {
|
||||
ebus.emit('navigate_offset', { filename });
|
||||
},
|
||||
[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(() => {
|
||||
let firstActived = head(activeFiles);
|
||||
let firstActivedIndex = indexOf(firstActived, pluck('filename', files));
|
||||
listRef.current?.scrollToItem(firstActivedIndex, 'smart');
|
||||
listRef.current?.scrollToIndex({
|
||||
index: firstActivedIndex,
|
||||
align: 'center'
|
||||
});
|
||||
}, [activeFiles]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
w="100%"
|
||||
h="100%"
|
||||
pl={4}
|
||||
sx={{ flexGrow: 1, overflowY: 'auto', contain: 'strict', overflowX: 'hidden' }}
|
||||
ref={parentRef}
|
||||
>
|
||||
{!isEmpty(files) && (
|
||||
<FixedSizeList itemCount={filesCount} itemSize={35} height={parentHeight} ref={listRef}>
|
||||
{({ index, style }) => (
|
||||
<Virtuoso
|
||||
style={{ height: parentHeight - 36 }}
|
||||
totalCount={filesCount}
|
||||
ref={listRef}
|
||||
itemContent={index => (
|
||||
<Box
|
||||
bg={includes(files[index].filename, activeFiles) && 'grape'}
|
||||
key={index}
|
||||
px={4}
|
||||
py={2}
|
||||
onClick={() => handleFileSelectAction(files[index].filename)}
|
||||
sx={theme =>
|
||||
mergeLeft(style, {
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
color: not(includes(files[index].filename, activeFiles)) && theme.colors.red
|
||||
}
|
||||
})
|
||||
}
|
||||
onDoubleClick={() => setEditing(files[index].id)}
|
||||
sx={theme => ({
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
color: not(includes(files[index].filename, activeFiles)) && theme.colors.red
|
||||
}
|
||||
})}
|
||||
>
|
||||
{files[index].filename}
|
||||
<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>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Box>
|
||||
)}
|
||||
</FixedSizeList>
|
||||
/>
|
||||
)}
|
||||
{isEmpty(files) && (
|
||||
<Center h="100%">
|
||||
|
@@ -10,8 +10,8 @@ import { useZoomState } from '../states/zoom';
|
||||
export const SingleView: FC = () => {
|
||||
const files = useFileListStore.use.files();
|
||||
const actives = useFileListStore.use.actives();
|
||||
const zoom = useZoomState.use.currentZoom();
|
||||
const viewHeight = useZoomState.use.viewHeight();
|
||||
const { currentZoom: zoom, viewHeight, viewWidth } = useZoomState();
|
||||
const maxImageWidth = useMemo(() => viewWidth * (zoom / 100), [viewWidth, zoom]);
|
||||
const updateActives = useFileListStore.use.updateActiveFiles();
|
||||
const ebus = useContext<EventEmitter>(EventBusContext);
|
||||
const [pageConRef, { width: pageConWidth }] = useMeasure();
|
||||
@@ -21,9 +21,9 @@ export const SingleView: FC = () => {
|
||||
}, [files, actives]);
|
||||
const largerThanView = useMemo(() => {
|
||||
if (isNil(activeFile)) return false;
|
||||
let imageHeightAfterZoom = activeFile?.height * (zoom / 100);
|
||||
let imageHeightAfterZoom = activeFile?.height * (maxImageWidth / activeFile?.width);
|
||||
return gt(imageHeightAfterZoom, viewHeight);
|
||||
}, [activeFile, viewHeight, zoom]);
|
||||
}, [activeFile, viewHeight, maxImageWidth]);
|
||||
const handlePaginationAction = useCallback(
|
||||
(event: BaseSyntheticEvent) => {
|
||||
let middle = pageConWidth / 2;
|
||||
@@ -62,7 +62,7 @@ export const SingleView: FC = () => {
|
||||
height: largerThanView ? '100%' : viewHeight
|
||||
}}
|
||||
>
|
||||
<img src={activeFile.path} style={{ width: `${zoom}%` }} />
|
||||
<img src={activeFile.path} style={{ width: maxImageWidth }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@@ -1,3 +1,4 @@
|
||||
//@ts-nocheck
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { useDirTreeStore } from '../states/dirs';
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
//@ts-nocheck
|
||||
import {
|
||||
addIndex,
|
||||
append,
|
||||
@@ -22,7 +23,7 @@ interface DirsStates {
|
||||
drives: DirItem[];
|
||||
directories: DirItem[];
|
||||
focused?: DirItem;
|
||||
selected?: DirItem;
|
||||
selected?: string;
|
||||
expanded: string[];
|
||||
}
|
||||
|
||||
@@ -99,7 +100,7 @@ export const useDirTreeStore = createStoreHook<DirsStates & DirsActions>((set, g
|
||||
},
|
||||
unfocus() {
|
||||
set(df => {
|
||||
df.focus = undefined;
|
||||
df.focused = undefined;
|
||||
});
|
||||
},
|
||||
selectDirectory(dirId) {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
//@ts-nocheck
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri';
|
||||
import { addIndex, map, mergeLeft, sort } from 'ramda';
|
||||
import { FileItem } from '../models';
|
||||
|
@@ -1,3 +1,4 @@
|
||||
//@ts-nocheck
|
||||
import { SyncObjectCallback } from '../types';
|
||||
import { createStoreHook } from '../utils/store_creator';
|
||||
|
||||
@@ -6,11 +7,13 @@ interface ZoomState {
|
||||
currentZoom: number;
|
||||
viewMode: 'single' | 'continuation';
|
||||
viewHeight: number;
|
||||
viewWidth: number;
|
||||
}
|
||||
|
||||
type ZoomActions = {
|
||||
zoom: SyncObjectCallback<number>;
|
||||
updateViewHeight: SyncObjectCallback<number>;
|
||||
updateViewWidth: SyncObjectCallback<number>;
|
||||
switchViewMode: SyncObjectCallback<'single' | 'continuation'>;
|
||||
};
|
||||
|
||||
@@ -18,7 +21,8 @@ const initialState: ZoomState = {
|
||||
autoFit: false,
|
||||
currentZoom: 80,
|
||||
viewMode: 'continuation',
|
||||
viewHeight: 0
|
||||
viewHeight: 0,
|
||||
viewWidth: 0
|
||||
};
|
||||
|
||||
export const useZoomState = createStoreHook<ZoomState & ZoomActions>(set => ({
|
||||
@@ -33,6 +37,11 @@ export const useZoomState = createStoreHook<ZoomState & ZoomActions>(set => ({
|
||||
df.viewHeight = height;
|
||||
});
|
||||
},
|
||||
updateViewWidth(width) {
|
||||
set(df => {
|
||||
df.viewWidth = width;
|
||||
});
|
||||
},
|
||||
switchViewMode(mode) {
|
||||
set(df => {
|
||||
df.viewMode = mode;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
//@ts-nocheck
|
||||
import { and, gt, lt } from 'ramda';
|
||||
|
||||
export function withinRange(
|
||||
|
Reference in New Issue
Block a user