Environment#
System: Windows 11
IDE: VSCode + Github Copilot Chat + Rust + Tauri plugin
Env: npm, Rust
The project framework has been established through npm create tauri-app@latest
.
The following headings are all text entered in the copilot chat in VSCode, followed by updates and replacements of the code output by Copilot.
Modify the homepage to: the left side is the navigation area, displaying page links for different functions in a tree view; after clicking a function menu item in the left navigation area, the corresponding function page loads on the right side.#
Note
Here, 'App.tsx' is referenced.
GitHub Copilot: To achieve this functionality, we need to perform the following steps in App.tsx
:
- Create a navigation bar component that displays page links for different functions in a tree view.
- Use React Router to handle page navigation.
- Load the corresponding function page in the right area.
First, install React Router:
npm install react-router-dom
Then, modify the App.tsx
file as follows:
import { useState } from "react";
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import reactLogo from "./assets/react.svg";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
function Home() {
return <h2>Home Page</h2>;
}
function Feature1() {
return <h2>Feature 1 Page</h2>;
}
function Feature2() {
return <h2>Feature 2 Page</h2>;
}
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
At the same time, update the App.css
file to accommodate the new layout:
.container {
display: flex;
}
.sidebar {
width: 200px;
background-color: #f0f0f0;
padding: 10px;
}
.content {
flex-grow: 1;
padding: 10px;
}
Thus, the left side will display the navigation bar, and the right side will load the corresponding function page based on the selection from the navigation bar.
Similar code found with 1 license type
Create a standalone Home component#
GitHub Copilot: Okay, we can extract the Home
component into a separate file. Here are the steps:
- Create a new file
Home.tsx
. - Move the
Home
component code to theHome.tsx
file. - Import the
Home
component inApp.tsx
.
First, create the Home.tsx
file and add the following code:
// Home.tsx
import React from "react";
function Home() {
return <h2>Home Page</h2>;
}
export default Home;
Then, modify the App.tsx
file to import the Home
component:
import { useState } from "react";
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import reactLogo from "./assets/react.svg";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
import Home from "./Home";
function Feature1() {
return <h2>Feature 1 Page</h2>;
}
function Feature2() {
return <h2>Feature 2 Page</h2>;
}
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
Thus, the Home
component has been extracted into a standalone file.
Similar code found with 1 license type
Make the left navigation bar stretch vertically to fill the page#
Note
The left navigation bar only occupies the area containing text, so we hope it can fill the entire height.
GitHub Copilot: To make the left navigation bar stretch vertically to fill the page, we need to adjust the CSS styles. The specific steps are as follows:
- Modify the
App.css
file to ensure the navigation bar occupies the full height of the page. - Ensure the container's height also fills the page.
Here is the updated App.css
file:
/* App.css */
html, body, #root {
height: 100%;
margin: 0;
padding: 0;
}
.container {
display: flex;
height: 100%;
}
.sidebar {
width: 200px;
background-color: #f0f0f0;
padding: 10px;
height: 100%;
}
.content {
flex-grow: 1;
padding: 10px;
}
Ensure that the App.tsx
file contains the following code:
import { useState } from "react";
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
import Home from "./Home";
function Feature1() {
return <h2>Feature 1 Page</h2>;
}
function Feature2() {
return <h2>Feature 2 Page</h2>;
}
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
Thus, the left navigation bar will stretch vertically to fill the page height.
Similar code found with 1 license type
Ensure the right page and left navigation bar are top-aligned#
Note
There was a situation where the right content area displayed below the navigation area; 4, 5, and 6 were attempts to fix this prompt, and later it was found that the .container
style in CSS was not fully replaced. After replacing it again, it displayed correctly.
GitHub Copilot: To ensure the right page and left navigation bar are top-aligned, we need to adjust the CSS styles. The specific steps are as follows:
- Ensure the container's height fills the page, and both the navigation bar and content area start aligned from the top.
- Update the
App.css
file to ensure the layout is correct.
Here is the updated App.css
file:
/* App.css */
html, body, #root {
height: 100%;
margin: 0;
padding: 0;
}
.container {
display: flex;
height: 100%;
align-items: flex-start; /* Ensure child elements are top-aligned */
}
.sidebar {
width: 200px;
background-color: #f0f0f0;
padding: 10px;
height: 100%;
}
.content {
flex-grow: 1;
padding: 10px;
height: 100%;
}
Ensure that the App.tsx
file contains the following code:
import { useState } from "react";
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
import Home from "./Home";
function Feature1() {
return <h2>Feature 1 Page</h2>;
}
function Feature2() {
return <h2>Feature 2 Page</h2>;
}
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
Thus, the right page and left navigation bar will be top-aligned.
Similar code found with 1 license type
Ensure the child elements in .container
are arranged in a left-right layout#
GitHub Copilot: To ensure the child elements in .container
are arranged in a left-right layout, we need to adjust the CSS styles. The specific steps are as follows:
- Ensure
.container
usesflex
layout, and the child elements are arranged left-right. - Remove duplicate
.container
style definitions.
Here is the updated App.css
file:
/* App.css */
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
html, body, #root {
height: 100%;
margin: 0;
padding: 0;
}
.container {
display: flex;
height: 100%;
align-items: flex-start; /* Ensure child elements are top-aligned */
}
.sidebar {
width: 200px;
background-color: #f0f0f0;
padding: 10px;
height: 100%;
}
.content {
flex-grow: 1;
padding: 10px;
height: 100%;
}
Ensure that the App.tsx
file contains the following code:
import { useState } from "react";
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
import Home from "./Home";
function Feature1() {
return <h2>Feature 1 Page</h2>;
}
function Feature2() {
return <h2>Feature 2 Page</h2>;
}
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
Thus, the child elements in .container
will be arranged in a left-right layout and top-aligned.
Similar code found with 1 license type
Set the background color of the navigation bar to the system background color#
Note
The system theme is dark, but the default background color is white, and this code did not take effect. Finally, the background color configuration of .sidebar
was manually removed.
GitHub Copilot: To set the background color of the navigation bar to the system background color, you can use the background-color
property in CSS and set its value to the system color. Here is the updated App.css
file:
/* App.css */
html, body, #root {
height: 100%;
margin: 0;
padding: 0;
}
.container {
display: flex;
height: 100%;
align-items: flex-start; /* Ensure child elements are top-aligned */
}
.sidebar {
width: 200px;
background-color: var(--system-background-color, #f0f0f0); /* Use system background color */
padding: 10px;
height: 100%;
}
.content {
flex-grow: 1;
padding: 10px;
height: 100%;
}
.logo {
height: 6em;
}
In the above code, var(--system-background-color, #f0f0f0)
uses a CSS variable to set the background color. If the system background color is not defined, it will use the default color #f0f0f0
. You can adjust the default color as needed.
Move Feature1 into a separate module under the Features subdirectory#
GitHub Copilot: Okay, we can extract the Feature1
component into a separate file and place it in the Features
subdirectory. Here are the steps:
- Create a new directory
Features
. - In the
Features
directory, create a new fileFeature1.tsx
. - Move the
Feature1
component code to theFeature1.tsx
file. - Import the
Feature1
component inApp.tsx
.
First, create the Features/Feature1.tsx
file and add the following code:
// Features/Feature1.tsx
import React from "react";
function Feature1() {
return <h2>Feature 1 Page</h2>;
}
export default Feature1;
Then, modify the App.tsx
file to import the Feature1
component:
import { useState } from "react";
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
import Home from "./Home";
import Feature1 from "./Features/Feature1";
function Feature2() {
return <h2>Feature 2 Page</h2>;
}
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
Thus, the Feature1
component has been extracted into a separate module in the Features
subdirectory.
Similar code found with 1 license type
Similarly, move Feature2 into a separate file#
GitHub Copilot: Okay, we can extract the Feature2
component into a separate file and place it in the Features
subdirectory. Here are the steps:
- In the
Features
directory, create a new fileFeature2.tsx
. - Move the
Feature2
component code to theFeature2.tsx
file. - Import the
Feature2
component inApp.tsx
.
First, create the Features/Feature2.tsx
file and add the following code:
// Features/Feature2.tsx
import React from "react";
function Feature2() {
return <h2>Feature 2 Page</h2>;
}
export default Feature2;
Then, modify the App.tsx
file to import the Feature2
component:
import { useState } from "react";
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
import Home from "./Home";
import Feature1 from "./Features/Feature1";
import Feature2 from "./Features/Feature2";
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
Thus, the Feature2
component has been extracted into a separate module in the Features
subdirectory.
Similar code found with 1 license type
Adjust the page height to avoid scroll bars#
Note
The default content area has scroll bars.
GitHub Copilot: To ensure the page height is adjusted to avoid scroll bars, we need to ensure that both the container's height and the content's height are set correctly. We can use overflow: hidden
to prevent scroll bars from appearing and ensure that the content area does not exceed the viewport height.
Here is the updated App.css
file:
/* App.css */
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
html, body, #root {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden; /* Prevent scroll bars from appearing */
}
.container {
display: flex;
height: 100%;
align-items: flex-start; /* Ensure child elements are top-aligned */
}
.sidebar {
width: 200px;
padding: 10px;
height: 100%;
background-color: var(--system-background-color, #f0f0f0); /* Use system background color */
}
.content {
flex-grow: 1;
padding: 10px;
height: 100%;
overflow: auto; /* Allow content area to scroll */
}
Ensure that the App.tsx
file contains the following code:
import { useState } from "react";
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
import Home from "./Home";
import Feature1 from "./Features/Feature1";
import Feature2 from "./Features/Feature2";
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
Thus, the page height will be adjusted to avoid scroll bars, and the content area will allow scrolling.
Similar code found with 1 license type
Ensure greetMsg is displayed on a new line#
Note
The reason here is to retain the greet interaction in the example, manually moving the code from App.tsx to Home.tsx, but the <p>
tag did not take effect, and the text output area displayed on the same line. The issue was resolved in the next prompt by using a <div>
tag.
GitHub Copilot: To ensure greetMsg
is displayed on a new line, we can place it in a paragraph (<p>
) tag. Here is the updated Home.tsx
file:
// Home.tsx
import React from "react";
function Home({ greetMsg }: { greetMsg: string }) {
return (
<div>
<h2>Home Page</h2>
<p>{greetMsg}</p>
</div>
);
}
export default Home;
At the same time, ensure that greetMsg
is passed as a prop to the Home
component in App.tsx
:
import { useState } from "react";
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
import Home from "./Home";
import Feature1 from "./Features/Feature1";
import Feature2 from "./Features/Feature2";
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home greetMsg={greetMsg} />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
Thus, greetMsg
will be displayed in the Home
component on a new line.
Similar code found with 1 license type
No need to pass greetMsg from App#
GitHub Copilot: Okay, we can define greetMsg
directly in the Home
component without needing to pass it from App
. Here is the updated Home.tsx
file:
// Home.tsx
import React, { useState } from "react";
import { invoke } from "@tauri-apps/api/core";
function Home() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
setGreetMsg(await invoke("greet", { name }));
}
return (
<div>
<h2>Home Page</h2>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
<button onClick={greet}>Greet</button>
<p>{greetMsg}</p>
</div>
);
}
export default Home;
At the same time, the App.tsx
file does not need any modifications since greetMsg
is no longer needed to be passed from App
:
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import "./App.css";
import Home from "./Home";
import Feature1 from "./Features/Feature1";
import Feature2 from "./Features/Feature2";
function App() {
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feature1">Feature 1</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/feature1" element={<Feature1 />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
Thus, greetMsg
will be defined and used in the Home
component.
Similar code found with 2 license types
Rename Feature1 to ClipBoard#
GitHub Copilot: Okay, we can rename the Feature1
component to ClipBoard
. Here are the steps:
- Rename the file
Feature1.tsx
toFeatures/ClipBoard.tsx
. - Update the component name to
ClipBoard
. - Import the
ClipBoard
component inApp.tsx
.
First, rename the file Feature1.tsx
to Features/ClipBoard.tsx
and update the file content as follows:
// Features/ClipBoard.tsx
import React from "react";
function ClipBoard() {
return <h2>ClipBoard Page</h2>;
}
export default ClipBoard;
Then, modify the App.tsx
file to import the ClipBoard
component:
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import "./App.css";
import Home from "./Home";
import ClipBoard from "./Features/ClipBoard";
import Feature2 from "./Features/Feature2";
function App() {
return (
<Router>
<div className="container">
<nav className="sidebar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/clipboard">ClipBoard</Link>
</li>
<li>
<Link to="/feature2">Feature 2</Link>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/clipboard" element={<ClipBoard />} />
<Route path="/feature2" element={<Feature2 />} />
</Routes>
</div>
</div>
</Router>
);
}
export default App;
Thus, the Feature1
component has been renamed to ClipBoard
and is correctly imported and used in App.tsx
.
On the ClipBoard page, display the most recent clipboard content, sorted in reverse chronological order, showing a maximum of 100 entries#
GitHub Copilot: To implement the ClipBoard page displaying the most recent clipboard content, sorted in reverse chronological order, showing a maximum of 100 entries, we need the following steps:
- Create a state to store clipboard content.
- Use the
useEffect
hook to simulate fetching clipboard content. - Sort the clipboard content in reverse chronological order and limit the display to 100 entries.
Here is the updated ClipBoard.tsx
file:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
interface ClipboardItem {
content: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
useEffect(() => {
// Simulate fetching clipboard content
const fetchClipboardItems = () => {
const items = [
{ content: "Item 1", timestamp: Date.now() - 1000 },
{ content: "Item 2", timestamp: Date.now() - 2000 },
// Add more simulated data
];
setClipboardItems(items);
};
fetchClipboardItems();
}, []);
// Sort in reverse chronological order and limit to 100 entries
const sortedItems = clipboardItems
.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, 100);
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{sortedItems.map((item, index) => (
<li key={index}>
<p>{item.content}</p>
<small>{new Date(item.timestamp).toLocaleString()}</small>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
In this implementation, we:
- Defined a
ClipboardItem
interface to represent clipboard content items. - Used the
useState
hook to store clipboard content. - Used the
useEffect
hook to simulate fetching clipboard content and store it in the state. - Sorted the clipboard content in reverse chronological order and limited the display to 100 entries.
- Rendered the clipboard content list in the component.
You can replace the simulated data part with actual clipboard content as needed.
Read the clipboard every second, and if there is new content, add it to the clipboard content list#
Note
Here, you need to manually add clipboard read/write permissions in src-tauri/capabilities/default.json
. The same applies to subsequent file operations. Also, the import
in the Copilot code is still in Tauri v1 format, which needs to be manually adjusted to v2 format.
GitHub Copilot: To implement reading the clipboard every second and adding new content to the clipboard content list if it exists, we can use setInterval
to read the clipboard content periodically and handle it in useEffect
.
Here is the updated ClipBoard.tsx
file:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText } from "@tauri-apps/plugin-clipboard";
interface ClipboardItem {
content: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent) {
setClipboardItems((prevItems) => [
{ content, timestamp: Date.now() },
...prevItems,
]);
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
// Sort in reverse chronological order and limit to 100 entries
const sortedItems = clipboardItems.slice(0, 100);
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{sortedItems.map((item, index) => (
<li key={index}>
<p>{item.content}</p>
<small>{new Date(item.timestamp).toLocaleString()}</small>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
In this implementation, we:
- Used the
useState
hook to store clipboard content and the last read content. - Used the
useEffect
hook to set up a timer that reads the clipboard content every second. - If new content is read and is different from the last read content, it is added to the clipboard content list, and the last read content is updated.
- Sorted the clipboard content in reverse chronological order and limited the display to 100 entries.
- Rendered the clipboard content list in the component.
Thus, the clipboard content will be read every second, and new content will be added to the clipboard content list if it exists.
Display the time above the content#
GitHub Copilot: To display the time above the content, we need to adjust the rendering order in the ClipBoard.tsx
file. Here is the updated code:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText } from "@tauri-apps/plugin-clipboard";
interface ClipboardItem {
content: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent) {
setClipboardItems((prevItems) => [
{ content, timestamp: Date.now() },
...prevItems,
]);
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
// Sort in reverse chronological order and limit to 100 entries
const sortedItems = clipboardItems.slice(0, 100);
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{sortedItems.map((item, index) => (
<li key={index}>
<small>{new Date(item.timestamp).toLocaleString()}</small>
<p>{item.content}</p>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
In this implementation, we placed the time (<small>{new Date(item.timestamp).toLocaleString()}</small>
) above the content (<p>{item.content}</p>
). Thus, the time will be displayed above the content.
Provide "Copy" and "Delete" text links for each content item; when clicking "Copy," re-copy to the clipboard, and when clicking "Delete," remove that item from the list#
GitHub Copilot: To implement providing "Copy" and "Delete" text links for each content item, we need the following steps:
- Add "Copy" and "Delete" buttons.
- Implement the copy functionality to re-copy the content to the clipboard.
- Implement the delete functionality to remove the item from the list.
Here is the updated ClipBoard.tsx
file:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
interface ClipboardItem {
content: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent) {
setClipboardItems((prevItems) => [
{ content, timestamp: Date.now() },
...prevItems,
]);
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
const handleCopy = async (content: string) => {
await writeText(content);
};
const handleDelete = (timestamp: number) => {
setClipboardItems((prevItems) =>
prevItems.filter((item) => item.timestamp !== timestamp)
);
};
// Sort in reverse chronological order and limit to 100 entries
const sortedItems = clipboardItems.slice(0, 100);
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{sortedItems.map((item, index) => (
<li key={index}>
<small>{new Date(item.timestamp).toLocaleString()}</small>
<p>{item.content}</p>
<a href="#" onClick={() => handleCopy(item.content)}>Copy</a> |
<a href="#" onClick={() => handleDelete(item.timestamp)}>Delete</a>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
In this implementation, we:
- Added "Copy" and "Delete" buttons.
- Implemented the
handleCopy
function to re-copy the content to the clipboard. - Implemented the
handleDelete
function to remove the item from the list. - Added "Copy" and "Delete" links in each list item and bound the corresponding event handler functions.
Thus, each content item will provide "Copy" and "Delete" text links, and clicking "Copy" will re-copy to the clipboard, while clicking "Delete" will remove that item from the list.
Save both the content text and the MD5 value of the content in the list; when new content arrives, if the MD5 value matches the existing content text, only update the time, rather than adding a new record#
Note
Copying will generate a new record, so we distinguish by MD5 to ensure unique display.
GitHub Copilot: To save both the content text and the MD5 value of the content in the list, and only update the time when the new content's MD5 value matches the existing content text rather than adding a new record, we need the following steps:
- Install the
crypto-js
library to calculate the MD5 value. - Modify the
ClipboardItem
interface to include the MD5 value. - When reading clipboard content, calculate the MD5 value and check if there is already an item with the same MD5 value.
First, install the crypto-js
library:
npm install crypto-js
Then, update the ClipBoard.tsx
file:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
import CryptoJS from "crypto-js";
interface ClipboardItem {
content: string;
md5: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent) {
const md5 = CryptoJS.MD5(content).toString();
setClipboardItems((prevItems) => {
const existingItem = prevItems.find(item => item.md5 === md5);
if (existingItem) {
return prevItems.map(item =>
item.md5 === md5 ? { ...item, timestamp: Date.now() } : item
);
} else {
return [{ content, md5, timestamp: Date.now() }, ...prevItems];
}
});
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
const handleCopy = async (content: string) => {
await writeText(content);
};
const handleDelete = (timestamp: number) => {
setClipboardItems((prevItems) =>
prevItems.filter((item) => item.timestamp !== timestamp)
);
};
// Sort in reverse chronological order and limit to 100 entries
const sortedItems = clipboardItems.slice(0, 100);
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{sortedItems.map((item, index) => (
<li key={index}>
<small>{new Date(item.timestamp).toLocaleString()}</small>
<p>{item.content}</p>
<a href="#" onClick={() => handleCopy(item.content)}>Copy</a> |
<a href="#" onClick={() => handleDelete(item.timestamp)}>Delete</a>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
In this implementation, we:
- Installed and imported the
crypto-js
library to calculate the MD5 value. - Updated the
ClipboardItem
interface to include themd5
property. - When reading clipboard content, calculated the MD5 value and checked if there was an existing item with the same MD5 value. If there was, only the timestamp was updated; otherwise, a new record was added.
After updating the time for the same content, the list should be re-sorted#
GitHub Copilot: To ensure that after updating the time for the same content, the list is re-sorted, we need to sort the list after updating the timestamp. Here is the updated ClipBoard.tsx
file:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
import CryptoJS from "crypto-js";
interface ClipboardItem {
content: string;
md5: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent) {
const md5 = CryptoJS.MD5(content).toString();
setClipboardItems((prevItems) => {
const existingItem = prevItems.find(item => item.md5 === md5);
if (existingItem) {
const updatedItems = prevItems.map(item =>
item.md5 === md5 ? { ...item, timestamp: Date.now() } : item
);
return updatedItems.sort((a, b) => b.timestamp - a.timestamp);
} else {
const newItems = [{ content, md5, timestamp: Date.now() }, ...prevItems];
return newItems.slice(0, 100).sort((a, b) => b.timestamp - a.timestamp);
}
});
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
const handleCopy = async (content: string) => {
await writeText(content);
};
const handleDelete = (timestamp: number) => {
setClipboardItems((prevItems) =>
prevItems.filter((item) => item.timestamp !== timestamp)
);
};
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{clipboardItems.map((item, index) => (
<li key={index}>
<small>{new Date(item.timestamp).toLocaleString()}</small>
<p>{item.content}</p>
<a href="#" onClick={() => handleCopy(item.content)}>Copy</a> |
<a href="#" onClick={() => handleDelete(item.timestamp)}>Delete</a>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
In this implementation, we ensured that after updating the timestamp, the list is sorted. We also ensured that when adding new content, the list is sorted and limited to 100 entries.
Thus, when the same content's timestamp is updated, the list will be re-sorted.
After sorting, the page should refresh to display the new order#
Note
After sorting, the page did not refresh, so a forced refresh was required.
GitHub Copilot: To ensure that after sorting, the page refreshes to display the new order, we need to ensure that the sorted list is used in the rendering. Here is the updated ClipBoard.tsx
file:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
import CryptoJS from "crypto-js";
interface ClipboardItem {
content: string;
md5: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent) {
const md5 = CryptoJS.MD5(content).toString();
setClipboardItems((prevItems) => {
const existingItem = prevItems.find(item => item.md5 === md5);
if (existingItem) {
const updatedItems = prevItems.map(item =>
item.md5 === md5 ? { ...item, timestamp: Date.now() } : item
);
return updatedItems.sort((a, b) => b.timestamp - a.timestamp);
} else {
const newItems = [{ content, md5, timestamp: Date.now() }, ...prevItems];
return newItems.slice(0, 100).sort((a, b) => b.timestamp - a.timestamp);
}
});
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
const handleCopy = async (content: string) => {
await writeText(content);
};
const handleDelete = (timestamp: number) => {
setClipboardItems((prevItems) =>
prevItems.filter((item) => item.timestamp !== timestamp)
);
};
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{clipboardItems.map((item, index) => (
<li key={index}>
<small>{new Date(item.timestamp).toLocaleString()}</small>
<p>{item.content}</p>
<a href="#" onClick={() => handleCopy(item.content)}>Copy</a> |
<a href="#" onClick={() => handleDelete(item.timestamp)}>Delete</a>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
In this implementation, we ensured that the sorted list is used in rendering, so when the list is re-sorted, the page will refresh to display the new order.
Alternate rows should be distinguished by different background colors#
Note
UI optimization.
GitHub Copilot: To achieve alternating rows with different background colors, we can use CSS styles to implement a striped effect. Here is the updated ClipBoard.tsx
file and the corresponding CSS styles:
First, update the ClipBoard.tsx
file:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
import CryptoJS from "crypto-js";
import "./ClipBoard.css"; // Import CSS file
interface ClipboardItem {
content: string;
md5: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent) {
const md5 = CryptoJS.MD5(content).toString();
setClipboardItems((prevItems) => {
const existingItem = prevItems.find(item => item.md5 === md5);
if (existingItem) {
const updatedItems = prevItems.map(item =>
item.md5 === md5 ? { ...item, timestamp: Date.now() } : item
);
return updatedItems.sort((a, b) => b.timestamp - a.timestamp);
} else {
const newItems = [{ content, md5, timestamp: Date.now() }, ...prevItems];
return newItems.slice(0, 100).sort((a, b) => b.timestamp - a.timestamp);
}
});
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
const handleCopy = async (content: string) => {
await writeText(content);
};
const handleDelete = (timestamp: number) => {
setClipboardItems((prevItems) =>
prevItems.filter((item) => item.timestamp !== timestamp)
);
};
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{clipboardItems.map((item, index) => (
<li key={index} className={index % 2 === 0 ? "even" : "odd"}>
<small>{new Date(item.timestamp).toLocaleString()}</small>
<p>{item.content}</p>
<a href="#" onClick={() => handleCopy(item.content)}>Copy</a> |
<a href="#" onClick={() => handleDelete(item.timestamp)}>Delete</a>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
Then, create or update the ClipBoard.css
file and add the following styles:
/* ClipBoard.css */
ul {
list-style-type: none;
padding: 0;
}
li.even {
background-color: #f9f9f9;
}
li.odd {
background-color: #e9e9e9;
}
li {
padding: 10px;
margin: 5px 0;
}
a {
margin-right: 10px;
}
In this implementation, we:
- Imported the
ClipBoard.css
file inClipBoard.tsx
. - Used the
className
property to add different CSS classes (even
andodd
) to alternating rows. - Defined the background colors for the
even
andodd
classes in theClipBoard.css
file.
Thus, alternating rows will be distinguished by different background colors.
Change the background color of the content to a dark tone and adjust the text to a high-contrast color#
GitHub Copilot: To change the background color of the content to a dark tone and adjust the text to a high-contrast color, we can update the styles in the ClipBoard.css
file. Here is the updated ClipBoard.css
file:
/* ClipBoard.css */
ul {
list-style-type: none;
padding: 0;
}
li.even {
background-color: #333; /* Dark tone background */
color: #fff; /* High contrast text color */
}
li.odd {
background-color: #444; /* Dark tone background */
color: #fff; /* High contrast text color */
}
li {
padding: 10px;
margin: 5px 0;
}
a {
margin-right: 10px; /* High contrast link color */
}
a:hover {
text-decoration: underline;
}
In this implementation, we:
- Set the background colors of
li.even
andli.odd
to#333
and#444
, respectively, to achieve a dark tone background. - Set the text color of
li.even
andli.odd
to#fff
for high contrast. - Set the link color to
#1e90ff
for high contrast and added an underline on hover.
Thus, the background color of the content will be adjusted to a dark tone, and the text will be adjusted to a high-contrast color.
Merge the time and operation buttons into one line#
GitHub Copilot: To merge the time and operation buttons into one line, we can use CSS Flexbox layout. Here is the updated ClipBoard.tsx
file and the corresponding CSS styles:
First, update the ClipBoard.tsx
file:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
import CryptoJS from "crypto-js";
import "./ClipBoard.css"; // Import CSS file
interface ClipboardItem {
content: string;
md5: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent) {
const md5 = CryptoJS.MD5(content).toString();
setClipboardItems((prevItems) => {
const existingItem = prevItems.find(item => item.md5 === md5);
if (existingItem) {
const updatedItems = prevItems.map(item =>
item.md5 === md5 ? { ...item, timestamp: Date.now() } : item
);
return updatedItems.sort((a, b) => b.timestamp - a.timestamp);
} else {
const newItems = [{ content, md5, timestamp: Date.now() }, ...prevItems];
return newItems.slice(0, 100).sort((a, b) => b.timestamp - a.timestamp);
}
});
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
const handleCopy = async (content: string) => {
await writeText(content);
};
const handleDelete = (timestamp: number) => {
setClipboardItems((prevItems) =>
prevItems.filter((item) => item.timestamp !== timestamp)
);
};
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{clipboardItems.map((item, index) => (
<li key={index} className={index % 2 === 0 ? "even" : "odd"}>
<div className="item-header">
<small>{new Date(item.timestamp).toLocaleString()}</small>
<div>
<a href="#" onClick={() => handleCopy(item.content)}>Copy</a> |
<a href="#" onClick={() => handleDelete(item.timestamp)}>Delete</a>
</div>
</div>
<p>{item.content}</p>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
Then, update the ClipBoard.css
file and add the following styles:
/* ClipBoard.css */
ul {
list-style-type: none;
padding: 0;
}
li.even {
background-color: #333; /* Dark tone background */
color: #fff; /* High contrast text color */
}
li.odd {
background-color: #444; /* Dark tone background */
color: #fff; /* High contrast text color */
}
li {
padding: 10px;
margin: 5px 0;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
}
a {
margin-right: 10px; /* High contrast link color */
}
a:hover {
text-decoration: underline;
}
In this implementation, we:
- Placed the time and operation buttons in a
div
container with theitem-header
class. - Defined the
item-header
class inClipBoard.css
to use Flexbox layout to merge the time and operation buttons into one line and align them at both ends.
Thus, the time and operation buttons will be merged into one line.
Save the current content list and time information to the history.json file under the appdata directory at 1-minute intervals#
Note
Persistent saving of history records. The read/write APIs here also output in v1 format, so the API call method needs to be manually adjusted to v2 format. You can Ctrl+click the function name in VSCode's Tauri plugin to open the API source code, where the comments provide calling references.
GitHub Copilot: To save the current content list and time information to the history.json
file under the appdata
directory at 1-minute intervals, we need the following steps:
- Use the
fs
module to write to the file. - Use
setInterval
to save the content list at regular intervals. - Ensure to clear the timer when the component unmounts.
Here is the updated ClipBoard.tsx
file:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
import { writeTextFile, BaseDirectory } from "@tauri-apps/api/fs";
import CryptoJS from "crypto-js";
import "./ClipBoard.css"; // Import CSS file
interface ClipboardItem {
content: string;
md5: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const loadHistory = async () => {
try {
const data = await readTextFile("history.json", { dir: BaseDirectory.AppData });
const items: ClipboardItem[] = JSON.parse(data);
setClipboardItems(items);
} catch (error) {
console.error("Failed to load history:", error);
}
};
loadHistory();
}, []);
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent) {
const md5 = CryptoJS.MD5(content).toString();
setClipboardItems((prevItems) => {
const existingItem = prevItems.find(item => item.md5 === md5);
if (existingItem) {
const updatedItems = prevItems.map(item =>
item.md5 === md5 ? { ...item, timestamp: Date.now() } : item
);
return updatedItems.sort((a, b) => b.timestamp - a.timestamp);
} else {
const newItems = [{ content, md5, timestamp: Date.now() }, ...prevItems];
return newItems.slice(0, 100).sort((a, b) => b.timestamp - a.timestamp);
}
});
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
useEffect(() => {
const saveInterval = setInterval(async () => {
const data = JSON.stringify(clipboardItems, null, 2);
await writeTextFile({ path: "history.json", contents: data }, { dir: BaseDirectory.AppData });
}, 60000); // 1-minute interval
return () => clearInterval(saveInterval);
}, [clipboardItems]);
const handleCopy = async (content: string) => {
await writeText(content);
};
const handleDelete = (timestamp: number) => {
setClipboardItems((prevItems) =>
prevItems.filter((item) => item.timestamp !== timestamp)
);
};
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{clipboardItems.map((item, index) => (
<li key={index} className={index % 2 === 0 ? "even" : "odd"}>
<div className="item-header">
<small>{new Date(item.timestamp).toLocaleString()}</small>
<div>
<a href="#" onClick={() => handleCopy(item.content)}>Copy</a> |
<a href="#" onClick={() => handleDelete(item.timestamp)}>Delete</a>
</div>
</div>
<p>{item.content}</p>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
In this implementation, we:
- Used the
@tauri-apps/api/fs
module'swriteTextFile
function to write to the file. - Set up a timer using
useEffect
to save the current content list to thehistory.json
file every minute. - Cleared the timer when the component unmounts.
Thus, every minute, the current content list and time information will be saved to the history.json
file under the appdata
directory.
When first entering the ClipBoard page, read the history.json file from the AppData directory and parse the content as the clipboard history, displaying it on the interface#
GitHub Copilot: To read the history.json
file from the AppData
directory when first entering the ClipBoard page and parse the content as the clipboard history to display it on the interface, we need the following steps:
- Use the
fs
module to read the file. - Read the
history.json
file and parse the content when the component mounts. - Set the parsed content as the clipboard history.
Here is the updated ClipBoard.tsx
file:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
import { readTextFile, writeTextFile, BaseDirectory } from "@tauri-apps/plugin-fs";
import CryptoJS from "crypto-js";
import "./ClipBoard.css"; // Import CSS file
interface ClipboardItem {
content: string;
md5: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const loadHistory = async () => {
try {
const data = await readTextFile("history.json", { dir: BaseDirectory.AppData });
const items: ClipboardItem[] = JSON.parse(data);
setClipboardItems(items);
} catch (error) {
console.error("Failed to load history:", error);
}
};
loadHistory();
}, []);
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent) {
const md5 = CryptoJS.MD5(content).toString();
setClipboardItems((prevItems) => {
const existingItem = prevItems.find(item => item.md5 === md5);
if (existingItem) {
const updatedItems = prevItems.map(item =>
item.md5 === md5 ? { ...item, timestamp: Date.now() } : item
);
return updatedItems.sort((a, b) => b.timestamp - a.timestamp);
} else {
const newItems = [{ content, md5, timestamp: Date.now() }, ...prevItems];
return newItems.slice(0, 100).sort((a, b) => b.timestamp - a.timestamp);
}
});
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
useEffect(() => {
const saveInterval = setInterval(async () => {
const data = JSON.stringify(clipboardItems, null, 2);
await writeTextFile({ path: "history.json", contents: data }, { dir: BaseDirectory.AppData });
}, 60000); // 1-minute interval
return () => clearInterval(saveInterval);
}, [clipboardItems]);
const handleCopy = async (content: string) => {
await writeText(content);
};
const handleDelete = (timestamp: number) => {
setClipboardItems((prevItems) =>
prevItems.filter((item) => item.timestamp !== timestamp)
);
};
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{clipboardItems.map((item, index) => (
<li key={index} className={index % 2 === 0 ? "even" : "odd"}>
<div className="item-header">
<small>{new Date(item.timestamp).toLocaleString()}</small>
<div>
<a href="#" onClick={() => handleCopy(item.content)}>Copy</a> |
<a href="#" onClick={() => handleDelete(item.timestamp)}>Delete</a>
</div>
</div>
<p>{item.content}</p>
</li>
))}
</ul>
</div>
);
}
export default ClipBoard;
In this implementation, we:
- Used the
@tauri-apps/plugin-fs
module'sreadTextFile
function to read the file. - Read the
history.json
file and parsed the content when the component mounted. - Set the parsed content as the clipboard history and displayed it on the interface.
Thus, when first entering the ClipBoard page, the history.json
file will be read from the AppData
directory, and the content will be parsed as the clipboard history displayed on the interface.
Add exception handling for mapping ClipboardItems to interface elements, ignoring invalid data#
Note
When first opened, there was no history.json file, and after manually creating and building an empty JSON, an exception was thrown. Finally, after manually adding code to check if items are a valid array before assigning, it passed.
GitHub Copilot: To add exception handling for mapping clipboardItems
to interface elements and ignore invalid data, we can add error handling logic when rendering the list items. Here is the updated ClipBoard.tsx
file:
// Features/ClipBoard.tsx
import React, { useState, useEffect } from "react";
import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
import { readTextFile, writeTextFile, BaseDirectory } from "@tauri-apps/plugin-fs";
import CryptoJS from "crypto-js";
import "./ClipBoard.css"; // Import CSS file
interface ClipboardItem {
content: string;
md5: string;
timestamp: number;
}
function ClipBoard() {
const [clipboardItems, setClipboardItems] = useState<ClipboardItem[]>([]);
const [lastContent, setLastContent] = useState<string>("");
useEffect(() => {
const loadHistory = async () => {
try {
const data = await readTextFile("history.json", { dir: BaseDirectory.AppData });
const items: ClipboardItem[] = JSON.parse(data);
setClipboardItems(items);
} catch (error) {
console.error("Failed to load history:", error);
}
};
loadHistory();
}, []);
useEffect(() => {
const interval = setInterval(async () => {
const content = await readText();
if (content && content !== lastContent) {
const md5 = CryptoJS.MD5(content).toString();
setClipboardItems((prevItems) => {
const existingItem = prevItems.find(item => item.md5 === md5);
if (existingItem) {
const updatedItems = prevItems.map(item =>
item.md5 === md5 ? { ...item, timestamp: Date.now() } : item
);
return updatedItems.sort((a, b) => b.timestamp - a.timestamp);
} else {
const newItems = [{ content, md5, timestamp: Date.now() }, ...prevItems];
return newItems.slice(0, 100).sort((a, b) => b.timestamp - a.timestamp);
}
});
setLastContent(content);
}
}, 1000);
return () => clearInterval(interval);
}, [lastContent]);
useEffect(() => {
const saveInterval = setInterval(async () => {
const data = JSON.stringify(clipboardItems, null, 2);
await writeTextFile({ path: "history.json", contents: data }, { dir: BaseDirectory.AppData });
}, 60000); // 1-minute interval
return () => clearInterval(saveInterval);
}, [clipboardItems]);
const handleCopy = async (content: string) => {
await writeText(content);
};
const handleDelete = (timestamp: number) => {
setClipboardItems((prevItems) =>
prevItems.filter((item) => item.timestamp !== timestamp)
);
};
return (
<div>
<h2>ClipBoard Page</h2>
<ul>
{clipboardItems.map((item, index) => {
try {
if (!item.content || !item.timestamp) {
throw new Error("Invalid item");
}
return (
<li key={index} className={index % 2 === 0 ? "even" : "odd"}>
<div className="item-header">
<small>{new Date(item.timestamp).toLocaleString()}</small>
<div>
<a href="#" onClick={() => handleCopy(item.content)}>Copy</a> |
<a href="#" onClick={() => handleDelete(item.timestamp)}>Delete</a>
</div>
</div>
<p>{item.content}</p>
</li>
);
} catch (error) {
console.error("Invalid clipboard item:", error);
return null;
}
})}
</ul>
</div>
);
}
export default ClipBoard;
In this implementation, we:
- Added error handling logic in the
clipboardItems.map
function. - Checked if each
item
contains validcontent
andtimestamp
properties. - If the
item
is invalid, an error is thrown and logged to the console, andnull
is returned to ignore invalid data.
Thus, when mapping clipboardItems
to interface elements, invalid data will be ignored, and error information will be logged to the console.
List only the previous commands issued, without showing code updates#
Note
This summary only provided functional adjustment commands, ignoring project structure and UI optimization commands.
GitHub Copilot:
- Create a standalone
Home
component. - Move
Feature1
into a separate module under theFeatures
subdirectory. - Rename
Feature1
toClipBoard
. - Display the most recent clipboard content, sorted in reverse chronological order, showing a maximum of 100 entries.
- Ensure
greetMsg
is displayed on a new line. - Read the clipboard every second, and if there is new content, add it to the clipboard content list.
- Save both the content text and the MD5 value of the content in the list; when new content arrives, if the MD5 value matches the existing content text, only update the time, rather than adding a new record.
- Ensure the time and operation buttons are merged into one line.
- Save the current content list and time information to the
history.json
file under theappdata
directory at 1-minute intervals. - When first entering the ClipBoard page, read the
history.json
file from theAppData
directory and parse the content as the clipboard history, displaying it on the interface. - Add exception handling for mapping
ClipboardItems
to interface elements, ignoring invalid data.