style: formatting

This commit is contained in:
PandaDEV 2024-11-23 14:57:42 +10:00
parent 7e6b2f8b63
commit f2f554074b
No known key found for this signature in database
GPG key ID: 13EFF9BAF70EE75C
22 changed files with 1493 additions and 1171 deletions

214
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,214 @@
name: "Release"
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get_version.outputs.VERSION }}
steps:
- uses: actions/checkout@v4
- name: Get version
id: get_version
run: echo "VERSION=$(node -p "require('./src-tauri/tauri.conf.json').version")" >> $GITHUB_OUTPUT
build-macos:
needs: prepare
strategy:
matrix:
include:
- args: "--target aarch64-apple-darwin"
arch: "silicon"
- args: "--target x86_64-apple-darwin"
arch: "intel"
runs-on: macos-latest
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
steps:
- uses: actions/checkout@v4
- name: Redact Sensitive Information
run: |
function redact_output {
sed -e "s/${{ secrets.REDACT_PATTERN }}/REDACTED/g"
}
exec > >(redact_output) 2>&1
- uses: actions/setup-node@v4
with:
node-version: 20
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin,x86_64-apple-darwin
- uses: swatinem/rust-cache@v2
with:
workspaces: "src-tauri -> target"
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
shared-key: "macos-rust-cache"
save-if: "true"
- uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- run: npm install -g pnpm && pnpm install
- name: Import Apple Developer Certificate
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
args: ${{ matrix.args }}
tagName: v${{ needs.prepare.outputs.version }}
releaseName: v${{ needs.prepare.outputs.version }}
releaseBody: "See the assets to download this version and install."
releaseDraft: true
prerelease: false
- name: Rename macOS Artifacts
run: |
mv src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/dmg/*.dmg src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/dmg/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.dmg
mv src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/macos/*.app.tar.gz src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/macos/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.app.tar.gz
mv src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/macos/*.app.tar.gz.sig src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/macos/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.app.tar.gz.sig
build-windows:
needs: prepare
strategy:
matrix:
include:
- args: "--target x86_64-pc-windows-msvc"
arch: "x64"
- args: "--target aarch64-pc-windows-msvc"
arch: "arm64"
runs-on: windows-latest
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc,aarch64-pc-windows-msvc
- uses: swatinem/rust-cache@v2
with:
workspaces: "src-tauri -> target"
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
shared-key: "windows-rust-cache"
save-if: "true"
- uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- run: npm install -g pnpm && pnpm install
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: ${{ matrix.args }}
tagName: v${{ needs.prepare.outputs.version }}
releaseName: v${{ needs.prepare.outputs.version }}
releaseBody: "See the assets to download this version and install."
releaseDraft: true
prerelease: false
- name: Rename Windows Artifacts
run: |
mv src-tauri/target/release/bundle/msi/*.msi src-tauri/target/release/bundle/msi/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.msi
mv src-tauri/target/release/bundle/msi/*.msi.sig src-tauri/target/release/bundle/msi/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.msi.sig
build-linux:
needs: prepare
runs-on: ubuntu-latest
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
- uses: dtolnay/rust-toolchain@stable
- uses: swatinem/rust-cache@v2
with:
workspaces: "src-tauri -> target"
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
shared-key: "linux-rust-cache"
save-if: "true"
- uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y libwebkit2gtk-4.1-dev build-essential curl wget file libssl-dev libayatana-appindicator3-dev librsvg2-dev libasound2-dev rpm
echo "PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig" >> $GITHUB_ENV
- run: npm install -g pnpm && pnpm install
- name: Generate Changelog
id: changelog
run: |
CHANGELOG=$(git log $(git describe --tags --abbrev=0)..HEAD --pretty=format:"- %s")
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
echo "$CHANGELOG" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: --target x86_64-unknown-linux-gnu
tagName: v${{ needs.prepare.outputs.version }}
releaseName: v${{ needs.prepare.outputs.version }}
releaseBody: |
## Changelog
${{ env.CHANGELOG }}
See the assets to download this version and install.
releaseDraft: true
prerelease: false
- name: Rename Linux Artifacts
run: |
mv src-tauri/target/release/bundle/deb/*.deb src-tauri/target/release/bundle/deb/Qopy-${{ needs.prepare.outputs.version }}_amd64.deb
mv src-tauri/target/release/bundle/appimage/*.AppImage src-tauri/target/release/bundle/appimage/Qopy-${{ needs.prepare.outputs.version }}_amd64.AppImage
mv src-tauri/target/release/bundle/appimage/*.AppImage.sig src-tauri/target/release/bundle/appimage/Qopy-${{ needs.prepare.outputs.version }}_amd64.AppImage.sig
mv src-tauri/target/release/bundle/rpm/*.rpm src-tauri/target/release/bundle/rpm/Qopy-${{ needs.prepare.outputs.version }}_amd64.rpm
- name: Create Draft Release
uses: softprops/action-gh-release@v1
with:
draft: true
files: |
src-tauri/target/release/bundle/deb/*.deb
src-tauri/target/release/bundle/appimage/*.AppImage
src-tauri/target/release/bundle/appimage/*.AppImage.sig
src-tauri/target/release/bundle/rpm/*.rpm
body: |
## Changelog
${{ env.CHANGELOG }}
See the assets to download this version and install.
tag_name: v${{ needs.prepare.outputs.version }}
name: v${{ needs.prepare.outputs.version }}

39
.prettierignore Normal file
View file

@ -0,0 +1,39 @@
# Rust
/target/
/src-tauri/target/
Cargo.lock
*.rs
# Node
node_modules/
.nuxt/
.output/
dist/
# Build
/build/
/out/
# Tauri
.tauri/
# System
.DS_Store
*.pem
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Local env files
.env
.env.*
# IDE
.idea/
.vscode/
*.suo
*.ntvs*
*.njsproj
*.sln

10
.prettierrc Normal file
View file

@ -0,0 +1,10 @@
{
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"useTabs": true,
"printWidth": 100,
"trailingComma": "es5",
"vueIndentScriptAndStyle": true,
"plugins": ["prettier-plugin-vue"]
}

View file

@ -5,7 +5,7 @@ The hotkey for Qopy is Windows+V which is also the hotkey for the default clipbo
All the data of Qopy is stored inside of a SQLite database. The location for the file differs for windows and linux. All the data of Qopy is stored inside of a SQLite database. The location for the file differs for windows and linux.
| Operating System | Path | | Operating System | Path |
|------------------|-------------------------------------------------------| | ---------------- | ----------------------------------------------------- |
| Windows | `C:\Users\USERNAME\AppData\Roaming\net.pandadev.qopy` | | Windows | `C:\Users\USERNAME\AppData\Roaming\net.pandadev.qopy` |
| Linux | `` | | Linux | `` |

View file

@ -60,6 +60,7 @@ The fixed and simple clipboard manager for both Windows and Linux.
Qopy is a fixed clipboard manager designed as a simple alternative to the standard clipboard on Windows. It aims to provide a faster, more reliable experience while providing an extensive set of features compared to its Windows counterpart. Qopy is a fixed clipboard manager designed as a simple alternative to the standard clipboard on Windows. It aims to provide a faster, more reliable experience while providing an extensive set of features compared to its Windows counterpart.
## 🚧 Roadmap ## 🚧 Roadmap
- [ ] [Setup guide](https://github.com/0PandaDEV/Qopy/blob/main/GET_STARTED.md) - [ ] [Setup guide](https://github.com/0PandaDEV/Qopy/blob/main/GET_STARTED.md)
- [ ] Settings https://github.com/0PandaDEV/Qopy/issues/2 - [ ] Settings https://github.com/0PandaDEV/Qopy/issues/2
- [ ] Metadata for copied items https://github.com/0PandaDEV/Qopy/issues/5 - [ ] Metadata for copied items https://github.com/0PandaDEV/Qopy/issues/5
@ -72,6 +73,7 @@ Qopy is a fixed clipboard manager designed as a simple alternative to the standa
<sup>If you have ideas for features to include, please write a feature request [here](https://github.com/0pandadev/Qopy/issues).</sup> <sup>If you have ideas for features to include, please write a feature request [here](https://github.com/0pandadev/Qopy/issues).</sup>
## 📦 Preview ## 📦 Preview
<img width="800px" src="https://github.com/user-attachments/assets/18e1f9e3-414c-46e2-9c51-61c6e63a06d2"/> <img width="800px" src="https://github.com/user-attachments/assets/18e1f9e3-414c-46e2-9c51-61c6e63a06d2"/>
<img width="800px" src="https://github.com/user-attachments/assets/46ec4672-f156-4426-a2cb-3a40d00dbcd6"/> <img width="800px" src="https://github.com/user-attachments/assets/46ec4672-f156-4426-a2cb-3a40d00dbcd6"/>

32
app.vue
View file

@ -5,27 +5,27 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { listen } from '@tauri-apps/api/event' import { app, window } from "@tauri-apps/api";
import { app, window } from '@tauri-apps/api'; import { listen } from "@tauri-apps/api/event";
import { onMounted } from 'vue' import { onMounted } from "vue";
onMounted(async () => { onMounted(async () => {
await listen('change_keybind', async () => { await listen("change_keybind", async () => {
console.log("change_keybind"); console.log("change_keybind");
await navigateTo('/keybind') await navigateTo("/keybind");
await app.show(); await app.show();
await window.getCurrentWindow().show(); await window.getCurrentWindow().show();
}) });
await listen('main_route', async () => { await listen("main_route", async () => {
console.log("main_route"); console.log("main_route");
await navigateTo('/') await navigateTo("/");
}) });
}) });
</script> </script>
<style lang="scss"> <style lang="scss">
@font-face { @font-face {
font-family: SFRoundedRegular; font-family: SFRoundedRegular;
font-display: swap; font-display: swap;
src: url("~/assets/fonts/SFRoundedRegular.otf") format("opentype"); src: url("~/assets/fonts/SFRoundedRegular.otf") format("opentype");
@ -53,16 +53,16 @@ onMounted(async () => {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
color: #E5DFD5; color: #e5dfd5;
text-decoration: none; text-decoration: none;
font-family: SFRoundedRegular; font-family: SFRoundedRegular;
scroll-behavior: smooth; scroll-behavior: smooth;
scrollbar-width: thin; scrollbar-width: thin;
user-select: none; user-select: none;
--os-handle-bg: #ADA9A1; --os-handle-bg: #ada9a1;
--os-handle-bg-hover: #78756F; --os-handle-bg-hover: #78756f;
--os-handle-bg-active: #78756F; --os-handle-bg-active: #78756f;
} }
html, html,

View file

@ -1,10 +1,10 @@
$primary: #2E2D2B; $primary: #2e2d2b;
$accent: #FEB453; $accent: #feb453;
$divider: #ffffff0d; $divider: #ffffff0d;
$text: #E5DFD5; $text: #e5dfd5;
$text2: #ADA9A1; $text2: #ada9a1;
$mutedtext: #78756F; $mutedtext: #78756f;
.bg { .bg {
width: 750px; width: 750px;
@ -25,7 +25,7 @@ $mutedtext: #78756F;
gap: 8px; gap: 8px;
align-items: center; align-items: center;
img{ img {
background-color: $divider; background-color: $divider;
border-radius: 6px; border-radius: 6px;
padding: 8px 6px; padding: 8px 6px;
@ -123,7 +123,7 @@ $mutedtext: #78756F;
background-color: $divider; background-color: $divider;
margin-left: 8px; margin-left: 8px;
margin-right: 4px; margin-right: 4px;
transition: all .2s; transition: all 0.2s;
} }
.actions { .actions {
@ -134,7 +134,7 @@ $mutedtext: #78756F;
gap: 8px; gap: 8px;
border-radius: 7px; border-radius: 7px;
background-color: transparent; background-color: transparent;
transition: all .2s; transition: all 0.2s;
cursor: pointer; cursor: pointer;
} }
@ -142,7 +142,7 @@ $mutedtext: #78756F;
background-color: $divider; background-color: $divider;
} }
&:hover .actions:hover~.divider { &:hover .actions:hover ~ .divider {
opacity: 0; opacity: 0;
} }
} }

View file

@ -3,7 +3,7 @@
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.noise { .noise {
position: absolute; position: absolute;
overflow: hidden; overflow: hidden;
top: 0; top: 0;
@ -13,7 +13,7 @@
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
z-index: 0; z-index: 0;
background-image: url('/noise.png'); background-image: url("/noise.png");
background-repeat: repeat; background-repeat: repeat;
image-rendering: pixelated; image-rendering: pixelated;
overflow: hidden; overflow: hidden;

View file

@ -1,26 +1,31 @@
{ {
"name": "nuxt-app", "name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "tauri build",
"dev": "tauri dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.3", "@tauri-apps/api": "2.1.1",
"@tauri-apps/cli": "2.1.0", "@tauri-apps/cli": "2.1.0",
"@tauri-apps/plugin-autostart": "2.0.0", "@tauri-apps/plugin-autostart": "2.0.0",
"@tauri-apps/plugin-fs": "2.0.2", "@tauri-apps/plugin-fs": "2.0.2",
"@tauri-apps/plugin-os": "2.0.0", "@tauri-apps/plugin-os": "2.0.0",
"@tauri-apps/plugin-sql": "2.0.1", "@tauri-apps/plugin-sql": "2.0.1",
"nuxt": "3.14.159", "nuxt": "3.14.1592",
"nuxt-build-cache": "0.1.1", "nuxt-build-cache": "0.1.1",
"overlayscrollbars": "2.10.0", "overlayscrollbars": "2.10.0",
"overlayscrollbars-vue": "0.5.9", "overlayscrollbars-vue": "0.5.9",
"prettier": "^3.3.3",
"sass": "1.81.0", "sass": "1.81.0",
"vue": "3.5.13" "vue": "3.5.13"
},
"private": true,
"scripts": {
"build": "tauri build",
"dev": "tauri dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"format": "prettier --write \"**/*.{js,ts,vue,scss,css,json,md}\""
},
"type": "module",
"devDependencies": {
"prettier-plugin-vue": "^1.1.6"
} }
} }

View file

@ -1,8 +1,23 @@
<template> <template>
<div class="bg" @keydown.down.prevent="selectNext" @keydown.up.prevent="selectPrevious" <div
@keydown.enter.prevent="pasteSelectedItem" @keydown.esc="hideApp" tabindex="0"> class="bg"
<input ref="searchInput" v-model="searchQuery" @input="searchHistory" autocorrect="off" autocapitalize="off" @keydown.down.prevent="selectNext"
spellcheck="false" class="search" type="text" placeholder="Type to filter entries..." /> @keydown.up.prevent="selectPrevious"
@keydown.enter.prevent="pasteSelectedItem"
@keydown.esc="hideApp"
tabindex="0"
>
<input
ref="searchInput"
v-model="searchQuery"
@input="searchHistory"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
class="search"
type="text"
placeholder="Type to filter entries..."
/>
<div class="bottom-bar"> <div class="bottom-bar">
<div class="left"> <div class="left">
<img class="logo" width="18px" src="../public/logo.png" alt="" /> <img class="logo" width="18px" src="../public/logo.png" alt="" />
@ -24,29 +39,62 @@
</div> </div>
</div> </div>
</div> </div>
<OverlayScrollbarsComponent class="results" ref="resultsContainer" <OverlayScrollbarsComponent
:options="{ scrollbars: { autoHide: 'scroll' } }"> class="results"
ref="resultsContainer"
:options="{ scrollbars: { autoHide: 'scroll' } }"
>
<template v-for="(group, groupIndex) in groupedHistory" :key="groupIndex"> <template v-for="(group, groupIndex) in groupedHistory" :key="groupIndex">
<div class="time-separator">{{ group.label }}</div> <div class="time-separator">{{ group.label }}</div>
<div v-for="(item, index) in group.items" :key="item.id" :class="[ <div
'result clothoid-corner', v-for="(item, index) in group.items"
{ selected: isSelected(groupIndex, index) }, :key="item.id"
]" @click="selectItem(groupIndex, index)" :ref="(el) => { :class="['result clothoid-corner', { selected: isSelected(groupIndex, index) }]"
if (isSelected(groupIndex, index)) @click="selectItem(groupIndex, index)"
selectedElement = el as HTMLElement; :ref="
(el) => {
if (isSelected(groupIndex, index)) selectedElement = el as HTMLElement;
} }
"> "
>
<template v-if="item.content_type === 'image'"> <template v-if="item.content_type === 'image'">
<img v-if="!imageLoading && !imageLoadError" :src="getComputedImageUrl(item)" alt="Image" class="image" <img
@error="onImageError" /> v-if="!imageLoading && !imageLoadError"
<img v-if="imageLoading || imageLoadError" src="../public/icons/Image.svg" class="icon" /> :src="getComputedImageUrl(item)"
alt="Image"
class="image"
@error="onImageError"
/>
<img
v-if="imageLoading || imageLoadError"
src="../public/icons/Image.svg"
class="icon"
/>
</template> </template>
<img v-else-if="hasFavicon(item.favicon ?? '')" :src="getFaviconFromDb(item.favicon ?? '')" alt="Favicon" <img
class="favicon" /> v-else-if="hasFavicon(item.favicon ?? '')"
<img src="../public/icons/File.svg" class="icon" v-else-if="item.content_type === 'files'" /> :src="getFaviconFromDb(item.favicon ?? '')"
<img src="../public/icons/Text.svg" class="icon" v-else-if="item.content_type === 'text'" /> alt="Favicon"
<img src="../public/icons/Code.svg" class="icon" v-else-if="item.content_type === 'code'" /> class="favicon"
<span v-if="item.content_type === 'image'">Image ({{ item.dimensions || "Loading..." }})</span> />
<img
src="../public/icons/File.svg"
class="icon"
v-else-if="item.content_type === 'files'"
/>
<img
src="../public/icons/Text.svg"
class="icon"
v-else-if="item.content_type === 'text'"
/>
<img
src="../public/icons/Code.svg"
class="icon"
v-else-if="item.content_type === 'code'"
/>
<span v-if="item.content_type === 'image'"
>Image ({{ item.dimensions || "Loading..." }})</span
>
<span v-else>{{ truncateContent(item.content) }}</span> <span v-else>{{ truncateContent(item.content) }}</span>
</div> </div>
</template> </template>
@ -55,8 +103,12 @@
<img :src="getComputedImageUrl(selectedItem)" alt="Image" class="image" /> <img :src="getComputedImageUrl(selectedItem)" alt="Image" class="image" />
</div> </div>
<OverlayScrollbarsComponent v-else class="content"> <OverlayScrollbarsComponent v-else class="content">
<img v-if="selectedItem?.content && isYoutubeWatchUrl(selectedItem.content)" <img
:src="getYoutubeThumbnail(selectedItem.content)" alt="YouTube Thumbnail" class="full-image" /> v-if="selectedItem?.content && isYoutubeWatchUrl(selectedItem.content)"
:src="getYoutubeThumbnail(selectedItem.content)"
alt="YouTube Thumbnail"
class="full-image"
/>
<span v-else>{{ selectedItem?.content || "" }}</span> <span v-else>{{ selectedItem?.content || "" }}</span>
</OverlayScrollbarsComponent> </OverlayScrollbarsComponent>
<Noise /> <Noise />
@ -64,50 +116,48 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, watch, nextTick, shallowRef } from "vue"; import Database from "@tauri-apps/plugin-sql";
import Database from "@tauri-apps/plugin-sql"; import type { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue"; import { computed, nextTick, onMounted, ref, shallowRef, watch } from "vue";
import "overlayscrollbars/overlayscrollbars.css"; import "overlayscrollbars/overlayscrollbars.css";
import { app, window } from "@tauri-apps/api"; import { app, window } from "@tauri-apps/api";
import { platform } from "@tauri-apps/plugin-os"; import { invoke } from "@tauri-apps/api/core";
import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event";
import { enable, isEnabled } from "@tauri-apps/plugin-autostart"; import { enable, isEnabled } from "@tauri-apps/plugin-autostart";
import { listen } from "@tauri-apps/api/event"; import { readFile } from "@tauri-apps/plugin-fs";
import { readFile } from "@tauri-apps/plugin-fs"; import { platform } from "@tauri-apps/plugin-os";
interface HistoryItem { interface HistoryItem {
id: number; id: number;
content: string; content: string;
content_type: string; content_type: string;
timestamp: string; timestamp: string;
favicon?: string; favicon?: string;
dimensions?: string; dimensions?: string;
} }
interface GroupedHistory { interface GroupedHistory {
label: string; label: string;
items: HistoryItem[]; items: HistoryItem[];
} }
const db: Ref<Database | null> = ref(null); const db: Ref<Database | null> = ref(null);
const history: Ref<HistoryItem[]> = ref([]); const history: Ref<HistoryItem[]> = ref([]);
const chunkSize: number = 50; const chunkSize: number = 50;
let offset: number = 0; let offset = 0;
let isLoading: boolean = false; let isLoading = false;
const resultsContainer: Ref<InstanceType< const resultsContainer: Ref<InstanceType<typeof OverlayScrollbarsComponent> | null> = ref(null);
typeof OverlayScrollbarsComponent const searchQuery: Ref<string> = ref("");
> | null> = ref(null); const selectedGroupIndex: Ref<number> = ref(0);
const searchQuery: Ref<string> = ref(""); const selectedItemIndex: Ref<number> = ref(0);
const selectedGroupIndex: Ref<number> = ref(0); const selectedElement: Ref<HTMLElement | null> = ref(null);
const selectedItemIndex: Ref<number> = ref(0); const searchInput: Ref<HTMLInputElement | null> = ref(null);
const selectedElement: Ref<HTMLElement | null> = ref(null); const os: Ref<string> = ref("");
const searchInput: Ref<HTMLInputElement | null> = ref(null); const imageLoadError = ref(false);
const os: Ref<string> = ref(""); const imageLoading = ref(true);
const imageLoadError = ref(false); const imageUrls: Ref<Record<number, string>> = shallowRef({});
const imageLoading = ref(true);
const imageUrls: Ref<Record<number, string>> = shallowRef({});
const groupedHistory: ComputedRef<GroupedHistory[]> = computed(() => { const groupedHistory: ComputedRef<GroupedHistory[]> = computed(() => {
const now = new Date(); const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
@ -143,10 +193,7 @@ const groupedHistory: ComputedRef<GroupedHistory[]> = computed(() => {
if (itemDate.toDateString() === today.toDateString()) { if (itemDate.toDateString() === today.toDateString()) {
groups[0].items.push(item); groups[0].items.push(item);
} else if ( } else if (itemDate.toDateString() === new Date(today.getTime() - 86400000).toDateString()) {
itemDate.toDateString() ===
new Date(today.getTime() - 86400000).toDateString()
) {
groups[1].items.push(item); groups[1].items.push(item);
} else if (itemYear === thisYear && itemWeek === thisWeek) { } else if (itemYear === thisYear && itemWeek === thisWeek) {
groups[2].items.push(item); groups[2].items.push(item);
@ -160,21 +207,18 @@ const groupedHistory: ComputedRef<GroupedHistory[]> = computed(() => {
}); });
return groups.filter((group) => group.items.length > 0); return groups.filter((group) => group.items.length > 0);
}); });
const selectedItem: ComputedRef<HistoryItem | null> = computed(() => { const selectedItem: ComputedRef<HistoryItem | null> = computed(() => {
const group = groupedHistory.value[selectedGroupIndex.value]; const group = groupedHistory.value[selectedGroupIndex.value];
return group ? group.items[selectedItemIndex.value] : null; return group ? group.items[selectedItemIndex.value] : null;
}); });
const isSelected = (groupIndex: number, itemIndex: number): boolean => { const isSelected = (groupIndex: number, itemIndex: number): boolean => {
return ( return selectedGroupIndex.value === groupIndex && selectedItemIndex.value === itemIndex;
selectedGroupIndex.value === groupIndex && };
selectedItemIndex.value === itemIndex
);
};
const searchHistory = async (): Promise<void> => { const searchHistory = async (): Promise<void> => {
if (!db.value) return; if (!db.value) return;
history.value = []; history.value = [];
@ -195,9 +239,9 @@ const searchHistory = async (): Promise<void> => {
return item; return item;
}) })
); );
}; };
const selectNext = (): void => { const selectNext = (): void => {
const currentGroup = groupedHistory.value[selectedGroupIndex.value]; const currentGroup = groupedHistory.value[selectedGroupIndex.value];
if (selectedItemIndex.value < currentGroup.items.length - 1) { if (selectedItemIndex.value < currentGroup.items.length - 1) {
selectedItemIndex.value++; selectedItemIndex.value++;
@ -206,30 +250,29 @@ const selectNext = (): void => {
selectedItemIndex.value = 0; selectedItemIndex.value = 0;
} }
scrollToSelectedItem(); scrollToSelectedItem();
}; };
const selectPrevious = (): void => { const selectPrevious = (): void => {
if (selectedItemIndex.value > 0) { if (selectedItemIndex.value > 0) {
selectedItemIndex.value--; selectedItemIndex.value--;
} else if (selectedGroupIndex.value > 0) { } else if (selectedGroupIndex.value > 0) {
selectedGroupIndex.value--; selectedGroupIndex.value--;
selectedItemIndex.value = selectedItemIndex.value = groupedHistory.value[selectedGroupIndex.value].items.length - 1;
groupedHistory.value[selectedGroupIndex.value].items.length - 1;
} }
scrollToSelectedItem(); scrollToSelectedItem();
}; };
const selectItem = (groupIndex: number, itemIndex: number): void => { const selectItem = (groupIndex: number, itemIndex: number): void => {
selectedGroupIndex.value = groupIndex; selectedGroupIndex.value = groupIndex;
selectedItemIndex.value = itemIndex; selectedItemIndex.value = itemIndex;
scrollToSelectedItem(); scrollToSelectedItem();
}; };
const pasteSelectedItem = async (): Promise<void> => { const pasteSelectedItem = async (): Promise<void> => {
if (!selectedItem.value) return; if (!selectedItem.value) return;
let content = selectedItem.value.content; let content = selectedItem.value.content;
let contentType: String = selectedItem.value.content_type; const contentType: string = selectedItem.value.content_type;
if (contentType === "image") { if (contentType === "image") {
try { try {
content = readFile(content).toString(); content = readFile(content).toString();
@ -243,30 +286,27 @@ const pasteSelectedItem = async (): Promise<void> => {
content, content,
contentType, contentType,
}); });
}; };
const truncateContent = (content: string): string => { const truncateContent = (content: string): string => {
const maxWidth = 284; const maxWidth = 284;
const charWidth = 9; const charWidth = 9;
const maxChars = Math.floor(maxWidth / charWidth); const maxChars = Math.floor(maxWidth / charWidth);
return content.length > maxChars return content.length > maxChars ? `${content.slice(0, maxChars - 3)}...` : content;
? content.slice(0, maxChars - 3) + "..." };
: content;
};
const hasFavicon = (str: string): boolean => { const hasFavicon = (str: string): boolean => {
return str.trim() !== ""; return str.trim() !== "";
}; };
const isYoutubeWatchUrl = (url: string): boolean => { const isYoutubeWatchUrl = (url: string): boolean => {
return ( return (
/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/watch\?v=[\w-]+/.test( /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/watch\?v=[\w-]+/.test(url) ||
url /^(https?:\/\/)?(www\.)?youtu\.be\/[\w-]+/.test(url)
) || /^(https?:\/\/)?(www\.)?youtu\.be\/[\w-]+/.test(url)
); );
}; };
const getYoutubeThumbnail = (url: string): string => { const getYoutubeThumbnail = (url: string): string => {
let videoId; let videoId;
if (url.includes("youtu.be")) { if (url.includes("youtu.be")) {
videoId = url.split("youtu.be/")[1]; videoId = url.split("youtu.be/")[1];
@ -274,13 +314,13 @@ const getYoutubeThumbnail = (url: string): string => {
videoId = url.match(/[?&]v=([^&]+)/)?.[1]; videoId = url.match(/[?&]v=([^&]+)/)?.[1];
} }
return `https://img.youtube.com/vi/${videoId}/0.jpg`; return `https://img.youtube.com/vi/${videoId}/0.jpg`;
}; };
const getFaviconFromDb = (favicon: string): string => { const getFaviconFromDb = (favicon: string): string => {
return `data:image/png;base64,${favicon}`; return `data:image/png;base64,${favicon}`;
}; };
const getImageDimensions = (path: string): Promise<string> => { const getImageDimensions = (path: string): Promise<string> => {
return new Promise(async (resolve) => { return new Promise(async (resolve) => {
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {
@ -306,9 +346,9 @@ const getImageDimensions = (path: string): Promise<string> => {
resolve("0x0"); resolve("0x0");
} }
}); });
}; };
const getImageUrl = async (path: string): Promise<string> => { const getImageUrl = async (path: string): Promise<string> => {
const isWindows = path.includes("\\"); const isWindows = path.includes("\\");
const separator = isWindows ? "\\" : "/"; const separator = isWindows ? "\\" : "/";
const filename = path.split(separator).pop(); const filename = path.split(separator).pop();
@ -331,9 +371,9 @@ const getImageUrl = async (path: string): Promise<string> => {
imageLoading.value = false; imageLoading.value = false;
return ""; return "";
} }
}; };
const getComputedImageUrl = (item: HistoryItem): string => { const getComputedImageUrl = (item: HistoryItem): string => {
if (!imageUrls.value[item.id]) { if (!imageUrls.value[item.id]) {
imageUrls.value[item.id] = ""; imageUrls.value[item.id] = "";
getImageUrl(item.content) getImageUrl(item.content)
@ -346,9 +386,9 @@ const getComputedImageUrl = (item: HistoryItem): string => {
}); });
} }
return imageUrls.value[item.id] || ""; return imageUrls.value[item.id] || "";
}; };
const loadHistoryChunk = async (): Promise<void> => { const loadHistoryChunk = async (): Promise<void> => {
if (!db.value || isLoading) return; if (!db.value || isLoading) return;
isLoading = true; isLoading = true;
@ -386,9 +426,9 @@ const loadHistoryChunk = async (): Promise<void> => {
history.value = [...history.value, ...processedChunk]; history.value = [...history.value, ...processedChunk];
offset += chunkSize; offset += chunkSize;
isLoading = false; isLoading = false;
}; };
const handleScroll = (): void => { const handleScroll = (): void => {
if (!resultsContainer.value) return; if (!resultsContainer.value) return;
const viewport = resultsContainer.value?.osInstance()?.elements().viewport; const viewport = resultsContainer.value?.osInstance()?.elements().viewport;
@ -399,20 +439,20 @@ const handleScroll = (): void => {
if (scrollHeight - scrollTop - clientHeight < 100) { if (scrollHeight - scrollTop - clientHeight < 100) {
loadHistoryChunk(); loadHistoryChunk();
} }
}; };
const hideApp = async (): Promise<void> => { const hideApp = async (): Promise<void> => {
await app.hide(); await app.hide();
await window.getCurrentWindow().hide(); await window.getCurrentWindow().hide();
}; };
const focusSearchInput = (): void => { const focusSearchInput = (): void => {
nextTick(() => { nextTick(() => {
searchInput.value?.focus(); searchInput.value?.focus();
}); });
}; };
const scrollToSelectedItem = (forceScrollTop: boolean = false): void => { const scrollToSelectedItem = (forceScrollTop = false): void => {
nextTick(() => { nextTick(() => {
if (selectedElement.value && resultsContainer.value) { if (selectedElement.value && resultsContainer.value) {
const osInstance = resultsContainer.value.osInstance(); const osInstance = resultsContainer.value.osInstance();
@ -440,24 +480,24 @@ const scrollToSelectedItem = (forceScrollTop: boolean = false): void => {
} }
} }
}); });
}; };
const onImageError = (): void => { const onImageError = (): void => {
imageLoadError.value = true; imageLoadError.value = true;
imageLoading.value = false; imageLoading.value = false;
}; };
watch([selectedGroupIndex, selectedItemIndex], () => { watch([selectedGroupIndex, selectedItemIndex], () => {
scrollToSelectedItem(); scrollToSelectedItem();
}); });
watch(searchQuery, () => { watch(searchQuery, () => {
searchHistory(); searchHistory();
}); });
const lastUpdateTime = ref<number>(Date.now()); const lastUpdateTime = ref<number>(Date.now());
onMounted(async () => { onMounted(async () => {
db.value = await Database.load("sqlite:data.db"); db.value = await Database.load("sqlite:data.db");
await loadHistoryChunk(); await loadHistoryChunk();
@ -487,8 +527,7 @@ onMounted(async () => {
const previousGroupIndex = selectedGroupIndex.value; const previousGroupIndex = selectedGroupIndex.value;
const previousItemIndex = selectedItemIndex.value; const previousItemIndex = selectedItemIndex.value;
const previousScroll = const previousScroll =
resultsContainer.value?.osInstance()?.elements().viewport?.scrollTop || resultsContainer.value?.osInstance()?.elements().viewport?.scrollTop || 0;
0;
history.value = []; history.value = [];
offset = 0; offset = 0;
@ -521,13 +560,11 @@ onMounted(async () => {
} }
os.value = platform(); os.value = platform();
}); });
watch([selectedGroupIndex, selectedItemIndex], () => watch([selectedGroupIndex, selectedItemIndex], () => scrollToSelectedItem(false));
scrollToSelectedItem(false)
);
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@use "~/assets/css/index.scss"; @use "~/assets/css/index.scss";
</style> </style>

View file

@ -47,77 +47,85 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { invoke } from '@tauri-apps/api/core'; import { invoke } from "@tauri-apps/api/core";
import { onMounted, onUnmounted, reactive, ref } from 'vue'; import { platform } from "@tauri-apps/plugin-os";
import { platform } from '@tauri-apps/plugin-os'; import { onMounted, onUnmounted, reactive, ref } from "vue";
import { useRouter } from 'vue-router'; import { useRouter } from "vue-router";
const activeModifiers = reactive<Set<string>>(new Set()); const activeModifiers = reactive<Set<string>>(new Set());
const isKeybindInputFocused = ref(false); const isKeybindInputFocused = ref(false);
const keybind = ref<string[]>([]); const keybind = ref<string[]>([]);
const keybindInput = ref<HTMLElement | null>(null); const keybindInput = ref<HTMLElement | null>(null);
const lastBlurTime = ref(0); const lastBlurTime = ref(0);
const os = ref(''); const os = ref("");
const router = useRouter(); const router = useRouter();
const keyToDisplayMap: Record<string, string> = { const keyToDisplayMap: Record<string, string> = {
' ': 'Space', " ": "Space",
Alt: 'Alt', Alt: "Alt",
AltLeft: 'Alt L', AltLeft: "Alt L",
AltRight: 'Alt R', AltRight: "Alt R",
ArrowDown: '↓', ArrowDown: "↓",
ArrowLeft: '←', ArrowLeft: "←",
ArrowRight: '→', ArrowRight: "→",
ArrowUp: '↑', ArrowUp: "↑",
Control: 'Ctrl', Control: "Ctrl",
ControlLeft: 'Ctrl L', ControlLeft: "Ctrl L",
ControlRight: 'Ctrl R', ControlRight: "Ctrl R",
Enter: '↵', Enter: "↵",
Meta: 'Meta', Meta: "Meta",
MetaLeft: 'Meta L', MetaLeft: "Meta L",
MetaRight: 'Meta R', MetaRight: "Meta R",
Shift: '⇧', Shift: "⇧",
ShiftLeft: '⇧ L', ShiftLeft: "⇧ L",
ShiftRight: '⇧ R', ShiftRight: "⇧ R",
}; };
const modifierKeySet = new Set([ const modifierKeySet = new Set([
'Alt', 'AltLeft', 'AltRight', "Alt",
'Control', 'ControlLeft', 'ControlRight', "AltLeft",
'Meta', 'MetaLeft', 'MetaRight', "AltRight",
'Shift', 'ShiftLeft', 'ShiftRight' "Control",
]); "ControlLeft",
"ControlRight",
"Meta",
"MetaLeft",
"MetaRight",
"Shift",
"ShiftLeft",
"ShiftRight",
]);
const isModifier = (key: string): boolean => { const isModifier = (key: string): boolean => {
return modifierKeySet.has(key); return modifierKeySet.has(key);
}; };
const keyToDisplay = (key: string): string => { const keyToDisplay = (key: string): string => {
return keyToDisplayMap[key] || key; return keyToDisplayMap[key] || key;
}; };
const updateKeybind = () => { const updateKeybind = () => {
const modifiers = Array.from(activeModifiers).sort(); const modifiers = Array.from(activeModifiers).sort();
const nonModifiers = keybind.value.filter(key => !isModifier(key)); const nonModifiers = keybind.value.filter((key) => !isModifier(key));
keybind.value = [...modifiers, ...nonModifiers]; keybind.value = [...modifiers, ...nonModifiers];
}; };
const onBlur = () => { const onBlur = () => {
isKeybindInputFocused.value = false; isKeybindInputFocused.value = false;
lastBlurTime.value = Date.now(); lastBlurTime.value = Date.now();
}; };
const onFocus = () => { const onFocus = () => {
isKeybindInputFocused.value = true; isKeybindInputFocused.value = true;
activeModifiers.clear(); activeModifiers.clear();
keybind.value = []; keybind.value = [];
}; };
const onKeyDown = (event: KeyboardEvent) => { const onKeyDown = (event: KeyboardEvent) => {
event.preventDefault(); event.preventDefault();
const key = event.code; const key = event.code;
if (key === 'Escape') { if (key === "Escape") {
if (keybindInput.value) { if (keybindInput.value) {
keybindInput.value.blur(); keybindInput.value.blur();
} }
@ -127,50 +135,51 @@ const onKeyDown = (event: KeyboardEvent) => {
if (isModifier(key)) { if (isModifier(key)) {
activeModifiers.add(key); activeModifiers.add(key);
} else if (!keybind.value.includes(key)) { } else if (!keybind.value.includes(key)) {
keybind.value = keybind.value.filter(k => isModifier(k)); keybind.value = keybind.value.filter((k) => isModifier(k));
keybind.value.push(key); keybind.value.push(key);
} }
updateKeybind(); updateKeybind();
}; };
const saveKeybind = async () => { const saveKeybind = async () => {
console.log('New:', keybind.value); console.log("New:", keybind.value);
const oldKeybind = await invoke<string[]>('get_keybind'); const oldKeybind = await invoke<string[]>("get_keybind");
console.log('Old:', oldKeybind); console.log("Old:", oldKeybind);
await invoke('save_keybind', { keybind: keybind.value }); await invoke("save_keybind", { keybind: keybind.value });
}; };
const handleGlobalKeyDown = (event: KeyboardEvent) => { const handleGlobalKeyDown = (event: KeyboardEvent) => {
const now = Date.now(); const now = Date.now();
if ( if (
(os.value === 'macos' (os.value === "macos"
? (event.code === 'MetaLeft' || event.code === 'MetaRight') && event.key === 'Enter' ? (event.code === "MetaLeft" || event.code === "MetaRight") && event.key === "Enter"
: (event.code === 'ControlLeft' || event.code === 'ControlRight') && event.key === 'Enter') && : (event.code === "ControlLeft" || event.code === "ControlRight") &&
event.key === "Enter") &&
!isKeybindInputFocused.value !isKeybindInputFocused.value
) { ) {
event.preventDefault(); event.preventDefault();
saveKeybind(); saveKeybind();
} else if ( } else if (
event.key === 'Escape' && event.key === "Escape" &&
!isKeybindInputFocused.value && !isKeybindInputFocused.value &&
now - lastBlurTime.value > 100 now - lastBlurTime.value > 100
) { ) {
event.preventDefault(); event.preventDefault();
router.push('/'); router.push("/");
} }
}; };
onMounted(() => { onMounted(() => {
os.value = platform(); os.value = platform();
window.addEventListener('keydown', handleGlobalKeyDown); window.addEventListener("keydown", handleGlobalKeyDown);
}); });
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('keydown', handleGlobalKeyDown); window.removeEventListener("keydown", handleGlobalKeyDown);
}); });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@use '~/assets/css/keybind.scss'; @use "~/assets/css/keybind.scss";
</style> </style>

8
src-tauri/Cargo.lock generated
View file

@ -4508,9 +4508,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.132" version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
dependencies = [ dependencies = [
"itoa 1.0.11", "itoa 1.0.11",
"memchr", "memchr",
@ -6082,9 +6082,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.3" version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna",

View file

@ -10,7 +10,7 @@ rust-version = "1.70"
tauri-build = { version = "2.0.3", features = [] } tauri-build = { version = "2.0.3", features = [] }
[dependencies] [dependencies]
tauri = { version = "2.0.1", features = [ tauri = { version = "2.1.1", features = [
"macos-private-api", "macos-private-api",
"tray-icon", "tray-icon",
"image-png", "image-png",
@ -27,17 +27,17 @@ tauri-plugin-global-shortcut = "2.0.1"
sqlx = { version = "0.8.2", features = ["runtime-tokio-native-tls", "sqlite"] } sqlx = { version = "0.8.2", features = ["runtime-tokio-native-tls", "sqlite"] }
serde = { version = "1.0.215", features = ["derive"] } serde = { version = "1.0.215", features = ["derive"] }
tokio = { version = "1.41.1", features = ["full"] } tokio = { version = "1.41.1", features = ["full"] }
serde_json = "1.0.132" serde_json = "1.0.133"
rdev = "0.5.3" rdev = "0.5.3"
rand = "0.8" rand = "0.8.5"
base64 = "0.22.1" base64 = "0.22.1"
image = "0.25.5" image = "0.25.5"
reqwest = { version = "0.12.9", features = ["blocking"] } reqwest = { version = "0.12.9", features = ["blocking"] }
url = "2.5.3" url = "2.5.4"
regex = "1.11.1" regex = "1.11.1"
sha2 = "0.10.6" sha2 = "0.10.8"
lazy_static = "1.4.0" lazy_static = "1.5.0"
time = "0.3" time = "0.3.36"
global-hotkey = "0.6.3" global-hotkey = "0.6.3"
[features] [features]

View file

@ -2,9 +2,7 @@
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default", "identifier": "default",
"description": "enables the default permissions", "description": "enables the default permissions",
"windows": [ "windows": ["main"],
"main"
],
"permissions": [ "permissions": [
"core:path:default", "core:path:default",
"core:event:default", "core:event:default",

View file

@ -6,6 +6,7 @@
mod api; mod api;
mod utils; mod utils;
use tauri::window::{Effect, EffectState, EffectsBuilder};
use tauri::Manager; use tauri::Manager;
use tauri::WebviewUrl; use tauri::WebviewUrl;
use tauri::WebviewWindow; use tauri::WebviewWindow;
@ -45,7 +46,9 @@ fn main() {
.visible(false) .visible(false)
.decorations(false) .decorations(false)
.transparent(true) .transparent(true)
.always_on_top(false) .always_on_top(true)
.content_protected(true)
.visible_on_all_workspaces(true)
.build()? .build()?
}; };
@ -68,6 +71,13 @@ fn main() {
api::updater::check_for_updates(app_handle).await; api::updater::check_for_updates(app_handle).await;
}); });
main_window.set_effects(
EffectsBuilder::new()
.effect(Effect::Popover)
.state(EffectState::Active)
.build(),
)?;
Ok(()) Ok(())
}) })
.on_window_event(|_app, _event| { .on_window_event(|_app, _event| {

View file

@ -1,6 +1,6 @@
{ {
"productName": "Qopy", "productName": "Qopy",
"version": "0.2.0", "version": "0.2.1",
"identifier": "net.pandadev.qopy", "identifier": "net.pandadev.qopy",
"build": { "build": {
"frontendDist": "../dist", "frontendDist": "../dist",
@ -51,9 +51,7 @@
"plugins": { "plugins": {
"updater": { "updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNDIzNjA1QjE0NjU1OTkKUldTWlZVYXhCVFpDRWNvNmt0UE5lQmZkblEyZGZiZ2tHelJvT2YvNVpLU1RIM1RKZFQrb2tzWWwK", "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNDIzNjA1QjE0NjU1OTkKUldTWlZVYXhCVFpDRWNvNmt0UE5lQmZkblEyZGZiZ2tHelJvT2YvNVpLU1RIM1RKZFQrb2tzWWwK",
"endpoints": [ "endpoints": ["https://qopy.pandadev.net/"]
"https://qopy.pandadev.net/"
]
} }
}, },
"$schema": "../node_modules/@tauri-apps/cli/schema.json" "$schema": "../node_modules/@tauri-apps/cli/schema.json"