diff --git a/README.md b/README.md index 0e796d3..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,214 +0,0 @@ -# Tauri + Next.js Template - -![Tauri window screenshot](public/tauri-nextjs-template_screenshot.png) - -This is a [Tauri](https://tauri.app/) project template using [Next.js](https://nextjs.org/), -bootstrapped by combining [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) -and [`create tauri-app`](https://tauri.app/v1/guides/getting-started/setup). - -This template uses [`pnpm`](https://pnpm.io/) as the Node.js dependency -manager. - -## Template Features - -- TypeScript frontend using Next.js React framework -- [TailwindCSS](https://tailwindcss.com/) as a utility-first atomic CSS framework - - The example page in this template app has been updated to use only TailwindCSS - - While not included by default, consider using - [Radix UI Primitives](https://www.radix-ui.com/) and/or - [HeadlessUI components](https://headlessui.com/) for completely unstyled and fully - accessible UI components, which integrate nicely with TailwindCSS -- Opinionated formatting and linting already setup and enabled - - [ESLint](https://eslint.org/) + [Prettier](https://prettier.io/) for frontend, - [clippy](https://github.com/rust-lang/rust-clippy) and - [rustfmt](https://github.com/rust-lang/rustfmt) for Rust code -- GitHub Actions to check code formatting and linting for both TypeScript and Rust - -## Getting Started - -### Running development server and use Tauri window - -After cloning for the first time, set up git pre-commit hooks: - -```shell -pnpm prepare -``` - -To develop and run the frontend in a Tauri window: - -```shell -pnpm dev -``` - -This will load the Next.js frontend directly in a Tauri webview window, in addition to -starting a development server on `localhost:3000`. - -### Building for release - -To export the Next.js frontend via SSG and build the Tauri application for release: - -```shell -pnpm build -``` - -Please remember to change the bundle identifier in -`tauri.conf.json > tauri > bundle > identifier`, as the default value will yield an -error that prevents you from building the application for release. - -### Source structure - -Next.js frontend source files are located in `src/` and Tauri Rust application source -files are located in `src-tauri/`. Please consult the Next.js and Tauri documentation -respectively for questions pertaining to either technology. - -## Caveats - -### Static Site Generation / Pre-rendering - -Next.js is a great React frontend framework which supports server-side rendering (SSR) -as well as static site generation (SSG or pre-rendering). For the purposes of creating a -Tauri frontend, only SSG can be used since SSR requires an active Node.js server. - -Using Next.js and SSG helps to provide a quick and performant single-page-application -(SPA) frontend experience. More information regarding this can be found here: -https://nextjs.org/docs/basic-features/pages#pre-rendering - -### `next/image` - -The [`next/image` component](https://nextjs.org/docs/basic-features/image-optimization) -is an enhancement over the regular `` HTML element with additional optimizations -built in. However, because we are not deploying the frontend onto Vercel directly, some -optimizations must be disabled to properly build and export the frontend via SSG. -As such, the -[`unoptimized` property](https://nextjs.org/docs/api-reference/next/image#unoptimized) -is set to true for the `next/image` component in the `next.config.js` configuration. -This will allow the image to be served as-is from source, without -changes to its quality, size, or format. - -### error[E0554]: `#![feature]` may not be used on the stable release channel - -If you are getting this issue when trying to run `pnpm tauri dev`, it may be that you -have a newer version of a Rust dependency that uses an unstable feature. -`pnpm tauri build` should still work for production builds, but to get the dev command -working, either downgrade the dependency or use Rust nightly via -`rustup override set nightly`. - -### ReferenceError: navigator is not defined - -If you are using Tauri's `invoke` function or any OS related Tauri function from within -JavaScript, you may encounter this error when importing the function in a global, -non-browser context. This is due to the nature of Next.js' dev server effectively -running a Node.js server for SSR and hot module replacement (HMR), and Node.js does not -have a notion of `window` or `navigator`. - -#### Solution 1 - Dependency Injection (may not always work) - -Make sure that you are calling these functions within the browser context, e.g. within a -React component inside a `useEffect` hook when the DOM actually exists by then. If you -are trying to use a Tauri function in a generalized utility source file, a workaround is -to use dependency injection for the function itself to delay the actual importing of the -real function (see example below for more info). - -Example using Tauri's `invoke` function: - -`src/lib/some_tauri_functions.ts` (problematic) - -```typescript -// Generalized file containing all the invoke functions we need to fetch data from Rust -import { invoke } from "@tauri-apps/api/tauri" - -const loadFoo = (): Promise => { - return invoke("invoke_handler_foo") -} - -const loadBar = (): Promise => { - return invoke("invoke_handler_bar") -} - -const loadBaz = (): Promise => { - return invoke("invoke_handler_baz") -} - -// and so on ... -``` - -`src/lib/some_tauri_functions.ts` (fixed) - -```typescript -// Generalized file containing all the invoke functions we need to fetch data from Rust -// -// We apply the idea of dependency injection to use a supplied invoke function as a -// function argument, rather than directly referencing the Tauri invoke function. -// Hence, don't import invoke globally in this file. -// -// import { invoke } from "@tauri-apps/api/tauri" <-- remove this! -// - -import { InvokeArgs } from "@tauri-apps/api/tauri" -type InvokeFunction = (cmd: string, args?: InvokeArgs | undefined) => Promise - -const loadFoo = (invoke: InvokeFunction): Promise => { - return invoke("invoke_handler_foo") -} - -const loadBar = (invoke: InvokeFunction): Promise => { - return invoke("invoke_handler_bar") -} - -const loadBaz = (invoke: InvokeFunction): Promise => { - return invoke("invoke_handler_baz") -} - -// and so on ... -``` - -Then, when using `loadFoo`/`loadBar`/`loadBaz` within your React components, import the -invoke function from `@tauri-apps/api` and pass `invoke` into the loadXXX function as -the `InvokeFunction` argument. This should allow the actual Tauri API to be bundled -only within the context of a React component, so it should not be loaded by Next.js upon -initial startup until the browser has finished loading the page. - -#### Solution 2: Wrap Tauri API behind dynamic `import()` - -Since the Tauri API needs to read from the browser's `window` and `navigator` object, -this data does not exist in a Node.js and hence SSR environment. One can create an -exported function that wraps the Tauri API behind a dynamic runtime `import()` call. - -Example: create a `src/lib/tauri.ts` to re-export `invoke` - -```typescript -import type { InvokeArgs } from "@tauri-apps/api/tauri" - -const isNode = (): boolean => - Object.prototype.toString.call(typeof process !== "undefined" ? process : 0) === - "[object process]" - -export async function invoke( - cmd: string, - args?: InvokeArgs | undefined, -): Promise { - if (isNode()) { - // This shouldn't ever happen when React fully loads - return Promise.resolve(undefined as unknown as T) - } - const tauriAppsApi = await import("@tauri-apps/api") - const tauriInvoke = tauriAppsApi.invoke - return tauriInvoke(cmd, args) -} -``` - -Then, instead of importing `import { invoke } from "@tauri-apps/api/tauri"`, use invoke -from `import { invoke } from "@/lib/tauri"`. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and - API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -And to learn more about Tauri, take a look at the following resources: - -- [Tauri Documentation - Guides](https://tauri.app/v1/guides/) - learn about the Tauri - toolkit. diff --git a/package.json b/package.json index 0d603b4..feb1ee3 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "email": "your-email-here@example.com" }, "scripts": { - "next-start": "cross-env BROWSER=none next dev", + "next-start": "cross-env BROWSER=none NODE_OPTIONS='--inspect' next dev", "next-build": "next build", "tauri": "tauri", "build": "tauri build", @@ -21,7 +21,8 @@ "next": "^14.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "tauri-plugin-clipboard-api": "^0.6.0" + "tauri-plugin-clipboard-api": "^0.6.0", + "zustand": "^4.5.0" }, "devDependencies": { "@prettier/plugin-xml": "^3.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb00e77..a740587 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ dependencies: tauri-plugin-clipboard-api: specifier: ^0.6.0 version: 0.6.0 + zustand: + specifier: ^4.5.0 + version: 4.5.0(@types/react@18.2.53)(react@18.2.0) devDependencies: '@prettier/plugin-xml': @@ -668,7 +671,6 @@ packages: /@types/prop-types@15.7.11: resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} - dev: true /@types/react-dom@18.2.18: resolution: {integrity: sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==} @@ -682,11 +684,9 @@ packages: '@types/prop-types': 15.7.11 '@types/scheduler': 0.16.8 csstype: 3.1.3 - dev: true /@types/scheduler@0.16.8: resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} - dev: true /@types/semver@7.5.6: resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} @@ -1419,7 +1419,6 @@ packages: /csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - dev: true /damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -4317,6 +4316,14 @@ packages: punycode: 2.3.1 dev: true + /use-sync-external-store@1.2.0(react@18.2.0): + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true @@ -4425,3 +4432,23 @@ packages: /zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} dev: false + + /zustand@4.5.0(@types/react@18.2.53)(react@18.2.0): + resolution: {integrity: sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + dependencies: + '@types/react': 18.2.53 + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false diff --git a/src/pages/index.tsx b/src/pages/index.tsx index dc86e87..8db2718 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,4 +1,6 @@ +import { useClipStore } from "@/store/clips.store"; import { UnlistenFn } from "@tauri-apps/api/event"; +import _ from "lodash"; import type { NextPage } from "next" import { useEffect, useState } from "react" import { @@ -11,33 +13,22 @@ import { } from "tauri-plugin-clipboard-api"; const Home: NextPage = () => { const [copiedText, setCopiedText] = useState("Copied text will be here"); + const { updateClips, clips } = useClipStore() let unlistenTextUpdate: UnlistenFn; - let unlistenImageUpdate: UnlistenFn; let unlistenClipboard: () => Promise; - let unlistenFiles: UnlistenFn; let monitorRunning = false; useEffect(() => { + const debouncedUpdateClips = _.debounce((newText) => { + updateClips(newText); + }, 300); const unlistenFunctions = async () => { unlistenTextUpdate = await onTextUpdate((newText) => { - console.log(newText); - setCopiedText(newText); - - }); - unlistenImageUpdate = await onImageUpdate((_) => { - console.log("Image updated"); - }); - unlistenFiles = await onFilesUpdate((_) => { - console.log("Files updated"); + console.log("new text::"); + debouncedUpdateClips(newText); }); unlistenClipboard = await startListening(); - - onClipboardUpdate(() => { - console.log( - "plugin:clipboard://clipboard-monitor/update event received" - ); - }); }; listenToMonitorStatusUpdate((running) => { @@ -49,33 +40,23 @@ const Home: NextPage = () => { if (unlistenTextUpdate) { unlistenTextUpdate(); } - if (unlistenImageUpdate) { - unlistenImageUpdate(); - } - if (unlistenClipboard) { - unlistenClipboard(); - } - if (unlistenFiles) { - unlistenFiles(); - } - console.log(monitorRunning); }; }, []); return ( -
-
- {/* { - clips.map((clip, index) => { +
+
+ { + clips.reverse().map((clip, index) => { return ( -
-

{copiedText || 'N/A'}

+
+

{clip}

) } - )} */} -

{copiedText}

+ ) + }
) diff --git a/src/store/clips.store.ts b/src/store/clips.store.ts new file mode 100644 index 0000000..56c2097 --- /dev/null +++ b/src/store/clips.store.ts @@ -0,0 +1,23 @@ +import { create } from 'zustand'; + +export interface ClipsStore { + clips: string[]; + updateClips: (clip: string) => void; +} + +const initialState: ClipsStore = { + clips: [], + updateClips: () => { } +} +export const useClipStore = create((set) => ({ + ...initialState, + updateClips: (clip) => { + set((state) => { + // Only add the clip if it's not already in the array + if (!state.clips.includes(clip)) { + return { clips: [...state.clips, clip] }; + } + return state; + }); + }, +}));