wingsfly

wingsfly

Observer

Using Copilot to Guide Tauri Development

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.


Note

Here, 'App.tsx' is referenced.


GitHub Copilot: To achieve this functionality, we need to perform the following steps in App.tsx:

  1. Create a navigation bar component that displays page links for different functions in a tree view.
  2. Use React Router to handle page navigation.
  3. 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:

  1. Create a new file Home.tsx.
  2. Move the Home component code to the Home.tsx file.
  3. Import the Home component in App.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:

  1. Modify the App.css file to ensure the navigation bar occupies the full height of the page.
  2. 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:

  1. Ensure the container's height fills the page, and both the navigation bar and content area start aligned from the top.
  2. 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:

  1. Ensure .container uses flex layout, and the child elements are arranged left-right.
  2. 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:

  1. Create a new directory Features.
  2. In the Features directory, create a new file Feature1.tsx.
  3. Move the Feature1 component code to the Feature1.tsx file.
  4. Import the Feature1 component in App.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:

  1. In the Features directory, create a new file Feature2.tsx.
  2. Move the Feature2 component code to the Feature2.tsx file.
  3. Import the Feature2 component in App.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:

  1. Rename the file Feature1.tsx to Features/ClipBoard.tsx.
  2. Update the component name to ClipBoard.
  3. Import the ClipBoard component in App.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:

  1. Create a state to store clipboard content.
  2. Use the useEffect hook to simulate fetching clipboard content.
  3. 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:

  1. Defined a ClipboardItem interface to represent clipboard content items.
  2. Used the useState hook to store clipboard content.
  3. Used the useEffect hook to simulate fetching clipboard content and store it in the state.
  4. Sorted the clipboard content in reverse chronological order and limited the display to 100 entries.
  5. 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:

  1. Used the useState hook to store clipboard content and the last read content.
  2. Used the useEffect hook to set up a timer that reads the clipboard content every second.
  3. 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.
  4. Sorted the clipboard content in reverse chronological order and limited the display to 100 entries.
  5. 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.



GitHub Copilot: To implement providing "Copy" and "Delete" text links for each content item, we need the following steps:

  1. Add "Copy" and "Delete" buttons.
  2. Implement the copy functionality to re-copy the content to the clipboard.
  3. 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:

  1. Added "Copy" and "Delete" buttons.
  2. Implemented the handleCopy function to re-copy the content to the clipboard.
  3. Implemented the handleDelete function to remove the item from the list.
  4. 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:

  1. Install the crypto-js library to calculate the MD5 value.
  2. Modify the ClipboardItem interface to include the MD5 value.
  3. 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:

  1. Installed and imported the crypto-js library to calculate the MD5 value.
  2. Updated the ClipboardItem interface to include the md5 property.
  3. 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:

  1. Imported the ClipBoard.css file in ClipBoard.tsx.
  2. Used the className property to add different CSS classes (even and odd) to alternating rows.
  3. Defined the background colors for the even and odd classes in the ClipBoard.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:

  1. Set the background colors of li.even and li.odd to #333 and #444, respectively, to achieve a dark tone background.
  2. Set the text color of li.even and li.odd to #fff for high contrast.
  3. 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:

  1. Placed the time and operation buttons in a div container with the item-header class.
  2. Defined the item-header class in ClipBoard.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:

  1. Use the fs module to write to the file.
  2. Use setInterval to save the content list at regular intervals.
  3. 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:

  1. Used the @tauri-apps/api/fs module's writeTextFile function to write to the file.
  2. Set up a timer using useEffect to save the current content list to the history.json file every minute.
  3. 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:

  1. Use the fs module to read the file.
  2. Read the history.json file and parse the content when the component mounts.
  3. 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:

  1. Used the @tauri-apps/plugin-fs module's readTextFile function to read the file.
  2. Read the history.json file and parsed the content when the component mounted.
  3. 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:

  1. Added error handling logic in the clipboardItems.map function.
  2. Checked if each item contains valid content and timestamp properties.
  3. If the item is invalid, an error is thrown and logged to the console, and null 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:

  1. Create a standalone Home component.
  2. Move Feature1 into a separate module under the Features subdirectory.
  3. Rename Feature1 to ClipBoard.
  4. Display the most recent clipboard content, sorted in reverse chronological order, showing a maximum of 100 entries.
  5. Ensure greetMsg is displayed on a new line.
  6. Read the clipboard every second, and if there is new content, add it to the clipboard content list.
  7. 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.
  8. Ensure the time and operation buttons are merged into one line.
  9. Save the current content list and time information to the history.json file under the appdata directory at 1-minute intervals.
  10. 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.
  11. Add exception handling for mapping ClipboardItems to interface elements, ignoring invalid data.

Final Effect Picture#

image


Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.