init commit
Some checks are pending
Lint Node.js / build (windows-latest) (push) Waiting to run
Lint Node.js / build (macos-latest) (push) Waiting to run
Lint Node.js / build (ubuntu-latest) (push) Waiting to run
Lint Rust / build (macos-latest) (push) Waiting to run
Lint Rust / build (ubuntu-latest) (push) Waiting to run
Lint Rust / build (windows-latest) (push) Waiting to run

This commit is contained in:
purp1e
2024-09-20 02:28:25 +08:00
commit e406a19c08
51 changed files with 11575 additions and 0 deletions

29
.eslintrc.json Normal file
View File

@@ -0,0 +1,29 @@
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/strict-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked",
"next/core-web-vitals"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": ["./tsconfig.json"]
},
"plugins": ["@typescript-eslint"],
"rules": {
"eqeqeq": "error",
"no-else-return": "error",
"no-implicit-coercion": ["error", { "disallowTemplateShorthand": true }],
"no-unneeded-ternary": "error",
"no-useless-call": "error",
"no-useless-computed-key": "error",
"no-useless-concat": "error",
"prefer-arrow-callback": "error",
"prefer-const": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "error",
"radix": ["error", "always"],
"react-hooks/exhaustive-deps": "error"
}
}

28
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
version: 2
updates:
# Enable version updates for Node.js dependencies
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
allow:
- dependency-type: "all"
groups:
all:
patterns:
- "*"
ignore:
- dependency-name: "eslint"
versions: ">= 9"
# Enable version updates for rust
- package-ecosystem: "cargo"
directory: "/src-tauri"
schedule:
interval: "weekly"
allow:
- dependency-type: "all"
groups:
all:
patterns:
- "*"

View File

@@ -0,0 +1,34 @@
# Automatically squashes and merges Dependabot dependency upgrades if tests pass
name: Dependabot Auto-merge
on: pull_request_target
permissions:
pull-requests: write
contents: write
jobs:
dependabot:
runs-on: ubuntu-latest
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Fetch Dependabot metadata
id: dependabot-metadata
uses: dependabot/fetch-metadata@v1.3.3
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Approve Dependabot PR
run: gh pr review --approve "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Auto-merge (squash) Dependabot PR
if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }}
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

46
.github/workflows/lint-js.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
# Installs Node.js dependencies and pnpm, and checks formatting + linting
name: Lint Node.js
on:
push:
branches:
- main
pull_request:
paths-ignore:
- "src-tauri/**"
- "README.md"
jobs:
build:
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Disable git core.autocrlf on Windows
if: matrix.os == 'windows-latest'
run: git config --global core.autocrlf false
- name: Checkout repository code
uses: actions/checkout@v4
- name: Set up pnpm package manager
uses: pnpm/action-setup@v4
with:
version: latest
- name: Set up Node.js v22
uses: actions/setup-node@v3
with:
node-version: 22
cache: "pnpm"
- name: Install dependencies from lockfile
run: pnpm install --frozen-lockfile
- name: Run lint step
run: pnpm lint

55
.github/workflows/lint-rs.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
# Installs Rust and checks formatting + linting
name: Lint Rust
on:
push:
branches:
- main
pull_request:
paths-ignore:
- "src/**"
- "package.json"
- "package-lock.json"
- "yarn.lock"
- "pnpm-lock.yaml"
- "README.md"
jobs:
build:
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Disable git core.autocrlf on Windows
if: matrix.os == 'windows-latest'
run: git config --global core.autocrlf false
- name: Checkout repository code
uses: actions/checkout@v3
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Install Linux dependencies
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt install libdbus-1-dev libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev
- name: Create empty 'out' directory
run: mkdir out
- name: Run rustfmt check
run: cargo fmt --all -- --check
working-directory: src-tauri
- name: Run clippy check and deny warnings
run: cargo clippy --all-targets --all-features -- -D warnings
working-directory: src-tauri

38
.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo

4
.husky/pre-commit Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm exec lint-staged

13
.lintstagedrc.js Normal file
View File

@@ -0,0 +1,13 @@
const path = require("path")
const buildEslintCommand = (filenames) =>
`next lint --fix --file ${filenames
.map((f) => path.relative(process.cwd(), f))
.join(" --file ")}`
module.exports = {
"src/**/*.{js,jsx,ts,tsx}": [buildEslintCommand],
"*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [
"biome check --apply --no-errors-on-unmatched", // Format, sort imports, lint, and apply safe fixes
],
}

1
.node-version Normal file
View File

@@ -0,0 +1 @@
22

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Kevin Xiao
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

216
README.md Normal file
View File

@@ -0,0 +1,216 @@
# 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
[React Aria components](https://react-spectrum.adobe.com/react-aria/index.html)
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/) for pure React + TypeScript linting, and
[Biome](https://biomejs.dev/) for a combination of fast formatting, linting, and
import sorting of JavaScript and TypeScript code
- [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 `<img>` 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<string> => {
return invoke<string>("invoke_handler_foo")
}
const loadBar = (): Promise<string> => {
return invoke<string>("invoke_handler_bar")
}
const loadBaz = (): Promise<string> => {
return invoke<string>("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 = <T>(cmd: string, args?: InvokeArgs | undefined) => Promise<T>
const loadFoo = (invoke: InvokeFunction): Promise<string> => {
return invoke<string>("invoke_handler_foo")
}
const loadBar = (invoke: InvokeFunction): Promise<string> => {
return invoke<string>("invoke_handler_bar")
}
const loadBaz = (invoke: InvokeFunction): Promise<string> => {
return invoke<string>("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<T>(
cmd: string,
args?: InvokeArgs | undefined,
): Promise<T> {
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.

24
biome.json Normal file
View File

@@ -0,0 +1,24 @@
{
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
},
"javascript": {
"formatter": {
"semicolons": "asNeeded",
"lineEnding": "lf",
"trailingComma": "all"
}
}
}

5
next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

11
next.config.js Normal file
View File

@@ -0,0 +1,11 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
images: {
unoptimized: true,
},
output: "export",
}
module.exports = nextConfig

59
package.json Normal file
View File

@@ -0,0 +1,59 @@
{
"name": "tauri-nextjs-template",
"version": "0.1.0",
"private": true,
"author": {
"name": "Your Name Here",
"email": "your-email-here@example.com"
},
"scripts": {
"next-start": "cross-env BROWSER=none next dev",
"next-build": "next build",
"tauri": "tauri",
"build": "tauri build",
"dev": "tauri dev",
"lint": "next lint && biome check src/",
"prepare": "husky"
},
"dependencies": {
"@tauri-apps/api": "^1.6.0",
"clsx": "^2.1.1",
"next": "^14.2.11",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@biomejs/biome": "^1.9.1",
"@tauri-apps/cli": "^1.6.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.13",
"@types/node": "^22.5.5",
"@types/react": "^18.3.6",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.5.0",
"@typescript-eslint/parser": "^8.5.0",
"autoprefixer": "^10.4.20",
"cross-env": "^7.0.3",
"cssnano": "^7.0.6",
"eslint": "^8.57.0",
"eslint-config-next": "^14.2.11",
"husky": "^9.1.6",
"lint-staged": "^15.2.10",
"postcss": "^8.4.47",
"postcss-import": "^16.1.0",
"postcss-nesting": "^13.0.0",
"tailwindcss": "^3.4.11",
"typescript": "^5.6.2"
},
"browserslist": {
"production": [">0.2%", "not dead", "not op_mini all"],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

5203
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

9
postcss.config.js Normal file
View File

@@ -0,0 +1,9 @@
module.exports = {
plugins: {
"postcss-import": {},
"tailwindcss/nesting": "postcss-nesting",
tailwindcss: {},
autoprefixer: {},
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}),
},
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -0,0 +1,21 @@
<svg width="430" height="134" fill="none" xmlns="http://www.w3.org/2000/svg"><path
d="M177.007 104.258c-.629 0-1.195-.219-1.698-.658-.441-.5-.661-1.064-.661-1.69V53.276H157.85c-.629 0-1.195-.22-1.699-.658-.44-.5-.66-1.064-.66-1.69V40.882c0-.688.22-1.252.66-1.69.504-.438 1.07-.657 1.699-.657h50.772c.692 0 1.258.22 1.699.657.44.438.66 1.002.66 1.69v10.046c0 .689-.22 1.252-.66 1.69-.441.439-1.007.658-1.699.658h-16.798v48.634c0 .626-.22 1.19-.661 1.69-.44.439-1.006.658-1.698.658h-12.458Zm32.049 0c-.504 0-.976-.188-1.416-.564-.377-.438-.566-.907-.566-1.408 0-.376.031-.657.094-.845l22.178-60.277a3.406 3.406 0 0 1 1.226-1.878c.629-.5 1.447-.751 2.454-.751h13.967c1.007 0 1.825.25 2.454.751a3.413 3.413 0 0 1 1.227 1.878l22.083 60.277.188.845c0 .501-.22.97-.66 1.408-.378.376-.85.564-1.416.564h-11.608c-1.51 0-2.516-.658-3.02-1.972l-3.68-9.577h-25.197l-3.586 9.577c-.504 1.314-1.542 1.972-3.115 1.972h-11.607Zm39.73-25.069-8.776-24.787-8.777 24.787h17.553Zm57.868 26.008c-8.87 0-15.823-2.16-20.856-6.479-4.97-4.319-7.455-10.828-7.455-19.529V40.882c0-.688.22-1.252.661-1.69.503-.438 1.069-.657 1.698-.657h12.174c.692 0 1.258.22 1.699.657.503.438.755 1.002.755 1.69v38.213c0 4.069.944 7.136 2.831 9.201 1.95 2.003 4.75 3.005 8.399 3.005 3.586 0 6.354-1.033 8.305-3.098 1.95-2.066 2.925-5.102 2.925-9.108V40.882c0-.688.221-1.252.661-1.69.503-.438 1.069-.657 1.699-.657h12.268c.692 0 1.258.22 1.699.657.44.438.66 1.002.66 1.69V79.19c0 8.7-2.516 15.21-7.549 19.53-4.971 4.318-11.828 6.478-20.574 6.478Zm42.997-.939c-.629 0-1.195-.219-1.699-.658-.44-.5-.66-1.064-.66-1.69V40.882c0-.688.22-1.252.66-1.69.504-.438 1.07-.657 1.699-.657h25.669c8.242 0 14.659 1.878 19.252 5.633 4.656 3.693 6.984 8.951 6.984 15.774 0 4.381-1.038 8.106-3.115 11.173-2.076 3.067-4.938 5.414-8.587 7.041l12.928 23.097c.189.376.284.72.284 1.033 0 .501-.221.97-.661 1.408-.377.376-.818.564-1.321.564h-12.552c-1.824 0-3.114-.845-3.869-2.535l-10.758-20.656h-9.626v20.843c0 .689-.252 1.252-.755 1.69-.441.439-1.007.658-1.699.658h-12.174Zm25.481-36.523c2.894 0 5.096-.689 6.606-2.066 1.51-1.44 2.265-3.411 2.265-5.915s-.755-4.475-2.265-5.915c-1.447-1.502-3.649-2.253-6.606-2.253h-10.853v16.149h10.853Zm39.768 36.523c-.629 0-1.195-.219-1.698-.658-.441-.5-.661-1.064-.661-1.69V40.882c0-.688.22-1.252.661-1.69.503-.438 1.069-.657 1.698-.657h12.646c.692 0 1.259.22 1.699.657.503.438.755 1.002.755 1.69v61.028c0 .689-.252 1.252-.755 1.69-.44.439-1.007.658-1.699.658H414.9ZM82.875 48.385c0 6.998-5.703 12.672-12.737 12.672-7.035 0-12.738-5.674-12.738-12.672 0-6.999 5.703-12.672 12.738-12.672 7.034 0 12.737 5.673 12.737 12.672Z"
fill="#2F2F2F"
/><ellipse
cx="48.716"
cy="84.673"
rx="12.737"
ry="12.672"
transform="rotate(180 48.716 84.673)"
fill="#2F2F2F"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M96.536 89.021a48.773 48.773 0 0 1-16.779 6.793 33.78 33.78 0 0 0 1.878-11.14c0-1.416-.087-2.811-.256-4.181a34.241 34.241 0 0 0 7.309-3.553 34.05 34.05 0 0 0 12.609-14.59 33.843 33.843 0 0 0 2.629-19.064 33.922 33.922 0 0 0-8.192-17.43 34.2 34.2 0 0 0-16.39-10.215 34.343 34.343 0 0 0-19.34.273A34.186 34.186 0 0 0 42.367 28.57a56.962 56.962 0 0 0-18.65 5.389 48.31 48.31 0 0 1 9.098-16.596A48.649 48.649 0 0 1 55.717 2.176a48.873 48.873 0 0 1 27.52-.388 48.67 48.67 0 0 1 23.326 14.537 48.28 48.28 0 0 1 11.658 24.804 48.163 48.163 0 0 1-3.741 27.129A48.46 48.46 0 0 1 96.536 89.02Zm-72.24-46.238 11.922 1.457a33.856 33.856 0 0 1 1.539-6.726 48.79 48.79 0 0 0-13.462 5.27Z"
fill="#2F2F2F"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M22.235 44.037a48.773 48.773 0 0 1 16.913-6.82 33.761 33.761 0 0 0-2.094 11.744c0 1.234.066 2.454.195 3.654a34.246 34.246 0 0 0-7.166 3.503 34.05 34.05 0 0 0-12.609 14.59 33.842 33.842 0 0 0-2.63 19.064 33.924 33.924 0 0 0 8.194 17.43 34.202 34.202 0 0 0 16.39 10.215 34.345 34.345 0 0 0 19.339-.272 34.19 34.19 0 0 0 17.645-12.667 56.948 56.948 0 0 0 18.658-5.426 48.315 48.315 0 0 1-9.114 16.643 48.648 48.648 0 0 1-22.902 15.187 48.871 48.871 0 0 1-27.52.388 48.67 48.67 0 0 1-23.325-14.537A48.275 48.275 0 0 1 .549 91.929 48.161 48.161 0 0 1 4.293 64.8a48.455 48.455 0 0 1 17.943-20.763Z"
fill="#2F2F2F"
/></svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

3
src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Generated by Cargo
# will have compiled files and executables
/target/

2
src-tauri/.rustfmt.toml Normal file
View File

@@ -0,0 +1,2 @@
tab_spaces = 4
max_width = 100

5385
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

28
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,28 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
default-run = "app"
edition = "2021"
rust-version = "1.66"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.5", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.8", features = ["api-all"] }
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
default = [ "custom-protocol" ]
# this feature is used used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = [ "tauri/custom-protocol" ]

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

23
src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,23 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
use std::time::{SystemTime, UNIX_EPOCH};
#[tauri::command]
fn on_button_clicked() -> String {
let start = SystemTime::now();
let since_the_epoch = start
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_millis();
format!("on_button_clicked called from Rust! (timestamp: {since_the_epoch}ms)")
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![on_button_clicked])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

66
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,66 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"build": {
"beforeBuildCommand": "pnpm next-build",
"beforeDevCommand": "pnpm next-start",
"devPath": "http://localhost:3000",
"distDir": "../out"
},
"package": {
"productName": "tauri-nextjs-template",
"version": "0.1.0"
},
"tauri": {
"allowlist": {
"all": true
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"deb": {
"depends": []
},
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "com.tauri.dev",
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"security": {
"csp": null
},
"updater": {
"active": false
},
"windows": [
{
"fullscreen": false,
"resizable": true,
"title": "Tauri NextJS App",
"width": 1280,
"height": 800
}
]
}
}

21
src/components/Card.tsx Normal file
View File

@@ -0,0 +1,21 @@
interface CardProps {
url: string
title: string
description: string
}
export const Card: React.FC<CardProps> = ({
url,
title,
description,
}: CardProps) => (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="m-4 max-w-xs rounded-xl border border-gray-200 p-6 text-left text-inherit transition-colors hover:border-blue-600 hover:text-blue-600 focus:border-blue-600 focus:text-blue-600 active:border-blue-600 active:text-blue-600"
>
<h2 className="mb-4 text-2xl">{title} &rarr;</h2>
<p className="m-0 text-xl">{description}</p>
</a>
)

View File

@@ -0,0 +1,20 @@
interface CardButtonProps {
onClick: () => void
title: string
description: string
}
export const CardButton: React.FC<CardButtonProps> = ({
onClick,
title,
description,
}: CardButtonProps) => (
<button
type="button"
onClick={onClick}
className="m-4 max-w-xs rounded-xl border border-gray-200 p-6 text-left text-inherit transition-colors hover:border-blue-600 hover:text-blue-600 focus:border-blue-600 focus:text-blue-600 active:border-blue-600 active:text-blue-600"
>
<h2 className="mb-4 text-2xl">{title} &rarr;</h2>
<p className="m-0 text-xl">{description}</p>
</button>
)

View File

@@ -0,0 +1,40 @@
import {
type ShortcutHandler,
isRegistered,
register,
unregister,
} from "@tauri-apps/api/globalShortcut"
import { useEffect } from "react"
/**
* A React hook to register global shortcuts using Tauri's globalShortcut API.
* Internally this uses a useEffect hook, but has proper support for React.StrictMode
* via cleaning up the effect hook, so as to maintain idempotency.
*
* @param shortcut The key combination string for the shortcut
* @param shortcutHandler The handler callback when the shortcut is triggered
*/
export const useGlobalShortcut = (
shortcut: string,
shortcutHandler: ShortcutHandler,
) => {
useEffect(() => {
let ignore = false
async function registerShortcut() {
const isShortcutRegistered = await isRegistered(shortcut)
if (!ignore && !isShortcutRegistered) {
await register(shortcut, shortcutHandler)
}
}
void registerShortcut().catch((err: unknown) => {
console.error(`Failed to register global shortcut '${shortcut}'`, err)
})
return () => {
ignore = true
void unregister(shortcut)
}
}, [shortcut, shortcutHandler])
}

8
src/pages/_app.tsx Normal file
View File

@@ -0,0 +1,8 @@
import "@/styles/globals.css"
import type { AppProps } from "next/app"
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
export default MyApp

108
src/pages/index.tsx Normal file
View File

@@ -0,0 +1,108 @@
import { Card } from "@/components/Card"
import { CardButton } from "@/components/CardButton"
import { useGlobalShortcut } from "@/hooks/tauri/shortcuts"
import { invoke } from "@tauri-apps/api/tauri"
import type { NextPage } from "next"
import Head from "next/head"
import Image from "next/image"
import { useCallback, useState } from "react"
const Home: NextPage = () => {
const [buttonDesc, setButtonDesc] = useState<string>(
"Waiting to be clicked. This calls 'on_button_clicked' from Rust.",
)
const onButtonClick = () => {
invoke<string>("on_button_clicked")
.then((value) => {
setButtonDesc(value)
})
.catch(() => {
setButtonDesc("Failed to invoke Rust command 'on_button_clicked'")
})
}
const shortcutHandler = useCallback(() => {
console.log("Ctrl+P was pressed!")
}, [])
useGlobalShortcut("CommandOrControl+P", shortcutHandler)
return (
<div className="flex min-h-screen flex-col bg-white">
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="flex flex-1 flex-col items-center justify-center py-8">
<h1 className="m-0 text-center text-6xl">
Welcome to{" "}
<a
href="https://nextjs.org"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline focus:underline active:underline"
>
Next.js!
</a>
</h1>
<p className="my-12 text-center text-2xl leading-9">
Get started by editing{" "}
<code className="rounded-md bg-gray-200 p-2 font-mono text-xl">
src/pages/index.tsx
</code>
</p>
<div className="flex max-w-3xl flex-wrap items-center justify-center">
<Card
url="https://nextjs.org/docs"
title="Documentation"
description="Find in-depth information about Next.js features and API."
/>
<Card
url="https://nextjs.org/learn"
title="Learn"
description="Learn about Next.js in an interactive course with quizzes!"
/>
<Card
url="https://github.com/vercel/next.js/tree/canary/examples"
title="Examples"
description="Discover and deploy boilerplate example Next.js projects."
/>
<CardButton
onClick={onButtonClick}
title="Tauri Invoke"
description={buttonDesc}
/>
</div>
</main>
<footer className="flex flex-1 flex-grow-0 items-center justify-center border-t border-gray-200 py-4">
<div>
<a
href="https://tauri.app/"
target="_blank"
rel="noopener noreferrer"
className="flex flex-grow items-center justify-center p-4"
>
Powered by{" "}
<span className="ml-2 h-6">
<Image
src="/tauri_logo_light.svg"
alt="Vercel Logo"
height={24}
width={78}
/>
</span>
</a>
</div>
</footer>
</div>
)
}
export default Home

20
src/styles/globals.css Normal file
View File

@@ -0,0 +1,20 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}

7
tailwind.config.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {},
},
plugins: [],
}

24
tsconfig.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "*.js"],
"exclude": ["node_modules", "src-tauri"]
}