feat: 完善项目。

- 后端代码结构调整;
- 所有屏幕截屏;
- 前端增加 tailwind;
- 前端 style lint。
This commit is contained in:
Ivan Li 2022-11-19 15:28:34 +08:00
parent fbb222ad23
commit 9814247f4e
14 changed files with 1319 additions and 83 deletions

7
.eslintignore Normal file
View File

@ -0,0 +1,7 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
node_modules/*
src-tauri

52
.eslintrc.cjs Normal file
View File

@ -0,0 +1,52 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
settings: {
react: {
version: 'detect',
},
'import/resolver': {
node: {
paths: ['src'],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
},
env: {
browser: true,
amd: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:jsx-a11y/recommended',
'plugin:prettier/recommended', // Make sure this is always the last element in the array.
],
plugins: ['simple-import-sort', 'prettier'],
rules: {
'prettier/prettier': ['error', {}, { usePrettierrc: true }],
'react/react-in-jsx-scope': 'off',
'jsx-a11y/accessible-emoji': 'off',
'react/prop-types': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
'jsx-a11y/anchor-is-valid': [
'error',
{
components: ['Link'],
specialLink: ['hrefLeft', 'hrefRight'],
aspects: ['invalidHref', 'preferButton'],
},
],
},
};

7
.prettierignore Normal file
View File

@ -0,0 +1,7 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
node_modules/*
src-tauri

8
.prettierrc.cjs Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
semi: true,
trailingComma: "all",
singleQuote: true,
printWidth: 90,
tabWidth: 2,
endOfLine: "auto",
};

View File

@ -10,17 +10,26 @@
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^1.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@tauri-apps/api": "^1.1.0"
"react-dom": "^18.2.0"
},
"devDependencies": {
"@tauri-apps/cli": "^1.1.0",
"@types/node": "^18.7.10",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^2.0.0",
"autoprefixer": "^10.4.13",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-simple-import-sort": "^8.0.0",
"postcss": "^8.4.19",
"prettier": "^2.7.1",
"tailwindcss": "^3.2.4",
"typescript": "^4.6.4",
"vite": "^3.0.2",
"@tauri-apps/cli": "^1.1.0"
"vite": "^3.0.2"
}
}

File diff suppressed because it is too large Load Diff

6
postcss.config.cjs Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

9
src-tauri/Cargo.lock generated
View File

@ -1742,6 +1742,12 @@ dependencies = [
"system-deps 6.0.3",
]
[[package]]
name = "paris"
version = "1.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eaf2319cd71dd9ff38c72bebde61b9ea657134abcf26ae4205f54f772a32810"
[[package]]
name = "parking_lot"
version = "0.12.1"
@ -2289,8 +2295,11 @@ dependencies = [
name = "screen-bg-light-desktop"
version = "0.0.0"
dependencies = [
"anyhow",
"base64",
"bmp",
"once_cell",
"paris",
"repng",
"scrap",
"serde",

View File

@ -22,6 +22,9 @@ repng = "0.2.2"
bmp = "0.5.0"
webp = "0.2.2"
base64 = "0.13.1"
anyhow = "1.0.66"
once_cell = "1.16.0"
paris = { version = "1.5", features = ["timestamps", "macros"] }
[features]
# by default Tauri runs in production mode

View File

@ -3,13 +3,11 @@
windows_subsystem = "windows"
)]
use bmp::{Image, Pixel};
use scrap::{Capturer, Display};
use std::io::ErrorKind::WouldBlock;
use std::path::Path;
use std::thread;
use std::time::Duration;
use std::{fs, time::Instant};
mod screen_color_picker;
use paris::*;
use screen_color_picker::{Screen, ScreenColorPicker};
use std::time::Instant;
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
@ -18,65 +16,77 @@ fn greet(name: &str) -> String {
}
#[tauri::command]
fn take_snapshot() -> String {
let one_second = Duration::new(1, 0);
let one_frame = one_second / 60;
fn refresh_displays() {
match ScreenColorPicker::global().refresh_displays() {
Ok(_) => {}
Err(error) => {
error!("{}", error)
}
}
}
let display = Display::primary().expect("Couldn't find primary display.");
let mut capturer = Capturer::new(display).expect("Couldn't begin capture.");
let (w, h) = (capturer.width(), capturer.height());
#[tauri::command]
fn take_snapshot() -> Vec<String> {
let start = Instant::now();
loop {
// Wait until there's a frame.
let start = Instant::now();
let buffer = match capturer.frame() {
Ok(buffer) => buffer,
Err(error) => {
if error.kind() == WouldBlock {
// Keep spinning.
thread::sleep(one_frame);
continue;
} else {
panic!("Error: {}", error);
let screenshots = match ScreenColorPicker::global().take_screenshots_for_all() {
Ok(bitmaps) => {
info!("bitmaps len: {}", bitmaps.len());
match ScreenColorPicker::global().screens.lock() {
Ok(screens) => Vec::from_iter(
screens
.iter()
.enumerate()
.map(|(index, screen)| bitmap_to_webp_base64(screen, &bitmaps[index])),
),
Err(error) => {
error!("can not lock screens. {}", error);
Vec::<String>::new()
}
}
};
println!("Captured! Saving...");
// Flip the ARGB image into a BGRA image.
let mut bitflipped = Vec::with_capacity(w * h * 3);
let stride = buffer.len() / h;
let mut img = Image::new(w as u32, h as u32);
for y in 0..h {
for x in 0..w {
let i = stride * y + 4 * x;
bitflipped.extend_from_slice(&[buffer[i + 2], buffer[i + 1], buffer[i]]);
// img.set_pixel(
// x as u32,
// y as u32,
// Pixel::new(buffer[i + 2], buffer[i + 1], buffer[i]),
// );
}
}
Err(error) => {
error!("can not take screenshots for all. {}", error);
Vec::<String>::new()
}
};
println!("运行耗时: {:?}", start.elapsed());
screenshots
}
let webp_memory =
webp::Encoder::from_rgb(bitflipped.as_slice(), w as u32, h as u32).encode(100.0);
let encode_text = base64::encode(&*webp_memory);
// let output_path = Path::new("assets").join("lake").with_extension("webp");
// std::fs::write(&output_path, &*webp_memory).unwrap();
println!("运行耗时: {:?}", start.elapsed());
return encode_text;
fn bitmap_to_webp_base64(screen: &Screen, bitmap: &Vec<u8>) -> String {
let mut bitflipped = Vec::with_capacity(screen.width * screen.height * 3);
let stride = bitmap.len() / screen.height;
// let mut img = Image::new(w as u32, h as u32);
for y in 0..screen.height {
for x in 0..screen.width {
let i = stride * y + 4 * x;
bitflipped.extend_from_slice(&[bitmap[i + 2], bitmap[i + 1], bitmap[i]]);
// img.set_pixel(
// x as u32,
// y as u32,
// Pixel::new(buffer[i + 2], buffer[i + 1], buffer[i]),
// );
}
}
let webp_memory = webp::Encoder::from_rgb(
bitflipped.as_slice(),
screen.width as u32,
screen.height as u32,
)
.encode(100.0);
return base64::encode(&*webp_memory);
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet, take_snapshot])
.invoke_handler(tauri::generate_handler![
greet,
take_snapshot,
refresh_displays
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -0,0 +1,96 @@
use once_cell::sync::OnceCell;
use scrap::{Capturer, Display};
use std::{
io,
sync::{Arc, Mutex},
};
use paris::*;
pub struct Screen {
bitmap: Option<Vec<u8>>,
capturer: Option<Capturer>,
init_error: Option<io::Error>,
pub height: usize,
pub width: usize,
}
unsafe impl Send for Screen {}
pub struct ScreenColorPicker {
pub screens: Arc<Mutex<Vec<Screen>>>,
}
impl ScreenColorPicker {
pub fn global() -> &'static ScreenColorPicker {
static SCREEN_COLOR_PICKER: OnceCell<ScreenColorPicker> = OnceCell::new();
SCREEN_COLOR_PICKER.get_or_init(|| ScreenColorPicker {
screens: Arc::new(Mutex::new(Vec::<Screen>::new())),
})
}
pub fn refresh_displays(&self) -> anyhow::Result<()> {
let displays = Display::all()
.map_err(|error| anyhow::anyhow!("Can not get all of displays. {}", error))?;
let mut screens = self
.screens
.lock()
.map_err(|error| anyhow::anyhow!("lock screens failed. {}", error))?;
screens.clear();
info!("number of displays: {}", displays.len());
for display in displays {
let height = display.height();
let width = display.width();
match Capturer::new(display) {
Ok(capturer) => screens.push(Screen {
bitmap: None,
capturer: Some(capturer),
init_error: None,
height,
width,
}),
Err(error) => screens.push(Screen {
bitmap: None,
capturer: None,
init_error: Some(error),
height,
width,
}),
}
}
Ok(())
}
pub fn take_screenshots_for_all(&self) -> anyhow::Result<Vec<Vec<u8>>> {
let mut screens = self
.screens
.lock()
.map_err(|error| anyhow::anyhow!("lock screens failed. {}", error))?;
let mut screenshots = Vec::<Vec<u8>>::new();
for screen in screens.iter_mut() {
let screenshot = screen.take().map_err(|error| {
anyhow::anyhow!("take screenshot for display failed. {}", error)
})?;
screenshots.push(screenshot);
}
anyhow::Ok(screenshots)
}
}
impl Screen {
fn take(&mut self) -> anyhow::Result<Vec<u8>> {
match self.capturer.as_mut() {
Some(capturer) => {
let buffer = capturer
.frame()
.map_err(|error| anyhow::anyhow!("failed to frame of display. {}", error))?;
self.bitmap = Some(buffer.to_vec());
anyhow::Ok(buffer.to_vec())
}
None => anyhow::bail!("Do not initialized"),
}
}
}

View File

@ -1,29 +1,33 @@
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import { invoke } from "@tauri-apps/api/tauri";
import "./App.css";
import { useCallback, useState } from 'react';
import reactLogo from './assets/react.svg';
import { invoke } from '@tauri-apps/api/tauri';
import './App.css';
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
const [greetMsg, setGreetMsg] = useState('');
const [name, setName] = useState('');
const [screenshots, setScreenshots] = useState<string[]>([]);
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 }));
}
async function takeSnapshot() {
const base64Text = await invoke("take_snapshot");
setScreenshots([`data:image/webp;base64,${base64Text}`]);
const base64TextList: string[] = await invoke('take_snapshot');
setScreenshots(base64TextList.map((text) => `data:image/webp;base64,${text}`));
}
const refreshDisplays = useCallback(async () => {
await invoke('refresh_displays');
}, []);
return (
<div className="container">
<h1>Welcome to Tauri!</h1>
<div className="row">
<div className="flex gap-5 justify-center">
<a href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" className="logo vite" alt="Vite logo" />
</a>
@ -35,23 +39,31 @@ function App() {
</a>
</div>
<div className='row'>
<img src={screenshots[0]} />
<div className="flex gap-1 justify-center w-screen overflow-hidden">
{screenshots.map((screenshot) => (
<div className="flex-auto">
<img src={screenshot} />
</div>
))}
</div>
<p>Click on the Tauri, Vite, and React logos to learn more.</p>
<div className="row">
<div>
<input
id="greet-input"
onChange={(e) => setName(e.currentTarget.value)}
placeholder="Enter a name..."
/>
<button type="button" onClick={() => takeSnapshot()}>
Take Snapshot
</button>
</div>
<div className="flex gap-5 justify-center ">
<button
className="bg-black bg-opacity-20"
type="button"
onClick={() => refreshDisplays()}
>
Refresh Displays
</button>
<button
className="bg-black bg-opacity-20"
type="button"
onClick={() => takeSnapshot()}
>
Take Snapshot
</button>
</div>
<p>{greetMsg}</p>
</div>

View File

@ -1,3 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;

12
tailwind.config.cjs Normal file
View File

@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
},
},
plugins: [],
}