mirror of
https://github.com/0PandaDEV/Qopy.git
synced 2025-04-21 13:14:04 +02:00
parent
f2f554074b
commit
d0b551b3fd
22 changed files with 1171 additions and 1493 deletions
214
.github/workflows/release.yml
vendored
214
.github/workflows/release.yml
vendored
|
@ -1,214 +0,0 @@
|
||||||
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 }}
|
|
|
@ -1,39 +0,0 @@
|
||||||
# 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
10
.prettierrc
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": false,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"useTabs": true,
|
|
||||||
"printWidth": 100,
|
|
||||||
"trailingComma": "es5",
|
|
||||||
"vueIndentScriptAndStyle": true,
|
|
||||||
"plugins": ["prettier-plugin-vue"]
|
|
||||||
}
|
|
|
@ -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 | `` |
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,6 @@ 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
|
||||||
|
@ -73,7 +72,6 @@ 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
32
app.vue
|
@ -5,27 +5,27 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { app, window } from "@tauri-apps/api";
|
import { listen } from '@tauri-apps/api/event'
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { app, window } from '@tauri-apps/api';
|
||||||
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 @@
|
||||||
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,
|
||||||
|
|
|
@ -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 0.2s;
|
transition: all .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 0.2s;
|
transition: all .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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
35
package.json
35
package.json
|
@ -1,31 +1,26 @@
|
||||||
{
|
{
|
||||||
"name": "nuxt-app",
|
"name": "nuxt-app",
|
||||||
"dependencies": {
|
|
||||||
"@tauri-apps/api": "2.1.1",
|
|
||||||
"@tauri-apps/cli": "2.1.0",
|
|
||||||
"@tauri-apps/plugin-autostart": "2.0.0",
|
|
||||||
"@tauri-apps/plugin-fs": "2.0.2",
|
|
||||||
"@tauri-apps/plugin-os": "2.0.0",
|
|
||||||
"@tauri-apps/plugin-sql": "2.0.1",
|
|
||||||
"nuxt": "3.14.1592",
|
|
||||||
"nuxt-build-cache": "0.1.1",
|
|
||||||
"overlayscrollbars": "2.10.0",
|
|
||||||
"overlayscrollbars-vue": "0.5.9",
|
|
||||||
"prettier": "^3.3.3",
|
|
||||||
"sass": "1.81.0",
|
|
||||||
"vue": "3.5.13"
|
|
||||||
},
|
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tauri build",
|
"build": "tauri build",
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare",
|
"postinstall": "nuxt prepare"
|
||||||
"format": "prettier --write \"**/*.{js,ts,vue,scss,css,json,md}\""
|
|
||||||
},
|
},
|
||||||
"type": "module",
|
"dependencies": {
|
||||||
"devDependencies": {
|
"@tauri-apps/api": "2.0.3",
|
||||||
"prettier-plugin-vue": "^1.1.6"
|
"@tauri-apps/cli": "2.1.0",
|
||||||
|
"@tauri-apps/plugin-autostart": "2.0.0",
|
||||||
|
"@tauri-apps/plugin-fs": "2.0.2",
|
||||||
|
"@tauri-apps/plugin-os": "2.0.0",
|
||||||
|
"@tauri-apps/plugin-sql": "2.0.1",
|
||||||
|
"nuxt": "3.14.159",
|
||||||
|
"nuxt-build-cache": "0.1.1",
|
||||||
|
"overlayscrollbars": "2.10.0",
|
||||||
|
"overlayscrollbars-vue": "0.5.9",
|
||||||
|
"sass": "1.81.0",
|
||||||
|
"vue": "3.5.13"
|
||||||
}
|
}
|
||||||
}
|
}
|
295
pages/index.vue
295
pages/index.vue
|
@ -1,23 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div class="bg" @keydown.down.prevent="selectNext" @keydown.up.prevent="selectPrevious"
|
||||||
class="bg"
|
@keydown.enter.prevent="pasteSelectedItem" @keydown.esc="hideApp" tabindex="0">
|
||||||
@keydown.down.prevent="selectNext"
|
<input ref="searchInput" v-model="searchQuery" @input="searchHistory" autocorrect="off" autocapitalize="off"
|
||||||
@keydown.up.prevent="selectPrevious"
|
spellcheck="false" class="search" type="text" placeholder="Type to filter entries..." />
|
||||||
@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="" />
|
||||||
|
@ -39,62 +24,29 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<OverlayScrollbarsComponent
|
<OverlayScrollbarsComponent class="results" ref="resultsContainer"
|
||||||
class="results"
|
:options="{ scrollbars: { autoHide: 'scroll' } }">
|
||||||
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
|
<div v-for="(item, index) in group.items" :key="item.id" :class="[
|
||||||
v-for="(item, index) in group.items"
|
'result clothoid-corner',
|
||||||
:key="item.id"
|
{ selected: isSelected(groupIndex, index) },
|
||||||
:class="['result clothoid-corner', { selected: isSelected(groupIndex, index) }]"
|
]" @click="selectItem(groupIndex, index)" :ref="(el) => {
|
||||||
@click="selectItem(groupIndex, index)"
|
if (isSelected(groupIndex, index))
|
||||||
:ref="
|
selectedElement = el as HTMLElement;
|
||||||
(el) => {
|
|
||||||
if (isSelected(groupIndex, index)) selectedElement = el as HTMLElement;
|
|
||||||
}
|
}
|
||||||
"
|
">
|
||||||
>
|
|
||||||
<template v-if="item.content_type === 'image'">
|
<template v-if="item.content_type === 'image'">
|
||||||
<img
|
<img v-if="!imageLoading && !imageLoadError" :src="getComputedImageUrl(item)" alt="Image" class="image"
|
||||||
v-if="!imageLoading && !imageLoadError"
|
@error="onImageError" />
|
||||||
:src="getComputedImageUrl(item)"
|
<img v-if="imageLoading || imageLoadError" src="../public/icons/Image.svg" class="icon" />
|
||||||
alt="Image"
|
|
||||||
class="image"
|
|
||||||
@error="onImageError"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
v-if="imageLoading || imageLoadError"
|
|
||||||
src="../public/icons/Image.svg"
|
|
||||||
class="icon"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<img
|
<img v-else-if="hasFavicon(item.favicon ?? '')" :src="getFaviconFromDb(item.favicon ?? '')" alt="Favicon"
|
||||||
v-else-if="hasFavicon(item.favicon ?? '')"
|
class="favicon" />
|
||||||
:src="getFaviconFromDb(item.favicon ?? '')"
|
<img src="../public/icons/File.svg" class="icon" v-else-if="item.content_type === 'files'" />
|
||||||
alt="Favicon"
|
<img src="../public/icons/Text.svg" class="icon" v-else-if="item.content_type === 'text'" />
|
||||||
class="favicon"
|
<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>
|
||||||
<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>
|
||||||
|
@ -103,12 +55,8 @@
|
||||||
<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
|
<img v-if="selectedItem?.content && isYoutubeWatchUrl(selectedItem.content)"
|
||||||
v-if="selectedItem?.content && isYoutubeWatchUrl(selectedItem.content)"
|
:src="getYoutubeThumbnail(selectedItem.content)" alt="YouTube Thumbnail" class="full-image" />
|
||||||
: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 />
|
||||||
|
@ -116,48 +64,50 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Database from "@tauri-apps/plugin-sql";
|
import { ref, computed, onMounted, watch, nextTick, shallowRef } from "vue";
|
||||||
import type { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
import Database from "@tauri-apps/plugin-sql";
|
||||||
import { computed, nextTick, onMounted, ref, shallowRef, watch } from "vue";
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||||
import "overlayscrollbars/overlayscrollbars.css";
|
import "overlayscrollbars/overlayscrollbars.css";
|
||||||
import { app, window } from "@tauri-apps/api";
|
import { app, window } from "@tauri-apps/api";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { platform } from "@tauri-apps/plugin-os";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { enable, isEnabled } from "@tauri-apps/plugin-autostart";
|
import { enable, isEnabled } from "@tauri-apps/plugin-autostart";
|
||||||
import { readFile } from "@tauri-apps/plugin-fs";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { platform } from "@tauri-apps/plugin-os";
|
import { readFile } from "@tauri-apps/plugin-fs";
|
||||||
|
|
||||||
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 = 0;
|
let offset: number = 0;
|
||||||
let isLoading = false;
|
let isLoading: boolean = false;
|
||||||
const resultsContainer: Ref<InstanceType<typeof OverlayScrollbarsComponent> | null> = ref(null);
|
const resultsContainer: Ref<InstanceType<
|
||||||
const searchQuery: Ref<string> = ref("");
|
typeof OverlayScrollbarsComponent
|
||||||
const selectedGroupIndex: Ref<number> = ref(0);
|
> | null> = ref(null);
|
||||||
const selectedItemIndex: Ref<number> = ref(0);
|
const searchQuery: Ref<string> = ref("");
|
||||||
const selectedElement: Ref<HTMLElement | null> = ref(null);
|
const selectedGroupIndex: Ref<number> = ref(0);
|
||||||
const searchInput: Ref<HTMLInputElement | null> = ref(null);
|
const selectedItemIndex: Ref<number> = ref(0);
|
||||||
const os: Ref<string> = ref("");
|
const selectedElement: Ref<HTMLElement | null> = ref(null);
|
||||||
const imageLoadError = ref(false);
|
const searchInput: Ref<HTMLInputElement | null> = ref(null);
|
||||||
const imageLoading = ref(true);
|
const os: Ref<string> = ref("");
|
||||||
const imageUrls: Ref<Record<number, string>> = shallowRef({});
|
const imageLoadError = ref(false);
|
||||||
|
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());
|
||||||
|
|
||||||
|
@ -193,7 +143,10 @@
|
||||||
|
|
||||||
if (itemDate.toDateString() === today.toDateString()) {
|
if (itemDate.toDateString() === today.toDateString()) {
|
||||||
groups[0].items.push(item);
|
groups[0].items.push(item);
|
||||||
} else if (itemDate.toDateString() === new Date(today.getTime() - 86400000).toDateString()) {
|
} else if (
|
||||||
|
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);
|
||||||
|
@ -207,18 +160,21 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
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 selectedGroupIndex.value === groupIndex && selectedItemIndex.value === itemIndex;
|
return (
|
||||||
};
|
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 = [];
|
||||||
|
@ -239,9 +195,9 @@
|
||||||
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++;
|
||||||
|
@ -250,29 +206,30 @@
|
||||||
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 = groupedHistory.value[selectedGroupIndex.value].items.length - 1;
|
selectedItemIndex.value =
|
||||||
|
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;
|
||||||
const contentType: string = selectedItem.value.content_type;
|
let contentType: String = selectedItem.value.content_type;
|
||||||
if (contentType === "image") {
|
if (contentType === "image") {
|
||||||
try {
|
try {
|
||||||
content = readFile(content).toString();
|
content = readFile(content).toString();
|
||||||
|
@ -286,27 +243,30 @@
|
||||||
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 ? `${content.slice(0, maxChars - 3)}...` : content;
|
return content.length > maxChars
|
||||||
};
|
? 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(url) ||
|
/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/watch\?v=[\w-]+/.test(
|
||||||
/^(https?:\/\/)?(www\.)?youtu\.be\/[\w-]+/.test(url)
|
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];
|
||||||
|
@ -314,13 +274,13 @@
|
||||||
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 = () => {
|
||||||
|
@ -346,9 +306,9 @@
|
||||||
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();
|
||||||
|
@ -371,9 +331,9 @@
|
||||||
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)
|
||||||
|
@ -386,9 +346,9 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
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;
|
||||||
|
@ -426,9 +386,9 @@
|
||||||
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;
|
||||||
|
@ -439,20 +399,20 @@
|
||||||
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 = false): void => {
|
const scrollToSelectedItem = (forceScrollTop: boolean = false): void => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (selectedElement.value && resultsContainer.value) {
|
if (selectedElement.value && resultsContainer.value) {
|
||||||
const osInstance = resultsContainer.value.osInstance();
|
const osInstance = resultsContainer.value.osInstance();
|
||||||
|
@ -480,24 +440,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
|
@ -527,7 +487,8 @@
|
||||||
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 || 0;
|
resultsContainer.value?.osInstance()?.elements().viewport?.scrollTop ||
|
||||||
|
0;
|
||||||
|
|
||||||
history.value = [];
|
history.value = [];
|
||||||
offset = 0;
|
offset = 0;
|
||||||
|
@ -560,11 +521,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
os.value = platform();
|
os.value = platform();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch([selectedGroupIndex, selectedItemIndex], () => scrollToSelectedItem(false));
|
watch([selectedGroupIndex, selectedItemIndex], () =>
|
||||||
|
scrollToSelectedItem(false)
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@use "~/assets/css/index.scss";
|
@use "~/assets/css/index.scss";
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -47,85 +47,77 @@
|
||||||
</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 { platform } from "@tauri-apps/plugin-os";
|
import { onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||||
import { onMounted, onUnmounted, reactive, ref } from "vue";
|
import { platform } from '@tauri-apps/plugin-os';
|
||||||
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",
|
'Alt', 'AltLeft', 'AltRight',
|
||||||
"AltLeft",
|
'Control', 'ControlLeft', 'ControlRight',
|
||||||
"AltRight",
|
'Meta', 'MetaLeft', 'MetaRight',
|
||||||
"Control",
|
'Shift', 'ShiftLeft', 'ShiftRight'
|
||||||
"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();
|
||||||
}
|
}
|
||||||
|
@ -135,51 +127,50 @@
|
||||||
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.code === 'ControlLeft' || event.code === 'ControlRight') && event.key === 'Enter') &&
|
||||||
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
8
src-tauri/Cargo.lock
generated
|
@ -4508,9 +4508,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.133"
|
version = "1.0.132"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
|
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
|
||||||
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.4"
|
version = "2.5.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
|
checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna",
|
||||||
|
|
|
@ -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.1.1", features = [
|
tauri = { version = "2.0.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.133"
|
serde_json = "1.0.132"
|
||||||
rdev = "0.5.3"
|
rdev = "0.5.3"
|
||||||
rand = "0.8.5"
|
rand = "0.8"
|
||||||
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.4"
|
url = "2.5.3"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.6"
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.4.0"
|
||||||
time = "0.3.36"
|
time = "0.3"
|
||||||
global-hotkey = "0.6.3"
|
global-hotkey = "0.6.3"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
"$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": ["main"],
|
"windows": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:path:default",
|
"core:path:default",
|
||||||
"core:event:default",
|
"core:event:default",
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
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;
|
||||||
|
@ -46,9 +45,7 @@ fn main() {
|
||||||
.visible(false)
|
.visible(false)
|
||||||
.decorations(false)
|
.decorations(false)
|
||||||
.transparent(true)
|
.transparent(true)
|
||||||
.always_on_top(true)
|
.always_on_top(false)
|
||||||
.content_protected(true)
|
|
||||||
.visible_on_all_workspaces(true)
|
|
||||||
.build()?
|
.build()?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -71,13 +68,6 @@ 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| {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"productName": "Qopy",
|
"productName": "Qopy",
|
||||||
"version": "0.2.1",
|
"version": "0.2.0",
|
||||||
"identifier": "net.pandadev.qopy",
|
"identifier": "net.pandadev.qopy",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
@ -51,7 +51,9 @@
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"updater": {
|
"updater": {
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNDIzNjA1QjE0NjU1OTkKUldTWlZVYXhCVFpDRWNvNmt0UE5lQmZkblEyZGZiZ2tHelJvT2YvNVpLU1RIM1RKZFQrb2tzWWwK",
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNDIzNjA1QjE0NjU1OTkKUldTWlZVYXhCVFpDRWNvNmt0UE5lQmZkblEyZGZiZ2tHelJvT2YvNVpLU1RIM1RKZFQrb2tzWWwK",
|
||||||
"endpoints": ["https://qopy.pandadev.net/"]
|
"endpoints": [
|
||||||
|
"https://qopy.pandadev.net/"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json"
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue