初次提交,原始状态

This commit is contained in:
2025-08-16 19:20:25 +08:00
commit b0eed27371
111 changed files with 31996 additions and 0 deletions

63
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
name: release
on:
push:
branches:
- release
jobs:
publish-tauri:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: macos-latest # Arm based macs
args: --target aarch64-apple-darwin
- platform: macos-latest # Intel based macs
args: --target x86_64-apple-darwin
- platform: ubuntu-22.04
args: ""
- platform: windows-latest
args: ""
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v4
with:
node-version: 23.8.0
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.13.1
- name: install frontend dependencies
run: pnpm install
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tagName: nuxtor-v__VERSION__
releaseName: Nuxtor v__VERSION__
releaseBody: After installing the Apple app you have to run "xattr -c /Applications/Nuxtor.app" once before launching
releaseDraft: false
prerelease: false
args: ${{ matrix.args }}

12
.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
node_modules
.DS_Store
*.log*
.data
.nuxt
.nitro
.cache
.output
.env
.cargo
dist
src-tauri/target

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
shamefully-hoist=true
strict-peer-dependencies=false

2
.nuxtignore Normal file
View File

@@ -0,0 +1,2 @@
./*
!./app/**

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
23.8.0

9
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"antfu.file-nesting",
"antfu.iconify",
"vue.volar",
"bradlc.vscode-tailwindcss"
]
}

49
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,49 @@
{
// Enable the ESlint flat config support
"eslint.useFlatConfig": true,
// Use eslint as default formatter
"editor.formatOnSave": false,
// Tailwind compatibility
"tailwindCSS.validate": true,
"tailwindCSS.experimental.classRegex": [
[
"ui:\\s*{([^)]*)\\s*}",
"[\"'`]([^\"'`]*).*?[\"'`]"
],
[
"/\\*\\s?ui\\s?\\*/\\s*{([^;]*)}",
":\\s*[\"'`]([^\"'`]*).*?[\"'`]"
]
],
"tailwindCSS.classAttributes": [
"class",
"ui"
],
"files.associations": {
"*.css": "tailwindcss"
},
"editor.quickSuggestions": {
"strings": "on"
},
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"typescript",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml",
"toml"
]
}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Nicola Spadari<https://github.com/NicolaSpadari>
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.

97
README.md Normal file
View File

@@ -0,0 +1,97 @@
<p align="center">
<img width="150" src="./public/logo.png" alt="logo">
</p>
<h1 align="center">NUXTOR</h1>
<p align="center">
A spiritual successor of <a href="https://github.com/NicolaSpadari/vitauri">ViTauri</a>, made with <a href="https://nuxt.com">Nuxt 4</a> and <a href="https://v2.tauri.app">Tauri 2</a>
<br>
Build super fast desktop applications!
</p>
<br />
<p float="left">
<img src="https://img.shields.io/github/package-json/v/NicolaSpadari/nuxtor" />
<img src="https://img.shields.io/github/license/NicolaSpadari/nuxtor" />
</p>
<br />
<div align="center">
<img src="./public/screenshot.png">
</div>
<p align="center">Powered by Nuxt 4</p>
Check more screenshots at [preview](https://github.com/NicolaSpadari/nuxtor/blob/main/preview.md)
<br />
## Technologies run-down
- Nuxt v4
- Tauri v2
- NuxtUI v3
- TailwindCSS v4
- Typescript
- ESLint
- Auto imports (for Tauri api too!)
## Functionalities
- Run shell commands from the app
- Send custom notifications to the client (remember to turn on/grant notifications in your computer settings)
- Display OS related informations
- Store and retrieve data locally
- Show tray icon
- Support all Nuxt functionalities (routing/layout/middleware/modules/etc...)
## Setup
- Before running this app, you need to configure your environment with Rust. Take a look at the [Tauri docs](https://v2.tauri.app/start/prerequisites).
- This project enforces [pnpm](https://pnpm.io). In order to use another package manager you need to update `package.json` and `tauri.conf.json`
- The frontend runs on the usual port `3000` of Nuxt, the Tauri server uses the port `3001`. This settings are customizable in the `nuxt.config.ts` and `tauri.conf.json`.
- Once ready, follow these commands:
```sh
# use this template
$ npx degit NicolaSpadari/nuxtor my-nuxtor-app
# go into the folder
$ cd my-nuxtor-app
# install dependencies
$ pnpm install
# start the project
$ pnpm run tauri:dev
```
This will run the Nuxt frontend and will launch the Tauri window.
## Build
```sh
$ pnpm run tauri:build
```
This command will generate the Nuxt static output and bundle the project under `src-tauri/target`.
## Debug
```sh
$ pnpm run tauri:build:debug
```
The same Tauri bundle will generate under `src-tauri/target`, but with the ability to open the console.
## Notes
- Tauri v2 brings some big refactors, such as packages names and permission management. New permissions have to be granted under `src-tauri/capabilities/main.json`
- Tauri functions are auto imported with the help of a custom module, named like `useTauri<LibraryName>`. If another Tauri plugin is added, then the module has to be updated to support its functions under `app/modules/tauri.ts`
- As per [documentation](https://v2.tauri.app/start/frontend/nuxt/#checklist), Nuxt SSR must be disabled in order for Tauri to act as the backend. Still, all Nuxt goodies will be functional.
- NuxtUI is a very powerful UI library that consolidates design over the entire application. While there is a more complete pro version, it requires a license. It's up to you to buy the pro version, or stick with the free version.
## License
MIT License © 2024-PRESENT [NicolaSpadari](https://github.com/NicolaSpadari)

73
app/app.config.ts Normal file
View File

@@ -0,0 +1,73 @@
export default defineAppConfig({
app: {
name: "Nuxtor",
author: "Nicola Spadari",
repo: "https://github.com/NicolaSpadari/nuxtor",
tauriSite: "https://tauri.app",
nuxtSite: "https://nuxt.com",
nuxtUiSite: "https://ui.nuxt.dev"
},
pageCategories: {
system: {
label: "System",
icon: "lucide:square-terminal"
},
storage: {
label: "Storage",
icon: "lucide:archive"
},
interface: {
label: "Interface",
icon: "lucide:app-window-mac"
},
other: {
label: "Other",
icon: "lucide:folder"
}
},
ui: {
colors: {
primary: "green",
neutral: "zinc"
},
button: {
slots: {
base: "cursor-pointer"
}
},
formField: {
slots: {
root: "w-full"
}
},
input: {
slots: {
root: "w-full"
}
},
textarea: {
slots: {
root: "w-full",
base: "resize-none"
}
},
accordion: {
slots: {
trigger: "cursor-pointer",
item: "md:py-2"
}
},
navigationMenu: {
slots: {
link: "cursor-pointer"
},
variants: {
disabled: {
true: {
link: "cursor-text"
}
}
}
}
}
});

11
app/app.vue Normal file
View File

@@ -0,0 +1,11 @@
<template>
<Html class="overflow-x-hidden">
<Body class="font-sans antialiased">
<UApp>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</UApp>
</Body>
</Html>
</template>

49
app/assets/css/main.css Normal file
View File

@@ -0,0 +1,49 @@
@import "tailwindcss";
@import "@nuxt/ui";
@theme {
--font-heading: "Montserrat", sans-serif;
--font-sans: "Inter", sans-serif;
--color-primary: var(--ui-color-primary-500);
--color-secondary: var(--ui-color-secondary-500);
--color-success: var(--ui-color-success-500);
--color-info: var(--ui-color-info-500);
--color-warning: var(--ui-color-warning-500);
--color-error: var(--ui-color-error-500)
}
@layer base {
-webkit-tap-highlight-color: transparent;
}
@layer utilities {
img{
user-select: none;
-webkit-user-drag: none;
}
.flex-center {
@apply flex justify-center items-center;
}
.absolute-center-h {
@apply left-1/2 transform -translate-x-1/2;
}
.absolute-center-v {
@apply top-1/2 transform -translate-y-1/2;
}
}
.page-enter-active,
.page-leave-active {
@apply transition-opacity ease-in-out duration-300;
}
.layout-enter-active,
.layout-leave-active {
@apply transition-opacity ease-in-out duration-500;
}
.page-enter-from,
.page-leave-to,
.layout-enter-from,
.layout-leave-to {
@apply opacity-0;
}

View File

@@ -0,0 +1,4 @@
<svg fill="#fff" xmlns="http://www.w3.org/2000/svg" viewBox="0 -0.018652355298399925 149.6599884033203 150.74864196777344">
<path d="M74.59 150.73H4.11c-4 0-4.08-.08-4.08-4.15Q0 88.14 0 29.69c0-2.5.7-4.09 2.9-5.32 8.55-4.8 17-9.71 25.51-14.61 1.69-1 2.53-.64 2.52 1.38v2.28c0 27.38 0 54.77-.05 82.15a5.59 5.59 0 0 0 3.24 5.6Q53.3 112 72.3 123.2a4.62 4.62 0 0 0 5.18 0Q95 113 112.51 102.84a.92.92 0 0 1 .21-.13c6.06-1.78 6.63-6.17 6.57-11.77-.28-25.77-.12-51.56-.11-77.34v-2.28c0-1.83.8-2.29 2.38-1.4 2.65 1.48 5.31 3 7.93 4.49 6.06 3.52 12.08 7.11 18.17 10.58 1.79 1 2 2.49 2 4.27v117.15c0 4-.28 4.3-4.34 4.3H74.59z" />
<path d="M51.79 43.09V3.82C51.8.28 52 .07 55.5.05c2.87 0 5.74.09 8.61 0 2.23-.09 3 .84 2.93 3-.11 13 .56 26 .44 39-.15 16.21 0 32.43 0 48.64 0 3.66-.5 3.9-3.74 2-3.07-1.76-6.12-3.57-9.28-5.18a4.54 4.54 0 0 1-2.69-4.63c.08-13.26 0-26.52 0-39.77zM97.91 43.09v39.55a4.89 4.89 0 0 1-2.94 5c-3.22 1.66-6.25 3.69-9.42 5.45-2.51 1.39-3.41.87-3.33-2 .74-29.39.29-58.79.38-88.18 0-2.24.88-3 3-2.92 3 .11 6.08.16 9.12 0C97.31-.13 98 1 97.93 3.3c-.07 13.26 0 26.53 0 39.79z" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

4
app/assets/logo.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -0.018652355298399925 149.6599884033203 150.74864196777344">
<path d="M74.59 150.73H4.11c-4 0-4.08-.08-4.08-4.15Q0 88.14 0 29.69c0-2.5.7-4.09 2.9-5.32 8.55-4.8 17-9.71 25.51-14.61 1.69-1 2.53-.64 2.52 1.38v2.28c0 27.38 0 54.77-.05 82.15a5.59 5.59 0 0 0 3.24 5.6Q53.3 112 72.3 123.2a4.62 4.62 0 0 0 5.18 0Q95 113 112.51 102.84a.92.92 0 0 1 .21-.13c6.06-1.78 6.63-6.17 6.57-11.77-.28-25.77-.12-51.56-.11-77.34v-2.28c0-1.83.8-2.29 2.38-1.4 2.65 1.48 5.31 3 7.93 4.49 6.06 3.52 12.08 7.11 18.17 10.58 1.79 1 2 2.49 2 4.27v117.15c0 4-.28 4.3-4.34 4.3H74.59z" fill="#00DC82"></path>
<path d="M51.79 43.09V3.82C51.8.28 52 .07 55.5.05c2.87 0 5.74.09 8.61 0 2.23-.09 3 .84 2.93 3-.11 13 .56 26 .44 39-.15 16.21 0 32.43 0 48.64 0 3.66-.5 3.9-3.74 2-3.07-1.76-6.12-3.57-9.28-5.18a4.54 4.54 0 0 1-2.69-4.63c.08-13.26 0-26.52 0-39.77zM97.91 43.09v39.55a4.89 4.89 0 0 1-2.94 5c-3.22 1.66-6.25 3.69-9.42 5.45-2.51 1.39-3.41.87-3.33-2 .74-29.39.29-58.79.38-88.18 0-2.24.88-3 3-2.92 3 .11 6.08.16 9.12 0C97.31-.13 98 1 97.93 3.3c-.07 13.26 0 26.53 0 39.79z" fill="#FFC131"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,11 @@
<template>
<div class="top=1/5 pointer-events-none absolute inset-x-0 transform-gpu blur-3xl -z-10" aria-hidden="true">
<div class="blob relative left-[calc(50%+36rem)] aspect-[1155/678] w-[72.1875rem] from-(--color-warning) to-(--color-success) bg-gradient-to-br opacity-30 -translate-x-1/2" />
</div>
</template>
<style scoped>
.blob{
clip-path: polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%);
}
</style>

View File

@@ -0,0 +1,11 @@
<template>
<div class="pointer-events-none absolute inset-x-0 transform-gpu blur-3xl -top-1/4 -z-10" aria-hidden="true">
<div class="blob relative left-[calc(50%-30rem)] aspect-[1155/678] w-[72.1875rem] rotate-30 from-(--color-warning) to-(--color-success) bg-gradient-to-tr opacity-30 -translate-x-1/2" />
</div>
</template>
<style scoped>
.blob{
clip-path: polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%);
}
</style>

View File

@@ -0,0 +1,24 @@
<template>
<div class="grid grid-cols-1 lg:grid-cols-2">
<div class="px-6 pb-10 pt-12 sm:pt-16 lg:px-8 lg:pt-22">
<div class="mx-auto max-w-xl lg:mx-0 lg:max-w-lg">
<h2 class="text-3xl font-bold tracking-tight">
{{ props.title }}
</h2>
<p class="mt-6 text-lg text-(--ui-text-muted) leading-8">
{{ props.description }}
</p>
</div>
</div>
<div class="px-6 pb-10 pt-12 sm:pt-16 lg:px-8 lg:pt-22">
<slot />
</div>
</div>
</template>
<script lang="ts" setup>
const props = defineProps<{
title: string
description: string
}>();
</script>

View File

@@ -0,0 +1,72 @@
<template>
<header class="top-0 z-10">
<UContainer class="md:py-2">
<UNavigationMenu
:items="mobileItems"
variant="link"
:ui="{
root: 'md:hidden'
}"
/>
<UNavigationMenu
:items="desktopItems"
variant="link"
:ui="{
root: 'hidden md:flex',
viewportWrapper: 'max-w-2xl absolute-center-h',
list: 'md:gap-x-2'
}"
/>
</UContainer>
</header>
</template>
<script lang="ts" setup>
const { pages } = usePages();
const { showSidebar } = useSidebar();
const tauriVersion = await useTauriAppGetTauriVersion();
const mobileItems = ref<any[]>([
[
{
avatar: {
icon: "local:logo",
size: "xl",
ui: {
root: "bg-transparent"
}
},
to: "/"
}
],
[
{
icon: "lucide:menu",
onSelect: () => showSidebar.value = true
}
]
]);
const desktopItems = ref<any[]>([
[
{
avatar: {
icon: "local:logo",
size: "3xl",
ui: {
root: "group bg-transparent",
icon: "opacity-70 group-hover:opacity-100"
}
},
to: "/"
}
],
pages,
[
{
label: `v${tauriVersion}`,
disabled: true
}
]
]);
</script>

View File

@@ -0,0 +1,37 @@
<template>
<USlideover :open="showSidebar" @update:open="showSidebar = false">
<template #title>
<div class="flex gap-x-3">
<Icon name="local:logo" class="size-6" />
<span class="uppercase">{{ name }}</span>
</div>
</template>
<template #description>
<VisuallyHidden>Description</VisuallyHidden>
</template>
<template #body>
<UNavigationMenu
orientation="vertical"
:items="items"
/>
</template>
</USlideover>
</template>
<script lang="ts" setup>
const { app: { name } } = useAppConfig();
const { pages } = usePages();
const { showSidebar } = useSidebar();
const tauriVersion = await useTauriAppGetTauriVersion();
const items = ref<any[]>([
pages,
[
{
label: `v${tauriVersion}`,
disabled: true
}
]
]);
</script>

35
app/composables/pages.ts Normal file
View File

@@ -0,0 +1,35 @@
export const usePages = () => {
const router = useRouter();
const { pageCategories } = useAppConfig();
const routes = router.getRoutes().filter((route) => route.name !== "index" && route.name !== "all");
const categorizedRoutes = routes.reduce((acc, route) => {
const category = route.meta.category as string || "other";
if (!category) return acc;
if (!acc[category]) {
acc[category] = {
label: pageCategories[category as keyof typeof pageCategories]?.label,
icon: pageCategories[category as keyof typeof pageCategories]?.icon || "i-lucide-folder",
to: route.path,
children: []
};
}
acc[category].children.push({
label: route.meta.name as string || route.name,
description: route.meta.description as string,
icon: route.meta.icon || "i-lucide-file",
to: route.path
});
return acc;
}, {} as Record<string, any>);
const pages = Object.values(categorizedRoutes);
return {
pages
};
};

View File

@@ -0,0 +1,7 @@
export const useSidebar = () => {
const showSidebar = useState("showSidebar", () => false);
return {
showSidebar
};
};

8
app/layouts/blank.vue Normal file
View File

@@ -0,0 +1,8 @@
<template>
<div>
<SiteNavbar class="sticky bg-(--ui-bg)/75 backdrop-blur" />
<SiteSidebar />
<slot />
</div>
</template>

10
app/layouts/default.vue Normal file
View File

@@ -0,0 +1,10 @@
<template>
<div>
<SiteNavbar class="sticky bg-(--ui-bg)/75 backdrop-blur" />
<SiteSidebar />
<UContainer>
<slot />
</UContainer>
</div>
</template>

13
app/layouts/home.vue Normal file
View File

@@ -0,0 +1,13 @@
<template>
<div>
<SiteNavbar class="fixed w-full" />
<SiteSidebar />
<div class="relative overflow-hidden px-6 lg:px-8">
<DesignTopBlob />
<DesignBottomBlob />
<slot />
</div>
</div>
</template>

View File

@@ -0,0 +1,4 @@
export default defineNuxtRouteMiddleware(() => {
const { showSidebar } = useSidebar();
showSidebar.value = false;
});

41
app/modules/tauri.ts Normal file
View File

@@ -0,0 +1,41 @@
import * as tauriApp from "@tauri-apps/api/app";
import * as tauriWebviewWindow from "@tauri-apps/api/webviewWindow";
import * as tauriFs from "@tauri-apps/plugin-fs";
import * as tauriNotification from "@tauri-apps/plugin-notification";
import * as tauriOs from "@tauri-apps/plugin-os";
import * as tauriShell from "@tauri-apps/plugin-shell";
import * as tauriStore from "@tauri-apps/plugin-store";
import { addImports, defineNuxtModule } from "nuxt/kit";
const capitalize = (name: string) => {
return name.charAt(0).toUpperCase() + name.slice(1);
};
const tauriModules = [
{ module: tauriApp, prefix: "App", importPath: "@tauri-apps/api/app" },
{ module: tauriWebviewWindow, prefix: "WebviewWindow", importPath: "@tauri-apps/api/webviewWindow" },
{ module: tauriShell, prefix: "Shell", importPath: "@tauri-apps/plugin-shell" },
{ module: tauriOs, prefix: "Os", importPath: "@tauri-apps/plugin-os" },
{ module: tauriNotification, prefix: "Notification", importPath: "@tauri-apps/plugin-notification" },
{ module: tauriFs, prefix: "Fs", importPath: "@tauri-apps/plugin-fs" },
{ module: tauriStore, prefix: "Store", importPath: "@tauri-apps/plugin-store" }
];
export default defineNuxtModule<ModuleOptions>({
meta: {
name: "nuxt-tauri",
configKey: "tauri"
},
defaults: {
prefix: "useTauri"
},
setup(options) {
tauriModules.forEach(({ module, prefix, importPath }) => {
Object.keys(module).filter((name) => name !== "default").forEach((name) => {
const prefixedName = `${options.prefix}${prefix}` || "";
const as = prefixedName ? prefixedName + capitalize(name) : name;
addImports({ from: importPath, name, as });
});
});
}
});

26
app/pages/[...all].vue Normal file
View File

@@ -0,0 +1,26 @@
<template>
<div class="grid place-items-center py-12 md:py-24">
<div class="flex flex-col items-center gap-y-4 md:gap-y-8">
<p class="text-(--ui-success) font-semibold">
404
</p>
<div class="text-center space-y-3">
<h1 class="text-3xl font-bold tracking-tight" sm="text-5xl">
Page not found
</h1>
<p class="text-base text-(--ui-muted) leading-7">
Sorry, we couldn't find the page you're looking for.
</p>
</div>
<UButton to="/" variant="outline" size="lg" :ui="{ base: 'px-5' }">
Go home
</UButton>
</div>
</div>
</template>
<script lang="ts" setup>
definePageMeta({
layout: "blank"
});
</script>

63
app/pages/commands.vue Normal file
View File

@@ -0,0 +1,63 @@
<template>
<LayoutTile
title="Commands"
description="Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application."
>
<div class="space-y-6 md:space-y-8">
<UForm :state="inputState" :schema="schema" class="flex flex-col gap-y-4 items-end" @submit="sendCommand">
<UFormField label="Command input" name="input">
<UInput v-model="inputState.input" variant="subtle" size="lg" />
</UFormField>
<UButton type="submit" size="lg">
Send command
</UButton>
</UForm>
<UForm :state="outputState" class="flex flex-col gap-y-4 items-end">
<UFormField label="Command output" name="command-output">
<UTextarea v-model="outputState.output" variant="subtle" size="lg" :rows="8" readonly />
</UFormField>
</UForm>
</div>
</LayoutTile>
</template>
<script lang="ts" setup>
definePageMeta({
name: "Shell commands",
icon: "lucide:terminal",
description: "Execute shell commands",
category: "system"
});
const schema = z.object({
input: z.string({
error: "Input is required"
}).nonempty()
});
type Schema = zInfer<typeof schema>;
const inputState = ref<Partial<Schema>>({
input: undefined
});
const outputState = ref({
output: ""
});
const sendCommand = async () => {
try {
const response = await useTauriShellCommand.create("exec-sh", [
"-c",
inputState.value.input!
]).execute();
outputState.value.output = JSON.stringify(response, null, 4);
} catch (error) {
outputState.value.output = JSON.stringify(error, null, 4);
} finally {
inputState.value.input = undefined;
}
};
</script>

83
app/pages/file.vue Normal file
View File

@@ -0,0 +1,83 @@
<template>
<LayoutTile
title="File System"
description="Access the file system. For this demo the only allowed permission is read/write to the Documents folder (no sub directories)."
>
<UForm :state="inputState" :schema="schema" class="flex flex-col gap-y-4 items-end" @submit="createFile">
<UFormField label="Text file name (with extension)" name="fileName">
<UInput v-model="inputState.fileName" variant="subtle" size="lg" />
</UFormField>
<UFormField label="File content" name="fileContent">
<UTextarea v-model="inputState.fileContent" variant="subtle" size="lg" :rows="10" />
</UFormField>
<UButton type="submit" size="lg">
Create file
</UButton>
</UForm>
</LayoutTile>
</template>
<script lang="ts" setup>
definePageMeta({
name: "Files",
icon: "lucide:file-text",
category: "storage",
description: "Create and manage files"
});
const schema = z.object({
fileName: z.string({
error: "File name is required"
}).nonempty().regex(/^[\w,\s-]+\.[A-Z0-9]+$/i, {
message: "Invalid filename format"
}),
fileContent: z.string({
error: "File content is required"
}).nonempty()
});
type Schema = zInfer<typeof schema>;
const inputState = ref<Partial<Schema>>({
fileName: undefined,
fileContent: undefined
});
const toast = useToast();
const createFile = async () => {
try {
const fileExists = await useTauriFsExists(inputState.value.fileName!, {
baseDir: useTauriFsBaseDirectory.Document
});
if (fileExists) {
toast.add({
title: "Error",
description: "The file already exists",
color: "error"
});
return;
}
await useTauriFsWriteTextFile(inputState.value.fileName!, inputState.value.fileContent!, {
baseDir: useTauriFsBaseDirectory.Document
});
toast.add({
title: "Success",
description: "The file has been created",
color: "success"
});
inputState.value.fileName = inputState.value.fileContent = undefined;
} catch (err) {
toast.add({
title: "Error",
description: String(err),
color: "error"
});
}
};
</script>

73
app/pages/index.vue Normal file
View File

@@ -0,0 +1,73 @@
<template>
<UContainer class="relative overflow-hidden h-screen">
<div class="grid size-full place-content-center gap-y-8">
<SvgoLogo :filled="true" :font-controlled="false" class="mx-auto size-40" />
<div class="flex flex-col items-center gap-y-3">
<h1 class="animate-pulse text-3xl sm:text-4xl text-pretty font-bold font-heading md:mb-5">
{{ app.name.toUpperCase() }}
</h1>
<p class="leading-7 text-pretty">
Powered by
</p>
<div class="flex flex-wrap justify-center gap-1 md:gap-3">
<UButton
variant="ghost"
size="xl"
:to="app.nuxtSite"
target="_blank"
external
>
Nuxt 4
</UButton>
<UButton
variant="ghost"
size="xl"
:to="app.tauriSite"
target="_blank"
external
>
Tauri 2
</UButton>
<UButton
variant="ghost"
size="xl"
:to="app.nuxtUiSite"
target="_blank"
external
>
NuxtUI 3
</UButton>
</div>
</div>
<div class="flex justify-center">
<UButton
:to="app.repo"
>
Star on GitHub
</UButton>
</div>
</div>
<div class="fixed bottom-6 text-sm absolute-center-h">
<div class="flex items-center gap-1 text-(--ui-text-muted)">
<p class="text-sm">
Made by
</p>
<ULink :to="app.repo" external target="_blank">
{{ app.author }}
</ULink>
</div>
</div>
</UContainer>
</template>
<script lang="ts" setup>
const { app } = useAppConfig();
definePageMeta({
layout: "home"
});
</script>

View File

@@ -0,0 +1,70 @@
<template>
<LayoutTile
title="Notifications"
description="Send native notifications to the client using the notification plugin."
>
<UForm :state="inputState" :schema="schema" class="flex flex-col gap-y-4 items-end" @submit="createNotification">
<UFormField label="Notification title" name="notificationTitle">
<UInput v-model="inputState.notificationTitle" variant="subtle" size="lg" />
</UFormField>
<UFormField label="Notification body (optional)" name="notificationBody">
<UInput v-model="inputState.notificationBody" variant="subtle" size="lg" />
</UFormField>
<UButton type="submit" size="lg">
Send notification
</UButton>
</UForm>
</LayoutTile>
</template>
<script lang="ts" setup>
definePageMeta({
name: "Notifications",
icon: "lucide:message-square-more",
category: "interface",
description: "Send native notifications"
});
const schema = z.object({
notificationTitle: z.string({
error: "Title is required"
}).nonempty(),
notificationBody: z.string().optional()
});
type Schema = zInfer<typeof schema>;
const inputState = ref<Partial<Schema>>({
notificationTitle: undefined,
notificationBody: undefined
});
const toast = useToast();
const permissionGranted = ref(false);
const createNotification = async () => {
permissionGranted.value = await useTauriNotificationIsPermissionGranted();
if (!permissionGranted.value) {
const permission = await useTauriNotificationRequestPermission();
permissionGranted.value = permission === "granted";
}
if (permissionGranted.value) {
useTauriNotificationSendNotification({
title: inputState.value.notificationTitle!,
body: inputState.value.notificationBody
});
inputState.value.notificationTitle = inputState.value.notificationBody = undefined;
} else {
toast.add({
title: "Error",
description: "Missing notifications permission",
color: "error"
});
}
};
</script>

35
app/pages/os.vue Normal file
View File

@@ -0,0 +1,35 @@
<template>
<LayoutTile
title="OS Information"
description="Read information about the operating system using the OS Information plugin."
>
<UAccordion :items="items" type="multiple" />
</LayoutTile>
</template>
<script lang="ts" setup>
definePageMeta({
name: "OS Informations",
icon: "lucide:info",
category: "system",
description: "Read operating system informations."
});
const items = ref([
{
label: "System",
icon: "lucide:monitor",
content: `${useTauriOsPlatform()} ${useTauriOsVersion()}`
},
{
label: "Arch",
icon: "lucide:microchip",
content: useTauriOsArch()
},
{
label: "Locale",
icon: "lucide:globe",
content: await useTauriOsLocale() || "Not detectable"
}
]);
</script>

91
app/pages/store.vue Normal file
View File

@@ -0,0 +1,91 @@
<template>
<LayoutTile
title="Store"
description="Persistent key-value store. Allows you to handle state to a file which can be saved and loaded on demand including between app restarts."
>
<div class="space-y-6 md:space-y-8">
<UForm :state="inputState" :schema="schema" class="flex flex-col gap-y-4 items-end" @submit="setStoreValue">
<UFormField label="Store value" name="value">
<UInput v-model="inputState.value" variant="subtle" size="lg" />
</UFormField>
<UButton type="submit" size="lg">
Set value
</UButton>
</UForm>
<UForm :state="outputState" class="flex flex-col gap-y-4 items-end">
<UFormField label="Store content" name="content">
<UTextarea v-model="outputState.content" variant="subtle" size="lg" :rows="8" readonly />
</UFormField>
</UForm>
</div>
</LayoutTile>
</template>
<script lang="ts" setup>
definePageMeta({
name: "Store",
icon: "lucide:database",
category: "storage",
description: "Handle file creation in the file system"
});
const schema = z.object({
value: z.string({
error: "Store key is required"
}).nonempty()
});
type Schema = zInfer<typeof schema>;
const inputState = ref<Partial<Schema>>({
value: undefined
});
const outputState = ref({
content: ""
});
const toast = useToast();
const autosave = ref(false);
const store = await useTauriStoreLoad("store.bin", {
autoSave: autosave.value
});
const getStoreValue = async () => {
try {
outputState.value.content = await store.get<string>("myData") || "";
} catch (error) {
toast.add({
title: "Error",
description: String(error),
color: "error"
});
outputState.value.content = JSON.stringify(error, null, 4);
}
};
await getStoreValue();
const setStoreValue = async () => {
try {
await store.set("myData", inputState.value!.value);
await getStoreValue();
toast.add({
title: "Success",
description: "Store value retieved",
color: "success"
});
} catch (error) {
toast.add({
title: "Error",
description: String(error),
color: "error"
});
outputState.value.content = JSON.stringify(error, null, 4);
} finally {
inputState.value.value = undefined;
}
};
</script>

51
app/pages/webview.vue Normal file
View File

@@ -0,0 +1,51 @@
<template>
<LayoutTile
title="Webview window"
description="Create new webview in a detached window. This will create a new window flagged 'secondary' that has the same permissions as the main one. If you need more windows, update the permissions under capabilities > main or create a new capabilities file for the new window only."
>
<div class="flex flex-col items-center gap-6">
<UButton variant="subtle" @click="openWindow((new Date).valueOf().toString(), app.repo)">
Create external Webview
</UButton>
<UButton variant="subtle" @click="openWindow('secondary', '/os')">
Create internal Webview
</UButton>
</div>
</LayoutTile>
</template>
<script lang="ts" setup>
definePageMeta({
name: "Webview",
icon: "lucide:app-window",
category: "interface",
description: "Create new webview in a detached window"
});
const { app } = useAppConfig();
const toast = useToast();
const openWindow = async (id: string, page: string) => {
const webview = new useTauriWebviewWindowWebviewWindow(id, {
title: "Nuxtor webview",
url: page,
width: 1280,
height: 720
});
webview.once("tauri://created", () => {
toast.add({
title: "Success",
description: "Webview created",
color: "success"
});
});
webview.once("tauri://error", (error) => {
toast.add({
title: "Error",
description: (error as any).payload,
color: "error"
});
});
};
</script>

22
app/router.options.ts Normal file
View File

@@ -0,0 +1,22 @@
import type { RouterOptions } from "@nuxt/schema";
export default {
scrollBehavior(to, _from, savedPosition) {
return new Promise((resolve, _reject) => {
setTimeout(() => {
if (savedPosition) {
resolve(savedPosition);
} else {
if (to.hash) {
resolve({
el: to.hash,
top: 0
});
} else {
resolve({ top: 0 });
}
}
}, 100);
});
}
} satisfies RouterOptions;

13
bump.config.ts Normal file
View File

@@ -0,0 +1,13 @@
import { defineConfig } from "bumpp";
export default defineConfig({
release: "prompt",
commit: false,
tag: false,
push: false,
files: [
"package.json",
"src-tauri/tauri.conf.json",
"src-tauri/Cargo.toml"
]
});

46
eslint.config.mjs Normal file
View File

@@ -0,0 +1,46 @@
import eslintConfig from "@antfu/eslint-config";
import nuxtConfig from "./.nuxt/eslint.config.mjs";
export default eslintConfig(
// General
{
typescript: true,
vue: true,
stylistic: {
indent: "tab",
quotes: "double"
},
rules: {
curly: "off",
"no-console": "off",
"no-new-func": "off",
"style/semi": ["error", "always"],
"style/indent": ["error", "tab"],
"style/quote-props": ["warn", "as-needed"],
"style/comma-dangle": ["warn", "never"],
"style/brace-style": ["warn", "1tbs"],
"style/arrow-parens": ["error", "always"],
"vue/block-order": ["error", {
order: ["template", "script", "style"]
}],
"vue/script-indent": ["error", "tab", {
baseIndent: 1
}],
"vue/comma-dangle": ["warn", "never"],
"antfu/top-level-function": "off",
"antfu/if-newline": "off",
"new-cap": "off",
"node/prefer-global/process": ["off"]
}
},
// Vue
{
files: ["**/*.vue"],
rules: {
"style/indent": "off"
}
},
nuxtConfig()
);

95
nuxt.config.ts Normal file
View File

@@ -0,0 +1,95 @@
export default defineNuxtConfig({
modules: [
"@vueuse/nuxt",
"@nuxt/ui",
"nuxt-svgo",
"reka-ui/nuxt",
"@nuxt/eslint"
],
app: {
head: {
title: "Nuxtor",
charset: "utf-8",
viewport: "width=device-width, initial-scale=1",
meta: [
{ name: "format-detection", content: "no" }
]
},
pageTransition: {
name: "page",
mode: "out-in"
},
layoutTransition: {
name: "layout",
mode: "out-in"
}
},
css: [
"@/assets/css/main.css"
],
icon: {
customCollections: [
{
prefix: "local",
dir: "./app/assets/icons"
}
]
},
svgo: {
autoImportPath: "@/assets/"
},
ssr: false,
dir: {
modules: "app/modules"
},
imports: {
presets: [
{
from: "zod",
imports: [
"z",
{
name: "infer",
as: "zInfer",
type: true
}
]
}
]
},
vite: {
clearScreen: false,
envPrefix: ["VITE_", "TAURI_"],
server: {
strictPort: true,
hmr: {
protocol: "ws",
host: "0.0.0.0",
port: 3001
},
watch: {
ignored: ["**/src-tauri/**"]
}
}
},
devServer: {
host: "0.0.0.0"
},
router: {
options: {
scrollBehaviorType: "smooth"
}
},
eslint: {
config: {
standalone: false
}
},
devtools: {
enabled: false
},
experimental: {
typedPages: true
},
compatibilityDate: "2025-07-01"
});

52
package.json Normal file
View File

@@ -0,0 +1,52 @@
{
"name": "nuxtor",
"type": "module",
"version": "1.4.0",
"private": true,
"packageManager": "pnpm@10.13.1",
"description": "Starter template for Nuxt 3 and Tauri 2",
"author": "Nicola Spadari",
"license": "MIT",
"engines": {
"node": ">=23"
},
"scripts": {
"dev": "nuxt dev",
"generate": "nuxt generate",
"preinstall": "npx only-allow pnpm",
"postinstall": "nuxt prepare",
"lint": "eslint . --fix",
"bump": "bumpp",
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build",
"tauri:build:debug": "tauri build --debug"
},
"dependencies": {
"@nuxt/ui": "^3.2.0",
"@tauri-apps/api": "^2.6.0",
"@tauri-apps/plugin-fs": "^2.4.0",
"@tauri-apps/plugin-notification": "^2.3.0",
"@tauri-apps/plugin-os": "^2.3.0",
"@tauri-apps/plugin-shell": "^2.3.0",
"@tauri-apps/plugin-store": "^2.3.0",
"nuxt": "^4.0.0",
"vue": "^3.5.17",
"vue-router": "^4.5.1",
"zod": "^4.0.5"
},
"devDependencies": {
"@antfu/eslint-config": "^4.17.0",
"@nuxt/eslint": "^1.5.2",
"@tauri-apps/cli": "^2.6.2",
"@vueuse/core": "^13.5.0",
"@vueuse/nuxt": "^13.5.0",
"bumpp": "^10.2.0",
"eslint": "^9.31.0",
"nuxt-svgo": "^4.2.4",
"typescript": "^5.8.3"
},
"resolutions": {
"typescript": "npm:tslite@latest"
}
}

11897
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

63
preview.md Normal file
View File

@@ -0,0 +1,63 @@
# Preview
Since Nuxtor is a desktop application, there is no way of showing the look and feel of the released project.
What follows are each page and its functionalities:
## Commands page
Access the system shell
<div align="center">
<img src="./public/page-commands.png">
</div>
---
## File system page
Access the file system
<div align="center">
<img src="./public/page-file-system.png">
</div>
---
## Notifications page
Send custom notifications at os-level
<div align="center">
<img src="./public/page-notifications.png">
</div>
---
## OS info page
Show system informations
<div align="center">
<img src="./public/page-os-info.png">
</div>
---
## Storage page
Read & write persistent key-value data
<div align="center">
<img src="./public/page-storage.png">
</div>
---
## Webview page
Create a secondary detached window
<div align="center">
<img src="./public/page-webview.png">
</div>

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
public/page-commands.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

BIN
public/page-file-system.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

BIN
public/page-os-info.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

BIN
public/page-storage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

BIN
public/page-webview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

BIN
public/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

5423
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,39 @@
[package]
name = "nuxtor"
version = "1.4.0"
description = "Starter template for Nuxt 3 and Tauri 2"
authors = [ "NicolaSpadari" ]
license = "MIT"
repository = "https://github.com/NicolaSpadari/nuxtor"
edition = "2021"
[lib]
name = "nuxtor_lib"
crate-type = [
"staticlib",
"cdylib",
"rlib"
]
[build-dependencies.tauri-build]
version = "2.3.0"
features = [ ]
[dependencies]
tauri-plugin-shell = "2.3.0"
tauri-plugin-notification = "2.3.0"
tauri-plugin-os = "2.3.0"
tauri-plugin-fs = "2.4.0"
tauri-plugin-store = "2.3.0"
serde_json = "1"
[dependencies.tauri]
version = "2.6.2"
features = [
"tray-icon",
"unstable"
]
[dependencies.serde]
version = "1"
features = [ "derive" ]

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

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

View File

@@ -0,0 +1,46 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main",
"description": "Capabilities for the app window",
"windows": [
"main",
"secondary"
],
"permissions": [
"core:path:default",
"core:event:default",
"core:window:default",
"core:app:default",
"core:resources:default",
"core:menu:default",
"core:tray:default",
"shell:allow-open",
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "exec-sh",
"cmd": "sh",
"args": [
"-c",
{
"validator": "\\S+"
}
],
"sidecar": false
}
]
},
"notification:default",
"os:allow-platform",
"os:allow-arch",
"os:allow-family",
"os:allow-version",
"os:allow-locale",
"fs:allow-document-read",
"fs:allow-document-write",
"store:default",
"core:webview:allow-create-webview",
"core:webview:allow-create-webview-window"
]
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"main":{"identifier":"main","description":"Capabilities for the app window","local":true,"windows":["main","secondary"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","shell:allow-open",{"identifier":"shell:allow-execute","allow":[{"args":["-c",{"validator":"\\S+"}],"cmd":"sh","name":"exec-sh","sidecar":false}]},"notification:default","os:allow-platform","os:allow-arch","os:allow-family","os:allow-version","os:allow-locale","fs:allow-document-read","fs:allow-document-write","store:default","core:webview:allow-create-webview","core:webview:allow-create-webview-window"]}}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 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: 30 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Some files were not shown because too many files have changed in this diff Show More