Compare commits

...

2 commits

79 changed files with 8565 additions and 8778 deletions

2
.github/FUNDING.yml vendored
View file

@ -1 +1 @@
github: 0pandadev
github: 0pandadev

View file

@ -1,75 +1,75 @@
name: "\U0001F41E Bug report"
description: Create a report to help me improve Qopy
labels: [Bug]
assignees:
- 0PandaDEV
body:
#
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
#
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
description: Steps to reproduce the behavior
value: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
validations:
required: false
- type: dropdown
id: os
attributes:
label: Operating system
options:
- Windows
- Linux
- macOS
validations:
required: true
- type: input
id: version
attributes:
label: Version of Qopy
placeholder: e.g. 0.1.0
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional context
description: Add any other context about the problem here.
validations:
required: false
name: "\U0001F41E Bug report"
description: Create a report to help me improve Qopy
labels: [Bug]
assignees:
- 0PandaDEV
body:
#
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
#
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
description: Steps to reproduce the behavior
value: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
validations:
required: false
- type: dropdown
id: os
attributes:
label: Operating system
options:
- Windows
- Linux
- macOS
validations:
required: true
- type: input
id: version
attributes:
label: Version of Qopy
placeholder: e.g. 0.1.0
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional context
description: Add any other context about the problem here.
validations:
required: false

View file

@ -1,19 +1,19 @@
name: "\U0001F4A1 Feature request"
description: Suggest an idea for Qopy
labels: [Feature]
assignees:
- 0PandaDEV
body:
#
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request!
#
- type: textarea
id: description
attributes:
label: Describe your requested feature
description: Give as many details as possible about your feature idea.
validations:
required: true
name: "\U0001F4A1 Feature request"
description: Suggest an idea for Qopy
labels: [Feature]
assignees:
- 0PandaDEV
body:
#
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request!
#
- type: textarea
id: description
attributes:
label: Describe your requested feature
description: Give as many details as possible about your feature idea.
validations:
required: true

View file

@ -1,40 +1,40 @@
#!/bin/bash
if [ -f .env ]; then
export $(cat .env | grep -v '^#' | xargs)
fi
set -e
required_vars=("APPLE_CERTIFICATE" "APPLE_CERTIFICATE_PASSWORD" "APPLE_ID" "APPLE_ID_PASSWORD" "KEYCHAIN_PASSWORD" "APP_BUNDLE_ID")
for var in "${required_vars[@]}"; do
if [ -z "${!var}" ]; then
exit 1
fi
done
bun run tauri build
rm -f certificate.p12
echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12 2>/dev/null
security import certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A 2>/dev/null
SIGNING_IDENTITY=$(security find-identity -v -p codesigning | grep "Apple Development" | head -1 | awk -F '"' '{print $2}')
if [ -z "$SIGNING_IDENTITY" ]; then
exit 1
fi
codesign --force --options runtime --sign "$SIGNING_IDENTITY" src-tauri/target/release/bundle/macos/*.app 2>/dev/null
rm -f certificate.p12
hdiutil create -volname "Qopy" -srcfolder src-tauri/target/release/bundle/dmg -ov -format UDZO Qopy.dmg
codesign --force --sign "$APPLE_CERTIFICATE" Qopy.dmg 2>/dev/null
xcrun notarytool submit Qopy.dmg --apple-id "$APPLE_ID" --password "$APPLE_ID_PASSWORD" --team-id "$APPLE_CERTIFICATE" --wait
xcrun stapler staple Qopy.dmg
exit 0
#!/bin/bash
if [ -f .env ]; then
export $(cat .env | grep -v '^#' | xargs)
fi
set -e
required_vars=("APPLE_CERTIFICATE" "APPLE_CERTIFICATE_PASSWORD" "APPLE_ID" "APPLE_ID_PASSWORD" "KEYCHAIN_PASSWORD" "APP_BUNDLE_ID")
for var in "${required_vars[@]}"; do
if [ -z "${!var}" ]; then
exit 1
fi
done
bun run tauri build
rm -f certificate.p12
echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12 2>/dev/null
security import certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A 2>/dev/null
SIGNING_IDENTITY=$(security find-identity -v -p codesigning | grep "Apple Development" | head -1 | awk -F '"' '{print $2}')
if [ -z "$SIGNING_IDENTITY" ]; then
exit 1
fi
codesign --force --options runtime --sign "$SIGNING_IDENTITY" src-tauri/target/release/bundle/macos/*.app 2>/dev/null
rm -f certificate.p12
hdiutil create -volname "Qopy" -srcfolder src-tauri/target/release/bundle/dmg -ov -format UDZO Qopy.dmg
codesign --force --sign "$APPLE_CERTIFICATE" Qopy.dmg 2>/dev/null
xcrun notarytool submit Qopy.dmg --apple-id "$APPLE_ID" --password "$APPLE_ID_PASSWORD" --team-id "$APPLE_CERTIFICATE" --wait
xcrun stapler staple Qopy.dmg
exit 0

View file

@ -1,249 +1,249 @@
name: "Nightly Builds"
on:
push:
branches:
- main
pull_request:
branches:
- main
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
runs-on: macos-latest
timeout-minutes: 30
strategy:
matrix:
include:
- args: "--target aarch64-apple-darwin"
arch: "arm64"
- args: "--target x86_64-apple-darwin"
arch: "x64"
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.APPLE_ID }}/REDACTED/g;s/${{ secrets.APPLE_ID_PASSWORD }}/REDACTED/g;s/${{ secrets.APPLE_CERTIFICATE }}/REDACTED/g;s/${{ secrets.APPLE_CERTIFICATE_PASSWORD }}/REDACTED/g;s/${{ secrets.KEYCHAIN_PASSWORD }}/REDACTED/g;s/${{ secrets.PAT }}/REDACTED/g;s/${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}/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 set-keychain-settings -lut 7200 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
security find-identity -v -p codesigning build.keychain
- name: Verify Certificate
run: |
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Apple Development")
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
echo "Certificate imported."
- 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_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
with:
args: ${{ matrix.args }}
- name: Debug Signing Process
if: failure()
run: |
echo "Attempting manual signing:"
timeout 300 codesign --force --options runtime --sign "$CERT_ID" --entitlements src-tauri/entitlements.plist src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/macos/Qopy.app
echo "Verifying signature:"
codesign -dv --verbose=4 "src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/macos/Qopy.app" | sed 's/.*Authority=.*/Authority=REDACTED/'
- name: Set architecture label
run: |
if [[ "${{ matrix.args }}" == "--target aarch64-apple-darwin" ]]; then
echo "ARCH_LABEL=aarch64-apple-darwin" >> $GITHUB_ENV
else
echo "ARCH_LABEL=x86_64-apple-darwin" >> $GITHUB_ENV
fi
- name: Rename and Publish macOS Artifacts
run: |
mv src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/dmg/*.dmg src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/dmg/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.dmg
mv src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/*.app.tar.gz src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.app.tar.gz
mv src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/*.app.tar.gz.sig src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.app.tar.gz.sig
- uses: actions/upload-artifact@v4
with:
name: macos-dmg-${{ matrix.arch }}
path: "src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/dmg/*.dmg"
- uses: actions/upload-artifact@v4
with:
name: updater-macos-${{ matrix.arch }}
path: |
src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/*.app.tar.gz
src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/*.app.tar.gz.sig
build-windows:
needs: prepare
strategy:
matrix:
include:
- args: "--target x86_64-pc-windows-msvc"
arch: "x64"
target: "x86_64-pc-windows-msvc"
- args: "--target aarch64-pc-windows-msvc"
arch: "arm64"
target: "aarch64-pc-windows-msvc"
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 }}
- name: List Bundle Directory
shell: pwsh
run: |
Write-Output "Checking build directories..."
Get-ChildItem -Path "src-tauri/target" -Recurse -Directory | Where-Object { $_.Name -eq "msi" } | ForEach-Object {
Write-Output "Found MSI directory: $($_.FullName)"
Get-ChildItem -Path $_.FullName -Filter "*.msi" | ForEach-Object {
Write-Output "Found MSI file: $($_.FullName)"
}
}
- name: Rename and Publish Windows Artifacts
run: |
mv src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi src-tauri/target/${{ matrix.target }}/release/bundle/msi/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.msi
mv src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi.sig src-tauri/target/${{ matrix.target }}/release/bundle/msi/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.msi.sig
- uses: actions/upload-artifact@v4
with:
name: windows-${{ matrix.arch }}
path: src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi
- uses: actions/upload-artifact@v4
with:
name: updater-windows-${{ matrix.arch }}
path: |
src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi
src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi.sig
build-ubuntu:
needs: prepare
runs-on: ubuntu-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-unknown-linux-gnu
- uses: swatinem/rust-cache@v2
with:
workspaces: "src-tauri -> target"
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
shared-key: "ubuntu-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
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: --target x86_64-unknown-linux-gnu
- name: Rename Linux Artifacts
run: |
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/Qopy-${{ needs.prepare.outputs.version }}.deb
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/Qopy-${{ needs.prepare.outputs.version }}.AppImage
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/Qopy-${{ needs.prepare.outputs.version }}.AppImage.sig
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/Qopy-${{ needs.prepare.outputs.version }}.rpm
- uses: actions/upload-artifact@v4
with:
name: ubuntu-deb
path: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
- uses: actions/upload-artifact@v4
with:
name: ubuntu-appimage
path: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
- uses: actions/upload-artifact@v4
with:
name: ubuntu-rpm
path: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
- uses: actions/upload-artifact@v4
with:
name: updater-ubuntu
path: |
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
name: "Nightly Builds"
on:
push:
branches:
- main
pull_request:
branches:
- main
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
runs-on: macos-latest
timeout-minutes: 30
strategy:
matrix:
include:
- args: "--target aarch64-apple-darwin"
arch: "arm64"
- args: "--target x86_64-apple-darwin"
arch: "x64"
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.APPLE_ID }}/REDACTED/g;s/${{ secrets.APPLE_ID_PASSWORD }}/REDACTED/g;s/${{ secrets.APPLE_CERTIFICATE }}/REDACTED/g;s/${{ secrets.APPLE_CERTIFICATE_PASSWORD }}/REDACTED/g;s/${{ secrets.KEYCHAIN_PASSWORD }}/REDACTED/g;s/${{ secrets.PAT }}/REDACTED/g;s/${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}/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 set-keychain-settings -lut 7200 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
security find-identity -v -p codesigning build.keychain
- name: Verify Certificate
run: |
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Apple Development")
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
echo "Certificate imported."
- 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_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
with:
args: ${{ matrix.args }}
- name: Debug Signing Process
if: failure()
run: |
echo "Attempting manual signing:"
timeout 300 codesign --force --options runtime --sign "$CERT_ID" --entitlements src-tauri/entitlements.plist src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/macos/Qopy.app
echo "Verifying signature:"
codesign -dv --verbose=4 "src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/macos/Qopy.app" | sed 's/.*Authority=.*/Authority=REDACTED/'
- name: Set architecture label
run: |
if [[ "${{ matrix.args }}" == "--target aarch64-apple-darwin" ]]; then
echo "ARCH_LABEL=aarch64-apple-darwin" >> $GITHUB_ENV
else
echo "ARCH_LABEL=x86_64-apple-darwin" >> $GITHUB_ENV
fi
- name: Rename and Publish macOS Artifacts
run: |
mv src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/dmg/*.dmg src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/dmg/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.dmg
mv src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/*.app.tar.gz src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.app.tar.gz
mv src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/*.app.tar.gz.sig src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.app.tar.gz.sig
- uses: actions/upload-artifact@v4
with:
name: macos-dmg-${{ matrix.arch }}
path: "src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/dmg/*.dmg"
- uses: actions/upload-artifact@v4
with:
name: updater-macos-${{ matrix.arch }}
path: |
src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/*.app.tar.gz
src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/*.app.tar.gz.sig
build-windows:
needs: prepare
strategy:
matrix:
include:
- args: "--target x86_64-pc-windows-msvc"
arch: "x64"
target: "x86_64-pc-windows-msvc"
- args: "--target aarch64-pc-windows-msvc"
arch: "arm64"
target: "aarch64-pc-windows-msvc"
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 }}
- name: List Bundle Directory
shell: pwsh
run: |
Write-Output "Checking build directories..."
Get-ChildItem -Path "src-tauri/target" -Recurse -Directory | Where-Object { $_.Name -eq "msi" } | ForEach-Object {
Write-Output "Found MSI directory: $($_.FullName)"
Get-ChildItem -Path $_.FullName -Filter "*.msi" | ForEach-Object {
Write-Output "Found MSI file: $($_.FullName)"
}
}
- name: Rename and Publish Windows Artifacts
run: |
mv src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi src-tauri/target/${{ matrix.target }}/release/bundle/msi/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.msi
mv src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi.sig src-tauri/target/${{ matrix.target }}/release/bundle/msi/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.msi.sig
- uses: actions/upload-artifact@v4
with:
name: windows-${{ matrix.arch }}
path: src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi
- uses: actions/upload-artifact@v4
with:
name: updater-windows-${{ matrix.arch }}
path: |
src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi
src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi.sig
build-ubuntu:
needs: prepare
runs-on: ubuntu-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-unknown-linux-gnu
- uses: swatinem/rust-cache@v2
with:
workspaces: "src-tauri -> target"
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
shared-key: "ubuntu-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
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: --target x86_64-unknown-linux-gnu
- name: Rename Linux Artifacts
run: |
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/Qopy-${{ needs.prepare.outputs.version }}.deb
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/Qopy-${{ needs.prepare.outputs.version }}.AppImage
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/Qopy-${{ needs.prepare.outputs.version }}.AppImage.sig
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/Qopy-${{ needs.prepare.outputs.version }}.rpm
- uses: actions/upload-artifact@v4
with:
name: ubuntu-deb
path: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
- uses: actions/upload-artifact@v4
with:
name: ubuntu-appimage
path: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
- uses: actions/upload-artifact@v4
with:
name: ubuntu-rpm
path: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
- uses: actions/upload-artifact@v4
with:
name: updater-ubuntu
path: |
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig

View file

@ -1,325 +1,325 @@
name: "Release"
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
prepare:
permissions: write-all
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get_version.outputs.VERSION }}
steps:
- uses: actions/checkout@v4
- name: Get version
id: get_version
run: |
VERSION=$(node -p 'require("./src-tauri/tauri.conf.json").version')
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
build-macos:
permissions: write-all
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_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
args: ${{ matrix.args }}
- 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
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: macos-${{ matrix.arch }}-binaries
path: |
src-tauri/target/**/release/bundle/dmg/*.dmg
build-windows:
permissions: write-all
needs: prepare
strategy:
matrix:
include:
- args: "--target x86_64-pc-windows-msvc"
arch: "x64"
target: "x86_64-pc-windows-msvc"
- args: "--target aarch64-pc-windows-msvc"
arch: "arm64"
target: "aarch64-pc-windows-msvc"
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 }}
- name: List Bundle Directory
shell: pwsh
run: |
$bundlePath = "src-tauri/target/${{ matrix.target }}/release/bundle/msi"
if (Test-Path $bundlePath) {
Write-Output "Contents of ${bundlePath}:"
Get-ChildItem -Path $bundlePath
} else {
Write-Output "Path ${bundlePath} does not exist."
}
- name: Rename Windows Artifacts
shell: pwsh
run: |
$bundlePath = "src-tauri/target/${{ matrix.target }}/release/bundle/msi"
$version = "${{ needs.prepare.outputs.version }}"
$arch = "${{ matrix.arch }}"
if (Test-Path $bundlePath) {
$msiFiles = Get-ChildItem -Path "$bundlePath/*.msi"
foreach ($file in $msiFiles) {
$newName = "Qopy-$version`_$arch.msi"
Rename-Item -Path $file.FullName -NewName $newName
}
} else {
Write-Error "Path ${bundlePath} does not exist."
exit 1
}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: windows-${{ matrix.arch }}-binaries
path: src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi
build-linux:
permissions: write-all
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
- name: Rename Linux Artifacts
run: |
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/Qopy-${{ needs.prepare.outputs.version }}.deb
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/Qopy-${{ needs.prepare.outputs.version }}.AppImage
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/Qopy-${{ needs.prepare.outputs.version }}.rpm
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: linux-binaries
path: |
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
create-release:
permissions: write-all
needs: [prepare, build-macos, build-windows, build-linux]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.PAT }}
- name: Check if release already exists
id: check_release
run: |
VERSION="${{ needs.prepare.outputs.version }}"
RELEASE_EXISTS=$(gh release view v$VERSION --json id --jq '.id' 2>/dev/null || echo "")
if [ -n "$RELEASE_EXISTS" ]; then
echo "SKIP_RELEASE=true" >> $GITHUB_ENV
else
echo "SKIP_RELEASE=false" >> $GITHUB_ENV
fi
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
- name: Download all artifacts
if: env.SKIP_RELEASE == 'false'
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Update CHANGELOG
if: env.SKIP_RELEASE == 'false'
id: changelog
uses: requarks/changelog-action@v1
with:
token: ${{ github.token }}
tag: ${{ github.ref_name }}
- name: Generate Release Body
if: env.SKIP_RELEASE == 'false'
id: release_body
run: |
VERSION="${{ needs.prepare.outputs.version }}"
# Calculate hashes with corrected paths
WINDOWS_ARM_HASH=$(sha256sum "artifacts/windows-arm64-binaries/Qopy-${VERSION}_arm64.msi" | awk '{ print $1 }')
WINDOWS_64_HASH=$(sha256sum "artifacts/windows-x64-binaries/Qopy-${VERSION}_x64.msi" | awk '{ print $1 }')
MAC_SILICON_HASH=$(sha256sum "artifacts/macos-silicon-binaries/aarch64-apple-darwin/release/bundle/dmg/Qopy-${VERSION}_silicon.dmg" | awk '{ print $1 }')
MAC_INTEL_HASH=$(sha256sum "artifacts/macos-intel-binaries/x86_64-apple-darwin/release/bundle/dmg/Qopy-${VERSION}_intel.dmg" | awk '{ print $1 }')
DEBIAN_HASH=$(sha256sum "artifacts/linux-binaries/deb/Qopy-${VERSION}.deb" | awk '{ print $1 }')
APPIMAGE_HASH=$(sha256sum "artifacts/linux-binaries/appimage/Qopy-${VERSION}.AppImage" | awk '{ print $1 }')
REDHAT_HASH=$(sha256sum "artifacts/linux-binaries/rpm/Qopy-${VERSION}.rpm" | awk '{ print $1 }')
# Debug output
echo "Calculated hashes:"
echo "Windows ARM: $WINDOWS_ARM_HASH"
echo "Windows x64: $WINDOWS_64_HASH"
echo "Mac Silicon: $MAC_SILICON_HASH"
echo "Mac Intel: $MAC_INTEL_HASH"
echo "Debian: $DEBIAN_HASH"
echo "AppImage: $APPIMAGE_HASH"
echo "Red Hat: $REDHAT_HASH"
RELEASE_BODY=$(cat <<-EOF
${{ needs.create-release.outputs.changelog }}
## ⬇️ Downloads
- [Windows (x64)](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}_x64.msi) - ${WINDOWS_64_HASH}
- [Windows (ARM64)](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}_arm64.msi) - ${WINDOWS_ARM_HASH}
- [macOS (Silicon)](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}_silicon.dmg) - ${MAC_SILICON_HASH}
- [macOS (Intel)](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}_intel.dmg) - ${MAC_INTEL_HASH}
- [Debian](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}.deb) - ${DEBIAN_HASH}
- [AppImage](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}.AppImage) - ${APPIMAGE_HASH}
- [Red Hat](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}.rpm) - ${REDHAT_HASH}
EOF
)
echo "RELEASE_BODY<<EOF" >> $GITHUB_ENV
echo "$RELEASE_BODY" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Create Release
if: env.SKIP_RELEASE == 'false'
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
with:
draft: true
tag_name: v${{ needs.prepare.outputs.version }}
name: v${{ needs.prepare.outputs.version }}
files: |
artifacts/**/*.dmg
artifacts/**/*.msi
artifacts/**/*.deb
artifacts/**/*.AppImage
artifacts/**/*.rpm
name: "Release"
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
prepare:
permissions: write-all
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get_version.outputs.VERSION }}
steps:
- uses: actions/checkout@v4
- name: Get version
id: get_version
run: |
VERSION=$(node -p 'require("./src-tauri/tauri.conf.json").version')
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
build-macos:
permissions: write-all
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_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
args: ${{ matrix.args }}
- 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
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: macos-${{ matrix.arch }}-binaries
path: |
src-tauri/target/**/release/bundle/dmg/*.dmg
build-windows:
permissions: write-all
needs: prepare
strategy:
matrix:
include:
- args: "--target x86_64-pc-windows-msvc"
arch: "x64"
target: "x86_64-pc-windows-msvc"
- args: "--target aarch64-pc-windows-msvc"
arch: "arm64"
target: "aarch64-pc-windows-msvc"
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 }}
- name: List Bundle Directory
shell: pwsh
run: |
$bundlePath = "src-tauri/target/${{ matrix.target }}/release/bundle/msi"
if (Test-Path $bundlePath) {
Write-Output "Contents of ${bundlePath}:"
Get-ChildItem -Path $bundlePath
} else {
Write-Output "Path ${bundlePath} does not exist."
}
- name: Rename Windows Artifacts
shell: pwsh
run: |
$bundlePath = "src-tauri/target/${{ matrix.target }}/release/bundle/msi"
$version = "${{ needs.prepare.outputs.version }}"
$arch = "${{ matrix.arch }}"
if (Test-Path $bundlePath) {
$msiFiles = Get-ChildItem -Path "$bundlePath/*.msi"
foreach ($file in $msiFiles) {
$newName = "Qopy-$version`_$arch.msi"
Rename-Item -Path $file.FullName -NewName $newName
}
} else {
Write-Error "Path ${bundlePath} does not exist."
exit 1
}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: windows-${{ matrix.arch }}-binaries
path: src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi
build-linux:
permissions: write-all
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
- name: Rename Linux Artifacts
run: |
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/Qopy-${{ needs.prepare.outputs.version }}.deb
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/Qopy-${{ needs.prepare.outputs.version }}.AppImage
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/Qopy-${{ needs.prepare.outputs.version }}.rpm
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: linux-binaries
path: |
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
create-release:
permissions: write-all
needs: [prepare, build-macos, build-windows, build-linux]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.PAT }}
- name: Check if release already exists
id: check_release
run: |
VERSION="${{ needs.prepare.outputs.version }}"
RELEASE_EXISTS=$(gh release view v$VERSION --json id --jq '.id' 2>/dev/null || echo "")
if [ -n "$RELEASE_EXISTS" ]; then
echo "SKIP_RELEASE=true" >> $GITHUB_ENV
else
echo "SKIP_RELEASE=false" >> $GITHUB_ENV
fi
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
- name: Download all artifacts
if: env.SKIP_RELEASE == 'false'
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Update CHANGELOG
if: env.SKIP_RELEASE == 'false'
id: changelog
uses: requarks/changelog-action@v1
with:
token: ${{ github.token }}
tag: ${{ github.ref_name }}
- name: Generate Release Body
if: env.SKIP_RELEASE == 'false'
id: release_body
run: |
VERSION="${{ needs.prepare.outputs.version }}"
# Calculate hashes with corrected paths
WINDOWS_ARM_HASH=$(sha256sum "artifacts/windows-arm64-binaries/Qopy-${VERSION}_arm64.msi" | awk '{ print $1 }')
WINDOWS_64_HASH=$(sha256sum "artifacts/windows-x64-binaries/Qopy-${VERSION}_x64.msi" | awk '{ print $1 }')
MAC_SILICON_HASH=$(sha256sum "artifacts/macos-silicon-binaries/aarch64-apple-darwin/release/bundle/dmg/Qopy-${VERSION}_silicon.dmg" | awk '{ print $1 }')
MAC_INTEL_HASH=$(sha256sum "artifacts/macos-intel-binaries/x86_64-apple-darwin/release/bundle/dmg/Qopy-${VERSION}_intel.dmg" | awk '{ print $1 }')
DEBIAN_HASH=$(sha256sum "artifacts/linux-binaries/deb/Qopy-${VERSION}.deb" | awk '{ print $1 }')
APPIMAGE_HASH=$(sha256sum "artifacts/linux-binaries/appimage/Qopy-${VERSION}.AppImage" | awk '{ print $1 }')
REDHAT_HASH=$(sha256sum "artifacts/linux-binaries/rpm/Qopy-${VERSION}.rpm" | awk '{ print $1 }')
# Debug output
echo "Calculated hashes:"
echo "Windows ARM: $WINDOWS_ARM_HASH"
echo "Windows x64: $WINDOWS_64_HASH"
echo "Mac Silicon: $MAC_SILICON_HASH"
echo "Mac Intel: $MAC_INTEL_HASH"
echo "Debian: $DEBIAN_HASH"
echo "AppImage: $APPIMAGE_HASH"
echo "Red Hat: $REDHAT_HASH"
RELEASE_BODY=$(cat <<-EOF
${{ needs.create-release.outputs.changelog }}
## ⬇️ Downloads
- [Windows (x64)](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}_x64.msi) - ${WINDOWS_64_HASH}
- [Windows (ARM64)](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}_arm64.msi) - ${WINDOWS_ARM_HASH}
- [macOS (Silicon)](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}_silicon.dmg) - ${MAC_SILICON_HASH}
- [macOS (Intel)](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}_intel.dmg) - ${MAC_INTEL_HASH}
- [Debian](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}.deb) - ${DEBIAN_HASH}
- [AppImage](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}.AppImage) - ${APPIMAGE_HASH}
- [Red Hat](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}.rpm) - ${REDHAT_HASH}
EOF
)
echo "RELEASE_BODY<<EOF" >> $GITHUB_ENV
echo "$RELEASE_BODY" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Create Release
if: env.SKIP_RELEASE == 'false'
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
with:
draft: true
tag_name: v${{ needs.prepare.outputs.version }}
name: v${{ needs.prepare.outputs.version }}
files: |
artifacts/**/*.dmg
artifacts/**/*.msi
artifacts/**/*.deb
artifacts/**/*.AppImage
artifacts/**/*.rpm
body: ${{ env.RELEASE_BODY }}

54
.gitignore vendored
View file

@ -1,28 +1,28 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example
bun.lockb
.gitignore
.vscode
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example
bun.lockb
.gitignore
.vscode
bun.lock

View file

@ -1,23 +1,23 @@
# Get Started
The default hotkey for Qopy is Windows+V which is also the hotkey for the default clipboard manager to turn that off follow [this guide](https://github.com/0PandaDEV/Qopy/blob/main/GET_STARTED.md#disable-windowsv-for-default-clipboard-manager).
All the data of Qopy is stored inside of a SQLite database.
| Operating System | Path |
|------------------|-----------------------------------------------------------------|
| Windows | `C:\Users\USERNAME\AppData\Roaming\net.pandadev.qopy` |
| macOS | `/Users/USERNAME/Library/Application Support/net.pandadev.qopy` |
| Linux | `/home/USERNAME/.local/share/net.pandadev.qopy` |
## Disable Windows+V for default clipboard manager
<video src="https://github.com/user-attachments/assets/723f9e07-3190-46ec-9bb7-15dfc112f620" controls title="Disable Windows+V for default clipboard manager"></video>
To disable the default clipboard manager popup from windows open Command prompt and run this command
```cmd
reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\System" /v AllowClipboardHistory /t REG_DWORD /d 0 /f
```
After that a restart may be reqired.
# Get Started
The default hotkey for Qopy is Windows+V which is also the hotkey for the default clipboard manager to turn that off follow [this guide](https://github.com/0PandaDEV/Qopy/blob/main/GET_STARTED.md#disable-windowsv-for-default-clipboard-manager).
All the data of Qopy is stored inside of a SQLite database.
| Operating System | Path |
|------------------|-----------------------------------------------------------------|
| Windows | `C:\Users\USERNAME\AppData\Roaming\net.pandadev.qopy` |
| macOS | `/Users/USERNAME/Library/Application Support/net.pandadev.qopy` |
| Linux | `/home/USERNAME/.local/share/net.pandadev.qopy` |
## Disable Windows+V for default clipboard manager
<video src="https://github.com/user-attachments/assets/723f9e07-3190-46ec-9bb7-15dfc112f620" controls title="Disable Windows+V for default clipboard manager"></video>
To disable the default clipboard manager popup from windows open Command prompt and run this command
```cmd
reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\System" /v AllowClipboardHistory /t REG_DWORD /d 0 /f
```
After that a restart may be reqired.

1322
LICENSE

File diff suppressed because it is too large Load diff

258
README.md
View file

@ -1,129 +1,129 @@
<div align="center">
<img align="center" width="128px" src="src-tauri/icons/icon.png" />
<h1 align="center"><b>Qopy</b></h1>
The fixed and simple clipboard manager for both Windows and Linux.
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_x64.msi">
<img src="./public/windows.png"> Windows (x64)
</a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_arm64.msi">
Windows (arm64)
</a>
<br>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3.deb">
<img src="./public/linux.png"> Linux (deb)
</a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3.rpm">
Linux (rpm)
</a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3.AppImage">
Linux (AppImage)
</a>
<br>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_silicon.dmg">
<img src="./public/apple.png"> macOS (Silicon)
</a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_intel.dmg">
macOS (Intel)
</a>
<br>
<br>
<sup>Nightly releases can be found <a href="https://github.com/0PandaDEV/qopy/actions/workflows/build.yml">here</a> </sup>
</div>
[discord »](https://discord.gg/invite/Y7SbYphVw9)
> \[!IMPORTANT]
>
> **Star this project**, You will receive all release notifications from GitHub without any delay \~ ⭐️
<details>
<summary><kbd>Star History</kbd></summary>
<a href="https://starchart.cc/0PandaDEV/Qopy">
<picture>
<img width="100%" src="https://starchart.cc/0PandaDEV/Qopy.svg?variant=adaptive">
</picture>
</a>
</details>
[![wakatime](https://wakatime.com/badge/user/018ce503-097f-4057-9599-db20b190920c/project/fe76359d-56c2-4a13-8413-55207b6ad298.svg?style=flat_square)](https://wakatime.com/badge/user/018ce503-097f-4057-9599-db20b190920c/project/fe76359d-56c2-4a13-8413-55207b6ad298)
## 📋 What is Qopy
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
- [x] [Setup guide](https://github.com/0PandaDEV/Qopy/blob/main/GET_STARTED.md)
- [ ] Sync Clipboard across devices https://github.com/0PandaDEV/Qopy/issues/8
- [x] Settings https://github.com/0PandaDEV/Qopy/issues/2
- [x] Metadata for copied items https://github.com/0PandaDEV/Qopy/issues/5
- [ ] Code highlighting https://github.com/0PandaDEV/Qopy/issues/7
- [ ] Streamshare integration https://github.com/0PandaDEV/Qopy/issues/4
- [ ] Content type filter https://github.com/0PandaDEV/Qopy/issues/16
- [ ] Preview for copied files https://github.com/0PandaDEV/Qopy/issues/15
- [ ] Convert files to other formats https://github.com/0PandaDEV/Qopy/issues/17
- [x] Option for custom keybind https://github.com/0PandaDEV/Qopy/issues/3
- [x] macOS Support https://github.com/0PandaDEV/Qopy/issues/13
<sup>If you have ideas for features to include, please write a feature request [here](https://github.com/0pandadev/Qopy/issues).</sup>
## 📦 Concepts
Here you can see a few concepts these might not be implemented:
![Clipboard](https://github.com/user-attachments/assets/45a44a13-6ebd-4f2d-84d2-55178e303a54)
![Settings](https://github.com/user-attachments/assets/bff5456a-f413-4e62-a43d-22c8e453aa87)
## ❤️ Donations & Support
Qopy is open-source and free to use. I appreciate donations to support ongoing development and improvements. Your contributions are voluntary and help me enhance the app for everyone.
<a href="https://buymeacoffee.com/pandadev_"><img src="https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black"/></a>
## ⌨️ Local development
You can use GitHub Codespaces for online development:
[![][codespaces-shield]][codespaces-link]
Or to get Qopy set up on your machine, you'll need to have Rust and bun installed. Then, follow these steps:
```zsh
git clone https://github.com/0pandadev/Qopy.git
cd Qopy
bun i
bun dev
```
> \[!TIP]
>
> If you are interested in contributing code, feel free to check out the [Issues](https://github.com/0pandadev/Qopy/issues) section.
## 🔨 Building for production
To build for production simply execute:
```zsh
bun build
```
> \[!NOTE]
>
> Don't worry, it will fail at the end because it can not detect a Private key, but the installer files will be generated regardless of that.
>
> You can find them in `src-tauri/target/release/bundle`.
## 📝 License
Qopy is licensed under AGPL-3. See the [LICENSE file](./LICENCE) for more information.
[codespaces-link]: https://codespaces.new/0pandadev/Qopy
[codespaces-shield]: https://github.com/codespaces/badge.svg
<div align="center">
<img align="center" width="128px" src="src-tauri/icons/icon.png" />
<h1 align="center"><b>Qopy</b></h1>
The fixed and simple clipboard manager for both Windows and Linux.
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_x64.msi">
<img src="./public/windows.png"> Windows (x64)
</a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_arm64.msi">
Windows (arm64)
</a>
<br>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3.deb">
<img src="./public/linux.png"> Linux (deb)
</a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3.rpm">
Linux (rpm)
</a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3.AppImage">
Linux (AppImage)
</a>
<br>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_silicon.dmg">
<img src="./public/apple.png"> macOS (Silicon)
</a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_intel.dmg">
macOS (Intel)
</a>
<br>
<br>
<sup>Nightly releases can be found <a href="https://github.com/0PandaDEV/qopy/actions/workflows/build.yml">here</a> </sup>
</div>
[discord »](https://discord.gg/invite/Y7SbYphVw9)
> \[!IMPORTANT]
>
> **Star this project**, You will receive all release notifications from GitHub without any delay \~ ⭐️
<details>
<summary><kbd>Star History</kbd></summary>
<a href="https://starchart.cc/0PandaDEV/Qopy">
<picture>
<img width="100%" src="https://starchart.cc/0PandaDEV/Qopy.svg?variant=adaptive">
</picture>
</a>
</details>
[![wakatime](https://wakatime.com/badge/user/018ce503-097f-4057-9599-db20b190920c/project/fe76359d-56c2-4a13-8413-55207b6ad298.svg?style=flat_square)](https://wakatime.com/badge/user/018ce503-097f-4057-9599-db20b190920c/project/fe76359d-56c2-4a13-8413-55207b6ad298)
## 📋 What is Qopy
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
- [x] [Setup guide](https://github.com/0PandaDEV/Qopy/blob/main/GET_STARTED.md)
- [ ] Sync Clipboard across devices https://github.com/0PandaDEV/Qopy/issues/8
- [x] Settings https://github.com/0PandaDEV/Qopy/issues/2
- [x] Metadata for copied items https://github.com/0PandaDEV/Qopy/issues/5
- [ ] Code highlighting https://github.com/0PandaDEV/Qopy/issues/7
- [ ] Streamshare integration https://github.com/0PandaDEV/Qopy/issues/4
- [ ] Content type filter https://github.com/0PandaDEV/Qopy/issues/16
- [ ] Preview for copied files https://github.com/0PandaDEV/Qopy/issues/15
- [ ] Convert files to other formats https://github.com/0PandaDEV/Qopy/issues/17
- [x] Option for custom keybind https://github.com/0PandaDEV/Qopy/issues/3
- [x] macOS Support https://github.com/0PandaDEV/Qopy/issues/13
<sup>If you have ideas for features to include, please write a feature request [here](https://github.com/0pandadev/Qopy/issues).</sup>
## 📦 Concepts
Here you can see a few concepts these might not be implemented:
![Clipboard](https://github.com/user-attachments/assets/45a44a13-6ebd-4f2d-84d2-55178e303a54)
![Settings](https://github.com/user-attachments/assets/bff5456a-f413-4e62-a43d-22c8e453aa87)
## ❤️ Donations & Support
Qopy is open-source and free to use. I appreciate donations to support ongoing development and improvements. Your contributions are voluntary and help me enhance the app for everyone.
<a href="https://buymeacoffee.com/pandadev_"><img src="https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black"/></a>
## ⌨️ Local development
You can use GitHub Codespaces for online development:
[![][codespaces-shield]][codespaces-link]
Or to get Qopy set up on your machine, you'll need to have Rust and bun installed. Then, follow these steps:
```zsh
git clone https://github.com/0pandadev/Qopy.git
cd Qopy
bun i
bun dev
```
> \[!TIP]
>
> If you are interested in contributing code, feel free to check out the [Issues](https://github.com/0pandadev/Qopy/issues) section.
## 🔨 Building for production
To build for production simply execute:
```zsh
bun build
```
> \[!NOTE]
>
> Don't worry, it will fail at the end because it can not detect a Private key, but the installer files will be generated regardless of that.
>
> You can find them in `src-tauri/target/release/bundle`.
## 📝 License
Qopy is licensed under AGPL-3. See the [LICENSE file](./LICENCE) for more information.
[codespaces-link]: https://codespaces.new/0pandadev/Qopy
[codespaces-shield]: https://github.com/codespaces/badge.svg

View file

@ -1,130 +1,130 @@
<div align="center">
<img align="center" width="128px" src="src-tauri/icons/icon.png" />
<h1 align="center"><b>Qopy</b></h1>
Простой и исправленный менеджер буфера обмена как для Windows, так и для Linux.
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1_x64.msi">
<img src="./public/windows.png"> Windows (x64)
</a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1_arm64.msi">
Windows (arm64)
</a>
<br>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1.deb">
<img src="./public/linux.png"> Linux (deb)
</a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1.rpm">
Linux (rpm)
</a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1.AppImage">
Linux (AppImage)
</a>
<br>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1_silicon.dmg">
<img src="./public/apple.png"> macOS (Silicon)
</a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1_intel.dmg">
macOS (Intel)
</a>
<br>
<br>
<sup>Тестовые версии можно найти <a href="https://github.com/0PandaDEV/qopy/actions/workflows/build.yml">тут</a> </sup>
</div>
[discord »](https://discord.gg/invite/Y7SbYphVw9)
> \[!IMPORTANT]
>
> **Нажав на звезду**, Вы будете получать все уведомления от Github о новых версиях без задержек \~ ⭐️
<details>
<summary><kbd>Star History</kbd></summary>
<a href="https://star-history.com/#0pandadev/qopy&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=0pandadev/qopy&theme=dark&type=Date">
<img width="100%" src="https://api.star-history.com/svg?repos=0pandadev/qopy&type=Date">
</picture>
</a>
</details>
[![wakatime](https://wakatime.com/badge/user/018ce503-097f-4057-9599-db20b190920c/project/fe76359d-56c2-4a13-8413-55207b6ad298.svg?style=flat_square)](https://wakatime.com/badge/user/018ce503-097f-4057-9599-db20b190920c/project/fe76359d-56c2-4a13-8413-55207b6ad298)
## 📋 Что такое Qopy
Qopy представляет собой исправленный менеджер буфера обмена, разработанный как простая альтернатива стандартному буферу обмена в Windows. Его цель - обеспечить более быструю и надежную работу, предоставляя при этом обширный набор функций по сравнению со своим аналогом в Windows.
## 🚧 Дорожная карта
- [ ] [Руководство по установке](https://github.com/0PandaDEV/Qopy/blob/main/GET_STARTED.md)
- [ ] Синхронизация буфера обмена между устройствами https://github.com/0PandaDEV/Qopy/issues/8
- [ ] Настройки https://github.com/0PandaDEV/Qopy/issues/2
- [x] Метаданные для скопированных элементов https://github.com/0PandaDEV/Qopy/issues/5
- [ ] Выделение кода https://github.com/0PandaDEV/Qopy/issues/7
- [ ] Интеграция Streamshare https://github.com/0PandaDEV/Qopy/issues/4
- [ ] Фильтр типов контента https://github.com/0PandaDEV/Qopy/issues/16
- [ ] Превью для скопированных файлов https://github.com/0PandaDEV/Qopy/issues/15
- [ ] Конвертация файлов в другие форматы https://github.com/0PandaDEV/Qopy/issues/17
- [x] Опция для пользовательской привязки клавиш https://github.com/0PandaDEV/Qopy/issues/3
- [x] Поддержка macOS https://github.com/0PandaDEV/Qopy/issues/13
<sup>Если у вас есть идеи для функций, которые можно добавить в будущем, пожалуйста, напишите об этом [здесь](https://github.com/0pandadev/Qopy/issues).</sup>
## 📦 Концепты
Здесь вы можете увидеть несколько концепцов, которые могут быть не реализованы:
![Clipboard](https://github.com/user-attachments/assets/45a44a13-6ebd-4f2d-84d2-55178e303a54)
![Settings](https://github.com/user-attachments/assets/bff5456a-f413-4e62-a43d-22c8e453aa87)
## ❤️ Пожертвования и Поддержка
Qopy имеет открытый исходный код и бесплатен для использования. Я ценю пожертвования в поддержку постоянной разработки и улучшений. Ваши взносы являются добровольными и помогают мне улучшить приложение для всех.
<a href="https://buymeacoffee.com/pandadev_"><img src="https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black"/></a>
## ⌨️ Локальная разработка
Вы можете использовать GitHub Codespaces для онлайн-разработки:
[![][codespaces-shield]][codespaces-link]
Или, чтобы настроить Qopy на вашем компьютере, вам необходимо установить Rust и bun. Затем выполните следующие действия:
```zsh
git clone https://github.com/0pandadev/Qopy.git
cd Qopy
bun i
bun dev
```
> \[!Tip]
>
> Если вы заинтересованы во внесении кода, не стесняйтесь смотреть здесь [Issues](https://github.com/0pandadev/Qopy/issues).
## 🔨 Сборка для продакшена
Чтобы собрать для продакшена,просто выполните:
```zsh
bun build
```
> \[!NOTE]
>
> Не волнуйтесь, в конце произойдет сбой, потому что он не сможет обнаружить Приватный ключ, но установочные файлы будут сгенерированы независимо от этого.
>
> Вы можете найти его в `src-tauri/target/release/bundle`.
## 📝 Лицензия
Qopy лицензирован под GPL-3. Смотрите [LICENSE file](./LICENCE) для дополнительной информации.
[codespaces-link]: https://codespaces.new/0pandadev/Qopy
[codespaces-shield]: https://github.com/codespaces/badge.svg
<div align="center">
<img align="center" width="128px" src="src-tauri/icons/icon.png" />
<h1 align="center"><b>Qopy</b></h1>
Простой и исправленный менеджер буфера обмена как для Windows, так и для Linux.
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1_x64.msi">
<img src="./public/windows.png"> Windows (x64)
</a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1_arm64.msi">
Windows (arm64)
</a>
<br>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1.deb">
<img src="./public/linux.png"> Linux (deb)
</a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1.rpm">
Linux (rpm)
</a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1.AppImage">
Linux (AppImage)
</a>
<br>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1_silicon.dmg">
<img src="./public/apple.png"> macOS (Silicon)
</a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1_intel.dmg">
macOS (Intel)
</a>
<br>
<br>
<sup>Тестовые версии можно найти <a href="https://github.com/0PandaDEV/qopy/actions/workflows/build.yml">тут</a> </sup>
</div>
[discord »](https://discord.gg/invite/Y7SbYphVw9)
> \[!IMPORTANT]
>
> **Нажав на звезду**, Вы будете получать все уведомления от Github о новых версиях без задержек \~ ⭐️
<details>
<summary><kbd>Star History</kbd></summary>
<a href="https://star-history.com/#0pandadev/qopy&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=0pandadev/qopy&theme=dark&type=Date">
<img width="100%" src="https://api.star-history.com/svg?repos=0pandadev/qopy&type=Date">
</picture>
</a>
</details>
[![wakatime](https://wakatime.com/badge/user/018ce503-097f-4057-9599-db20b190920c/project/fe76359d-56c2-4a13-8413-55207b6ad298.svg?style=flat_square)](https://wakatime.com/badge/user/018ce503-097f-4057-9599-db20b190920c/project/fe76359d-56c2-4a13-8413-55207b6ad298)
## 📋 Что такое Qopy
Qopy представляет собой исправленный менеджер буфера обмена, разработанный как простая альтернатива стандартному буферу обмена в Windows. Его цель - обеспечить более быструю и надежную работу, предоставляя при этом обширный набор функций по сравнению со своим аналогом в Windows.
## 🚧 Дорожная карта
- [ ] [Руководство по установке](https://github.com/0PandaDEV/Qopy/blob/main/GET_STARTED.md)
- [ ] Синхронизация буфера обмена между устройствами https://github.com/0PandaDEV/Qopy/issues/8
- [ ] Настройки https://github.com/0PandaDEV/Qopy/issues/2
- [x] Метаданные для скопированных элементов https://github.com/0PandaDEV/Qopy/issues/5
- [ ] Выделение кода https://github.com/0PandaDEV/Qopy/issues/7
- [ ] Интеграция Streamshare https://github.com/0PandaDEV/Qopy/issues/4
- [ ] Фильтр типов контента https://github.com/0PandaDEV/Qopy/issues/16
- [ ] Превью для скопированных файлов https://github.com/0PandaDEV/Qopy/issues/15
- [ ] Конвертация файлов в другие форматы https://github.com/0PandaDEV/Qopy/issues/17
- [x] Опция для пользовательской привязки клавиш https://github.com/0PandaDEV/Qopy/issues/3
- [x] Поддержка macOS https://github.com/0PandaDEV/Qopy/issues/13
<sup>Если у вас есть идеи для функций, которые можно добавить в будущем, пожалуйста, напишите об этом [здесь](https://github.com/0pandadev/Qopy/issues).</sup>
## 📦 Концепты
Здесь вы можете увидеть несколько концепцов, которые могут быть не реализованы:
![Clipboard](https://github.com/user-attachments/assets/45a44a13-6ebd-4f2d-84d2-55178e303a54)
![Settings](https://github.com/user-attachments/assets/bff5456a-f413-4e62-a43d-22c8e453aa87)
## ❤️ Пожертвования и Поддержка
Qopy имеет открытый исходный код и бесплатен для использования. Я ценю пожертвования в поддержку постоянной разработки и улучшений. Ваши взносы являются добровольными и помогают мне улучшить приложение для всех.
<a href="https://buymeacoffee.com/pandadev_"><img src="https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black"/></a>
## ⌨️ Локальная разработка
Вы можете использовать GitHub Codespaces для онлайн-разработки:
[![][codespaces-shield]][codespaces-link]
Или, чтобы настроить Qopy на вашем компьютере, вам необходимо установить Rust и bun. Затем выполните следующие действия:
```zsh
git clone https://github.com/0pandadev/Qopy.git
cd Qopy
bun i
bun dev
```
> \[!Tip]
>
> Если вы заинтересованы во внесении кода, не стесняйтесь смотреть здесь [Issues](https://github.com/0pandadev/Qopy/issues).
## 🔨 Сборка для продакшена
Чтобы собрать для продакшена,просто выполните:
```zsh
bun build
```
> \[!NOTE]
>
> Не волнуйтесь, в конце произойдет сбой, потому что он не сможет обнаружить Приватный ключ, но установочные файлы будут сгенерированы независимо от этого.
>
> Вы можете найти его в `src-tauri/target/release/bundle`.
## 📝 Лицензия
Qopy лицензирован под GPL-3. Смотрите [LICENSE file](./LICENCE) для дополнительной информации.
[codespaces-link]: https://codespaces.new/0pandadev/Qopy
[codespaces-shield]: https://github.com/codespaces/badge.svg

218
app.vue
View file

@ -1,109 +1,109 @@
<template>
<div>
<Noise />
<NuxtPage />
</div>
</template>
<script setup lang="ts">
import { listen } from "@tauri-apps/api/event";
import { app, window } from "@tauri-apps/api";
import { disable, enable } from "@tauri-apps/plugin-autostart";
import { onMounted } from "vue";
import { keyboard } from "wrdu-keyboard";
const { $settings } = useNuxtApp();
keyboard.init();
onMounted(async () => {
await listen("settings", async () => {
await navigateTo("/settings");
await app.show();
await window.getCurrentWindow().show();
});
if ((await $settings.getSetting("autostart")) === "true") {
await enable();
} else {
await disable();
}
await listen("main_route", async () => {
await navigateTo("/");
});
});
</script>
<style lang="scss">
@font-face {
font-family: SFRoundedRegular;
font-display: swap;
src: url("/fonts/SFRoundedRegular.otf") format("opentype");
}
@font-face {
font-family: SFRoundedMedium;
font-display: swap;
src: url("/fonts/SFRoundedMedium.otf") format("opentype");
}
@font-face {
font-family: SFRoundedSemiBold;
font-display: swap;
src: url("/fonts/SFRoundedSemiBold.otf") format("opentype");
}
@font-face {
font-family: CommitMono;
font-display: swap;
src: url("/fonts/CommitMono.woff2") format("woff2");
}
:root {
--background: #2e2d2b;
--accent: #feb453;
--border: #ffffff0d;
--red: #F84E4E;
--text: #e5dfd5;
--text-secondary: #ada9a1;
--text-muted: #78756f;
--sidebar-width: 286px;
--bottom-bar-height: 39px;
--info-panel-height: 160px;
--content-view-height: calc(
100% - var(--search-height) - var(--info-panel-height) -
var(--bottom-bar-height)
);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
text-decoration: none;
font-family: SFRoundedRegular;
scroll-behavior: smooth;
scrollbar-width: thin;
user-select: none;
--os-handle-bg: #ada9a1;
--os-handle-bg-hover: #78756f;
--os-handle-bg-active: #78756f;
}
html,
body {
background-color: transparent;
width: 750px;
height: 474px;
z-index: -1;
font-size: 14px;
}
.os-scrollbar-horizontal {
display: none;
}
</style>
<template>
<div>
<Noise />
<NuxtPage />
</div>
</template>
<script setup lang="ts">
import { listen } from "@tauri-apps/api/event";
import { app, window } from "@tauri-apps/api";
import { disable, enable } from "@tauri-apps/plugin-autostart";
import { onMounted } from "vue";
import { keyboard } from "wrdu-keyboard";
const { $settings } = useNuxtApp();
keyboard.init();
onMounted(async () => {
await listen("settings", async () => {
await navigateTo("/settings");
await app.show();
await window.getCurrentWindow().show();
});
if ((await $settings.getSetting("autostart")) === "true") {
await enable();
} else {
await disable();
}
await listen("main_route", async () => {
await navigateTo("/");
});
});
</script>
<style lang="scss">
@font-face {
font-family: SFRoundedRegular;
font-display: swap;
src: url("/fonts/SFRoundedRegular.otf") format("opentype");
}
@font-face {
font-family: SFRoundedMedium;
font-display: swap;
src: url("/fonts/SFRoundedMedium.otf") format("opentype");
}
@font-face {
font-family: SFRoundedSemiBold;
font-display: swap;
src: url("/fonts/SFRoundedSemiBold.otf") format("opentype");
}
@font-face {
font-family: CommitMono;
font-display: swap;
src: url("/fonts/CommitMono.woff2") format("woff2");
}
:root {
--background: #2e2d2b;
--accent: #feb453;
--border: #ffffff0d;
--red: #F84E4E;
--text: #e5dfd5;
--text-secondary: #ada9a1;
--text-muted: #78756f;
--sidebar-width: 286px;
--bottom-bar-height: 39px;
--info-panel-height: 160px;
--content-view-height: calc(
100% - var(--search-height) - var(--info-panel-height) -
var(--bottom-bar-height)
);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
text-decoration: none;
font-family: SFRoundedRegular;
scroll-behavior: smooth;
scrollbar-width: thin;
user-select: none;
--os-handle-bg: #ada9a1;
--os-handle-bg-hover: #78756f;
--os-handle-bg-active: #78756f;
}
html,
body {
background-color: transparent;
width: 750px;
height: 474px;
z-index: -1;
font-size: 14px;
}
.os-scrollbar-horizontal {
display: none;
}
</style>

File diff suppressed because it is too large Load diff

View file

@ -1,152 +1,152 @@
<template>
<div class="bottombar">
<div class="branding">
<img src="/logo.png" alt="logo" class="logo" />
<p class="name">Qopy</p>
</div>
<div class="buttons">
<div v-if="primaryAction" class="paste" @click="handlePrimaryClick">
<p class="text">{{ primaryAction.text }}</p>
<div class="keys">
<Key v-if="(os === 'windows' || os === 'linux') && primaryAction.showModifier" :input="'Ctrl'" />
<IconsCmd v-if="os === 'macos' && primaryAction.showModifier" />
<component :is="primaryAction.icon" :input="primaryAction.input" />
</div>
</div>
<div v-if="secondaryAction" class="divider"></div>
<div v-if="secondaryAction" class="actions" @click="handleSecondaryClick">
<p class="text">{{ secondaryAction.text }}</p>
<div class="keys">
<Key v-if="(os === 'windows' || os === 'linux') && secondaryAction.showModifier" :input="'Ctrl'" />
<IconsCmd v-if="os === 'macos' && secondaryAction.showModifier" />
<component :is="secondaryAction.icon" :input="secondaryAction.input" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { platform } from "@tauri-apps/plugin-os";
import IconsCmd from './Keys/Cmd.vue';
import Key from './Keys/Key.vue';
interface Action {
text: string;
icon: any;
onClick?: () => void;
showModifier?: boolean;
input?: string;
}
const props = defineProps<{
primaryAction?: Action;
secondaryAction?: Action;
}>();
const os = ref<string>("");
const handlePrimaryClick = (event: MouseEvent) => {
event.stopPropagation();
if (props.primaryAction?.onClick) {
props.primaryAction.onClick();
}
};
const handleSecondaryClick = (event: MouseEvent) => {
event.stopPropagation();
if (props.secondaryAction?.onClick) {
props.secondaryAction.onClick();
}
};
onMounted(async () => {
os.value = await platform();
});
</script>
<style scoped lang="scss">
.bottombar {
min-height: 40px;
width: 100%;
border-top: 1px solid var(--border);
backdrop-filter: blur(18px);
border-radius: 0 0 11px 11px;
background-color: rgba(46, 45, 43, 0.051);
display: flex;
align-items: center;
padding-left: 11px;
padding-right: 6px;
justify-content: space-between;
.branding {
display: flex;
gap: 8px;
align-items: center;
color: var(--text-secondary);
.logo {
width: 18px;
height: 18px;
}
}
.buttons {
display: flex;
align-items: center;
.text {
color: var(--text);
}
.keys {
display: flex;
align-items: center;
gap: 2px;
}
.divider {
width: 2px;
height: 12px;
background-color: var(--border);
margin-left: 8px;
margin-right: 4px;
transition: all 0.2s;
}
.paste,
.actions {
padding: 4px;
padding-left: 8px;
display: flex;
align-items: center;
gap: 8px;
border-radius: 7px;
background-color: transparent;
transition: all 0.2s;
cursor: pointer;
}
.paste:hover,
.actions:hover {
background-color: var(--border);
}
.paste:active,
.actions:active {
background-color: var(--border-active, #444);
transform: scale(0.98);
}
&:hover .paste:hover ~ .divider,
&:hover .divider:has(+ .actions:hover) {
opacity: 0;
}
}
}
p {
font-family: SFRoundedMedium;
}
</style>
<template>
<div class="bottombar">
<div class="branding">
<img src="/logo.png" alt="logo" class="logo" />
<p class="name">Qopy</p>
</div>
<div class="buttons">
<div v-if="primaryAction" class="paste" @click="handlePrimaryClick">
<p class="text">{{ primaryAction.text }}</p>
<div class="keys">
<Key v-if="(os === 'windows' || os === 'linux') && primaryAction.showModifier" :input="'Ctrl'" />
<IconsCmd v-if="os === 'macos' && primaryAction.showModifier" />
<component :is="primaryAction.icon" :input="primaryAction.input" />
</div>
</div>
<div v-if="secondaryAction" class="divider"></div>
<div v-if="secondaryAction" class="actions" @click="handleSecondaryClick">
<p class="text">{{ secondaryAction.text }}</p>
<div class="keys">
<Key v-if="(os === 'windows' || os === 'linux') && secondaryAction.showModifier" :input="'Ctrl'" />
<IconsCmd v-if="os === 'macos' && secondaryAction.showModifier" />
<component :is="secondaryAction.icon" :input="secondaryAction.input" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { platform } from "@tauri-apps/plugin-os";
import IconsCmd from './Keys/Cmd.vue';
import Key from './Keys/Key.vue';
interface Action {
text: string;
icon: any;
onClick?: () => void;
showModifier?: boolean;
input?: string;
}
const props = defineProps<{
primaryAction?: Action;
secondaryAction?: Action;
}>();
const os = ref<string>("");
const handlePrimaryClick = (event: MouseEvent) => {
event.stopPropagation();
if (props.primaryAction?.onClick) {
props.primaryAction.onClick();
}
};
const handleSecondaryClick = (event: MouseEvent) => {
event.stopPropagation();
if (props.secondaryAction?.onClick) {
props.secondaryAction.onClick();
}
};
onMounted(async () => {
os.value = await platform();
});
</script>
<style scoped lang="scss">
.bottombar {
min-height: 40px;
width: 100%;
border-top: 1px solid var(--border);
backdrop-filter: blur(18px);
border-radius: 0 0 11px 11px;
background-color: rgba(46, 45, 43, 0.051);
display: flex;
align-items: center;
padding-left: 11px;
padding-right: 6px;
justify-content: space-between;
.branding {
display: flex;
gap: 8px;
align-items: center;
color: var(--text-secondary);
.logo {
width: 18px;
height: 18px;
}
}
.buttons {
display: flex;
align-items: center;
.text {
color: var(--text);
}
.keys {
display: flex;
align-items: center;
gap: 2px;
}
.divider {
width: 2px;
height: 12px;
background-color: var(--border);
margin-left: 8px;
margin-right: 4px;
transition: all 0.2s;
}
.paste,
.actions {
padding: 4px;
padding-left: 8px;
display: flex;
align-items: center;
gap: 8px;
border-radius: 7px;
background-color: transparent;
transition: all 0.2s;
cursor: pointer;
}
.paste:hover,
.actions:hover {
background-color: var(--border);
}
.paste:active,
.actions:active {
background-color: var(--border-active, #444);
transform: scale(0.98);
}
&:hover .paste:hover ~ .divider,
&:hover .divider:has(+ .actions:hover) {
opacity: 0;
}
}
}
p {
font-family: SFRoundedMedium;
}
</style>

View file

@ -1,7 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#F84E4E" fill-rule="evenodd"
d="M9 2H7a.5.5 0 0 0-.5.5V3h3v-.5A.5.5 0 0 0 9 2m2 1v-.5a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2V3H2.251a.75.75 0 0 0 0 1.5h.312l.317 7.625A3 3 0 0 0 5.878 15h4.245a3 3 0 0 0 2.997-2.875l.318-7.625h.312a.75.75 0 0 0 0-1.5zm.936 1.5H4.064l.315 7.562A1.5 1.5 0 0 0 5.878 13.5h4.245a1.5 1.5 0 0 0 1.498-1.438zm-6.186 2v5a.75.75 0 0 0 1.5 0v-5a.75.75 0 0 0-1.5 0m3.75-.75a.75.75 0 0 1 .75.75v5a.75.75 0 0 1-1.5 0v-5a.75.75 0 0 1 .75-.75"
clip-rule="evenodd" />
</svg>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#F84E4E" fill-rule="evenodd"
d="M9 2H7a.5.5 0 0 0-.5.5V3h3v-.5A.5.5 0 0 0 9 2m2 1v-.5a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2V3H2.251a.75.75 0 0 0 0 1.5h.312l.317 7.625A3 3 0 0 0 5.878 15h4.245a3 3 0 0 0 2.997-2.875l.318-7.625h.312a.75.75 0 0 0 0-1.5zm.936 1.5H4.064l.315 7.562A1.5 1.5 0 0 0 5.878 13.5h4.245a1.5 1.5 0 0 0 1.498-1.438zm-6.186 2v5a.75.75 0 0 0 1.5 0v-5a.75.75 0 0 0-1.5 0m3.75-.75a.75.75 0 0 1 .75.75v5a.75.75 0 0 1-1.5 0v-5a.75.75 0 0 1 .75-.75"
clip-rule="evenodd" />
</svg>
</template>

View file

@ -1,14 +1,14 @@
<template>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<path
d="M0 1.38272C0 0.619063 0.596954 0 1.33333 0C1.33333 0 2.66667 0 2.66667 0C3.40305 0 4 0.619063 4 1.38272C4 1.38272 4 1.38272 4 1.38272C4 2.14637 3.40305 2.76543 2.66667 2.76543C2.66667 2.76543 1.33333 2.76543 1.33333 2.76543C0.596954 2.76543 0 2.14637 0 1.38272"
fill="none" stroke-width="1.5" stroke="#E5DFD5" stroke-linecap="round" stroke-linejoin="round"
transform="translate(5 0.778)" />
<path
d="M2.66667 0C2.66667 0 1.33333 0 1.33333 0C0.596954 0 0 0.619063 0 1.38272C0 1.38272 0 9.67901 0 9.67901C0 10.4427 0.596954 11.0617 1.33333 11.0617C1.33333 11.0617 8 11.0617 8 11.0617C8.73638 11.0617 9.33333 10.4427 9.33333 9.67901C9.33333 9.67901 9.33333 1.38272 9.33333 1.38272C9.33333 0.619063 8.73638 0 8 0C8 0 6.66667 0 6.66667 0"
fill="none" stroke-width="1.5" stroke="#E5DFD5" stroke-linecap="round" stroke-linejoin="round"
transform="translate(2.333 2.161)" />
</g>
</svg>
<template>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<path
d="M0 1.38272C0 0.619063 0.596954 0 1.33333 0C1.33333 0 2.66667 0 2.66667 0C3.40305 0 4 0.619063 4 1.38272C4 1.38272 4 1.38272 4 1.38272C4 2.14637 3.40305 2.76543 2.66667 2.76543C2.66667 2.76543 1.33333 2.76543 1.33333 2.76543C0.596954 2.76543 0 2.14637 0 1.38272"
fill="none" stroke-width="1.5" stroke="#E5DFD5" stroke-linecap="round" stroke-linejoin="round"
transform="translate(5 0.778)" />
<path
d="M2.66667 0C2.66667 0 1.33333 0 1.33333 0C0.596954 0 0 0.619063 0 1.38272C0 1.38272 0 9.67901 0 9.67901C0 10.4427 0.596954 11.0617 1.33333 11.0617C1.33333 11.0617 8 11.0617 8 11.0617C8.73638 11.0617 9.33333 10.4427 9.33333 9.67901C9.33333 9.67901 9.33333 1.38272 9.33333 1.38272C9.33333 0.619063 8.73638 0 8 0C8 0 6.66667 0 6.66667 0"
fill="none" stroke-width="1.5" stroke="#E5DFD5" stroke-linecap="round" stroke-linejoin="round"
transform="translate(2.333 2.161)" />
</g>
</svg>
</template>

View file

@ -1,7 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd"
d="M8.922 9.842q.077.425.078.896c0 1.907-1.387 3.66-3.79 3.894a4.78 4.78 0 0 1-4.208-1.774a2 2 0 0 1-.21-.333c-.231-.461-.292-1-.292-1.528c.312.047.599.045.852 0c.635-.112 1.061-.487 1.148-1C2.73 8.637 3.572 7 5.76 7q.224 0 .435.028l3.417-4.784a2.971 2.971 0 1 1 4.145 4.145zm-.56-1.444l2.819-2.013A2.7 2.7 0 0 0 9.615 4.82L7.626 7.605q.43.324.737.793m4.066-2.904l.457-.326a1.471 1.471 0 1 0-2.052-2.052l-.326.457a4.2 4.2 0 0 1 1.921 1.921M3.98 10.247c.086-.507.272-.962.54-1.264c.225-.254.572-.483 1.242-.483c.517 0 .913.197 1.198.523c.297.34.541.906.541 1.715c0 1.121-.786 2.24-2.435 2.4a3.3 3.3 0 0 1-2.63-.922c.76-.337 1.374-.965 1.544-1.969"
clip-rule="evenodd" />
</svg>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd"
d="M8.922 9.842q.077.425.078.896c0 1.907-1.387 3.66-3.79 3.894a4.78 4.78 0 0 1-4.208-1.774a2 2 0 0 1-.21-.333c-.231-.461-.292-1-.292-1.528c.312.047.599.045.852 0c.635-.112 1.061-.487 1.148-1C2.73 8.637 3.572 7 5.76 7q.224 0 .435.028l3.417-4.784a2.971 2.971 0 1 1 4.145 4.145zm-.56-1.444l2.819-2.013A2.7 2.7 0 0 0 9.615 4.82L7.626 7.605q.43.324.737.793m4.066-2.904l.457-.326a1.471 1.471 0 1 0-2.052-2.052l-.326.457a4.2 4.2 0 0 1 1.921 1.921M3.98 10.247c.086-.507.272-.962.54-1.264c.225-.254.572-.483 1.242-.483c.517 0 .913.197 1.198.523c.297.34.541.906.541 1.715c0 1.121-.786 2.24-2.435 2.4a3.3 3.3 0 0 1-2.63-.922c.76-.337 1.374-.965 1.544-1.969"
clip-rule="evenodd" />
</svg>
</template>

View file

@ -1,16 +1,16 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g>
<path
d="M3.75 16.0714L11.25 16.0714C12.2855 16.0714 13.125 15.208 13.125 14.1429L13.125 8.02672C13.1248 7.5147 12.9269 7.02399 12.5748 6.66239L8.52375 2.493C8.17225 2.13169 7.69568 1.92868 7.19875 1.92857L3.75 1.92857C2.71447 1.92857 1.875 2.79202 1.875 3.85715L1.875 14.1429C1.875 15.208 2.71447 16.0714 3.75 16.0714M15 8.02672C15.0003 7.00424 14.6053 6.02271 13.9018 5.29904L9.85 1.13143C9.1465 0.406921 8.19178 -0.000123228 7.19625 0L3.75 0C1.67893 0 0 1.7269 0 3.85714L0 14.1429C2.38419e-07 16.2731 1.67893 18 3.75 18L11.25 18C13.3211 18 15 16.2731 15 14.1429L15 8.02672ZM8.40003 12.2529C8.03446 11.8764 8.03446 11.2665 8.40003 10.89L9.61253 9.64286L8.40003 8.39571C8.05583 8.01577 8.06598 7.4237 8.423 7.05648C8.78002 6.68926 9.35564 6.67882 9.72503 7.03286L11.6 8.96143C11.9656 9.33791 11.9656 9.94781 11.6 10.3243L9.72503 12.2529C9.35901 12.6289 8.76605 12.6289 8.40003 12.2529M6.60003 8.39571C6.94423 8.01577 6.93407 7.4237 6.57706 7.05649C6.22004 6.68927 5.64442 6.67882 5.27503 7.03286L3.40003 8.96143C3.03446 9.33791 3.03446 9.94781 3.40003 10.3243L5.27503 12.2529C5.50874 12.5108 5.86072 12.617 6.19289 12.5298C6.52505 12.4425 6.78443 12.1757 6.86926 11.8341C6.95409 11.4924 6.85084 11.1304 6.60003 10.89L5.38753 9.64286L6.60003 8.39571Z"
fill="#E5DFD5"
fill-rule="evenodd"
transform="translate(1.5 0)" />
</g>
</svg>
</template>
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g>
<path
d="M3.75 16.0714L11.25 16.0714C12.2855 16.0714 13.125 15.208 13.125 14.1429L13.125 8.02672C13.1248 7.5147 12.9269 7.02399 12.5748 6.66239L8.52375 2.493C8.17225 2.13169 7.69568 1.92868 7.19875 1.92857L3.75 1.92857C2.71447 1.92857 1.875 2.79202 1.875 3.85715L1.875 14.1429C1.875 15.208 2.71447 16.0714 3.75 16.0714M15 8.02672C15.0003 7.00424 14.6053 6.02271 13.9018 5.29904L9.85 1.13143C9.1465 0.406921 8.19178 -0.000123228 7.19625 0L3.75 0C1.67893 0 0 1.7269 0 3.85714L0 14.1429C2.38419e-07 16.2731 1.67893 18 3.75 18L11.25 18C13.3211 18 15 16.2731 15 14.1429L15 8.02672ZM8.40003 12.2529C8.03446 11.8764 8.03446 11.2665 8.40003 10.89L9.61253 9.64286L8.40003 8.39571C8.05583 8.01577 8.06598 7.4237 8.423 7.05648C8.78002 6.68926 9.35564 6.67882 9.72503 7.03286L11.6 8.96143C11.9656 9.33791 11.9656 9.94781 11.6 10.3243L9.72503 12.2529C9.35901 12.6289 8.76605 12.6289 8.40003 12.2529M6.60003 8.39571C6.94423 8.01577 6.93407 7.4237 6.57706 7.05649C6.22004 6.68927 5.64442 6.67882 5.27503 7.03286L3.40003 8.96143C3.03446 9.33791 3.03446 9.94781 3.40003 10.3243L5.27503 12.2529C5.50874 12.5108 5.86072 12.617 6.19289 12.5298C6.52505 12.4425 6.78443 12.1757 6.86926 11.8341C6.95409 11.4924 6.85084 11.1304 6.60003 10.89L5.38753 9.64286L6.60003 8.39571Z"
fill="#E5DFD5"
fill-rule="evenodd"
transform="translate(1.5 0)" />
</g>
</svg>
</template>

View file

@ -1,10 +1,10 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<g fill="none">
<path fill="#E5DFD5" fill-rule="evenodd"
d="M7.47 1.22a.75.75 0 0 1 1.06 0l1.75 1.75a.75.75 0 1 1-1.06 1.06l-.47-.47v8.88l.47-.47a.75.75 0 1 1 1.06 1.06l-1.75 1.75a.75.75 0 0 1-1.06 0l-1.75-1.75a.75.75 0 1 1 1.06-1.06l.47.47V3.56l-.47.47a.75.75 0 0 1-1.06-1.06zM1.22 7.47a.75.75 0 0 0 0 1.06l1.75 1.75a.75.75 0 1 0 1.06-1.06L2.81 8l1.22-1.22a.75.75 0 0 0-1.06-1.06zm13.56 1.06l-1.75 1.75a.75.75 0 1 1-1.06-1.06L13.19 8l-1.22-1.22a.75.75 0 0 1 1.06-1.06l1.75 1.75a.75.75 0 0 1 0 1.06"
clip-rule="evenodd" />
<path stroke="#E5DFD5" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.5 8h11" />
</g>
</svg>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<g fill="none">
<path fill="#E5DFD5" fill-rule="evenodd"
d="M7.47 1.22a.75.75 0 0 1 1.06 0l1.75 1.75a.75.75 0 1 1-1.06 1.06l-.47-.47v8.88l.47-.47a.75.75 0 1 1 1.06 1.06l-1.75 1.75a.75.75 0 0 1-1.06 0l-1.75-1.75a.75.75 0 1 1 1.06-1.06l.47.47V3.56l-.47.47a.75.75 0 0 1-1.06-1.06zM1.22 7.47a.75.75 0 0 0 0 1.06l1.75 1.75a.75.75 0 1 0 1.06-1.06L2.81 8l1.22-1.22a.75.75 0 0 0-1.06-1.06zm13.56 1.06l-1.75 1.75a.75.75 0 1 1-1.06-1.06L13.19 8l-1.22-1.22a.75.75 0 0 1 1.06-1.06l1.75 1.75a.75.75 0 0 1 0 1.06"
clip-rule="evenodd" />
<path stroke="#E5DFD5" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.5 8h11" />
</g>
</svg>
</template>

View file

@ -1,16 +1,16 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g>
<path
d="M11.25 16.0714L3.75 16.0714C2.71447 16.0714 1.875 15.208 1.875 14.1429L1.875 3.85714C1.875 2.79202 2.71447 1.92857 3.75 1.92857L6.25 1.92857L6.25 5.14286C6.25 7.2731 7.92893 9 10 9L13.125 9L13.125 14.1429C13.125 15.208 12.2855 16.0714 11.25 16.0714M12.8788 7.07143C12.7961 6.92188 12.6944 6.78437 12.5763 6.66257L8.5225 2.493C8.40408 2.37151 8.2704 2.26687 8.125 2.18186L8.125 5.14286C8.125 6.20798 8.96447 7.07143 10 7.07143L12.8788 7.07143ZM13.9013 5.29843C14.6049 6.02193 15.0001 7.00338 15 8.02672L15 14.1429C15 16.2731 13.3211 18 11.25 18L3.75 18C1.67893 18 0 16.2731 0 14.1429L0 3.85714C-5.96046e-07 1.7269 1.67893 0 3.75 0L7.19625 0C8.19116 -0.000122309 9.14535 0.406423 9.84875 1.13014L13.9013 5.29843Z"
fill="#E5DFD5"
fill-rule="evenodd"
transform="translate(1.5 0)" />
</g>
</svg>
</template>
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g>
<path
d="M11.25 16.0714L3.75 16.0714C2.71447 16.0714 1.875 15.208 1.875 14.1429L1.875 3.85714C1.875 2.79202 2.71447 1.92857 3.75 1.92857L6.25 1.92857L6.25 5.14286C6.25 7.2731 7.92893 9 10 9L13.125 9L13.125 14.1429C13.125 15.208 12.2855 16.0714 11.25 16.0714M12.8788 7.07143C12.7961 6.92188 12.6944 6.78437 12.5763 6.66257L8.5225 2.493C8.40408 2.37151 8.2704 2.26687 8.125 2.18186L8.125 5.14286C8.125 6.20798 8.96447 7.07143 10 7.07143L12.8788 7.07143ZM13.9013 5.29843C14.6049 6.02193 15.0001 7.00338 15 8.02672L15 14.1429C15 16.2731 13.3211 18 11.25 18L3.75 18C1.67893 18 0 16.2731 0 14.1429L0 3.85714C-5.96046e-07 1.7269 1.67893 0 3.75 0L7.19625 0C8.19116 -0.000122309 9.14535 0.406423 9.84875 1.13014L13.9013 5.29843Z"
fill="#E5DFD5"
fill-rule="evenodd"
transform="translate(1.5 0)" />
</g>
</svg>
</template>

View file

@ -1,7 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd"
d="M7.199 2H8.8a.2.2 0 0 1 .2.2c0 1.808 1.958 2.939 3.524 2.034a.2.2 0 0 1 .271.073l.802 1.388a.2.2 0 0 1-.073.272c-1.566.904-1.566 3.164 0 4.069a.2.2 0 0 1 .073.271l-.802 1.388a.2.2 0 0 1-.271.073C10.958 10.863 9 11.993 9 13.8a.2.2 0 0 1-.199.2H7.2a.2.2 0 0 1-.2-.2c0-1.808-1.958-2.938-3.524-2.034a.2.2 0 0 1-.272-.073l-.8-1.388a.2.2 0 0 1 .072-.271c1.566-.905 1.566-3.165 0-4.07a.2.2 0 0 1-.073-.27l.801-1.389a.2.2 0 0 1 .272-.072C5.042 5.138 7 4.007 7 2.199c0-.11.089-.199.199-.199M5.5 2.2c0-.94.76-1.7 1.699-1.7H8.8c.94 0 1.7.76 1.7 1.7a.85.85 0 0 0 1.274.735a1.7 1.7 0 0 1 2.32.622l.802 1.388c.469.813.19 1.851-.622 2.32a.85.85 0 0 0 0 1.472a1.7 1.7 0 0 1 .622 2.32l-.802 1.388a1.7 1.7 0 0 1-2.32.622a.85.85 0 0 0-1.274.735c0 .939-.76 1.7-1.699 1.7H7.2a1.7 1.7 0 0 1-1.699-1.7a.85.85 0 0 0-1.274-.735a1.7 1.7 0 0 1-2.32-.622l-.802-1.388a1.7 1.7 0 0 1 .622-2.32a.85.85 0 0 0 0-1.471a1.7 1.7 0 0 1-.622-2.32l.801-1.389a1.7 1.7 0 0 1 2.32-.622A.85.85 0 0 0 5.5 2.2m4 5.8a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0M11 8a3 3 0 1 1-6 0a3 3 0 0 1 6 0"
clip-rule="evenodd" />
</svg>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd"
d="M7.199 2H8.8a.2.2 0 0 1 .2.2c0 1.808 1.958 2.939 3.524 2.034a.2.2 0 0 1 .271.073l.802 1.388a.2.2 0 0 1-.073.272c-1.566.904-1.566 3.164 0 4.069a.2.2 0 0 1 .073.271l-.802 1.388a.2.2 0 0 1-.271.073C10.958 10.863 9 11.993 9 13.8a.2.2 0 0 1-.199.2H7.2a.2.2 0 0 1-.2-.2c0-1.808-1.958-2.938-3.524-2.034a.2.2 0 0 1-.272-.073l-.8-1.388a.2.2 0 0 1 .072-.271c1.566-.905 1.566-3.165 0-4.07a.2.2 0 0 1-.073-.27l.801-1.389a.2.2 0 0 1 .272-.072C5.042 5.138 7 4.007 7 2.199c0-.11.089-.199.199-.199M5.5 2.2c0-.94.76-1.7 1.699-1.7H8.8c.94 0 1.7.76 1.7 1.7a.85.85 0 0 0 1.274.735a1.7 1.7 0 0 1 2.32.622l.802 1.388c.469.813.19 1.851-.622 2.32a.85.85 0 0 0 0 1.472a1.7 1.7 0 0 1 .622 2.32l-.802 1.388a1.7 1.7 0 0 1-2.32.622a.85.85 0 0 0-1.274.735c0 .939-.76 1.7-1.699 1.7H7.2a1.7 1.7 0 0 1-1.699-1.7a.85.85 0 0 0-1.274-.735a1.7 1.7 0 0 1-2.32-.622l-.802-1.388a1.7 1.7 0 0 1 .622-2.32a.85.85 0 0 0 0-1.471a1.7 1.7 0 0 1-.622-2.32l.801-1.389a1.7 1.7 0 0 1 2.32-.622A.85.85 0 0 0 5.5 2.2m4 5.8a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0M11 8a3 3 0 1 1-6 0a3 3 0 0 1 6 0"
clip-rule="evenodd" />
</svg>
</template>

View file

@ -1,7 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd"
d="M9.208 12.346c-.485 1-.953 1.154-1.208 1.154s-.723-.154-1.208-1.154c-.372-.768-.647-1.858-.749-3.187a21 21 0 0 0 3.914 0c-.102 1.329-.377 2.419-.75 3.187m.788-4.699C9.358 7.714 8.69 7.75 8 7.75s-1.358-.036-1.996-.103c.037-1.696.343-3.075.788-3.993C7.277 2.654 7.745 2.5 8 2.5s.723.154 1.208 1.154c.445.918.75 2.297.788 3.993m1.478 1.306c-.085 1.516-.375 2.848-.836 3.874a5.5 5.5 0 0 0 2.843-4.364c-.621.199-1.295.364-2.007.49m1.918-2.043c-.572.204-1.21.379-1.901.514c-.056-1.671-.354-3.14-.853-4.251a5.5 5.5 0 0 1 2.754 3.737m-8.883.514c.056-1.671.354-3.14.853-4.251A5.5 5.5 0 0 0 2.608 6.91c.572.204 1.21.379 1.901.514M2.52 8.463a5.5 5.5 0 0 0 2.843 4.364c-.46-1.026-.75-2.358-.836-3.874a15.5 15.5 0 0 1-2.007-.49M15 8A7 7 0 1 0 1 8a7 7 0 0 0 14 0"
clip-rule="evenodd" />
</svg>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd"
d="M9.208 12.346c-.485 1-.953 1.154-1.208 1.154s-.723-.154-1.208-1.154c-.372-.768-.647-1.858-.749-3.187a21 21 0 0 0 3.914 0c-.102 1.329-.377 2.419-.75 3.187m.788-4.699C9.358 7.714 8.69 7.75 8 7.75s-1.358-.036-1.996-.103c.037-1.696.343-3.075.788-3.993C7.277 2.654 7.745 2.5 8 2.5s.723.154 1.208 1.154c.445.918.75 2.297.788 3.993m1.478 1.306c-.085 1.516-.375 2.848-.836 3.874a5.5 5.5 0 0 0 2.843-4.364c-.621.199-1.295.364-2.007.49m1.918-2.043c-.572.204-1.21.379-1.901.514c-.056-1.671-.354-3.14-.853-4.251a5.5 5.5 0 0 1 2.754 3.737m-8.883.514c.056-1.671.354-3.14.853-4.251A5.5 5.5 0 0 0 2.608 6.91c.572.204 1.21.379 1.901.514M2.52 8.463a5.5 5.5 0 0 0 2.843 4.364c-.46-1.026-.75-2.358-.836-3.874a15.5 15.5 0 0 1-2.007-.49M15 8A7 7 0 1 0 1 8a7 7 0 0 0 14 0"
clip-rule="evenodd" />
</svg>
</template>

View file

@ -1,15 +1,15 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g>
<path
d="M13.8462 2.07692L4.15385 2.07692C3.00679 2.07692 2.07692 3.00679 2.07692 4.15385L2.07692 11.1143L3.40892 10.1451C4.26934 9.51991 5.43685 9.5289 6.28754 10.1672L7.57246 11.1309L10.8512 8.32016C11.7843 7.52111 13.1676 7.54669 14.0705 8.37969L15.9231 10.0897L15.9231 4.15385C15.9231 3.00679 14.9932 2.07692 13.8462 2.07692M18 12.4588L18 4.15385C18 1.85974 16.1403 0 13.8462 0L4.15385 0C1.85974 0 0 1.85974 0 4.15385L0 13.8462C3.30118e-07 16.1403 1.85974 18 4.15385 18L13.8462 18C16.1403 18 18 16.1403 18 13.8462L18 12.4588ZM15.9231 12.9157L12.6623 9.90554C12.5333 9.78671 12.3358 9.78314 12.2026 9.89723L8.29108 13.2508L7.65831 13.7935L6.99231 13.2937L5.04 11.8302C4.91867 11.7398 4.75269 11.7386 4.63015 11.8274L2.07692 13.6814L2.07692 13.8462C2.07692 14.9932 3.00679 15.9231 4.15385 15.9231L13.8462 15.9231C14.9932 15.9231 15.9231 14.9932 15.9231 13.8462L15.9231 12.9157ZM8.30769 6.23077C8.30769 7.37782 7.37782 8.30769 6.23077 8.30769C5.08372 8.30769 4.15385 7.37782 4.15385 6.23077C4.15385 5.08372 5.08372 4.15385 6.23077 4.15385C7.37782 4.15385 8.30769 5.08372 8.30769 6.23077"
fill="#E5DFD5"
fill-rule="evenodd" />
</g>
</svg>
</template>
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g>
<path
d="M13.8462 2.07692L4.15385 2.07692C3.00679 2.07692 2.07692 3.00679 2.07692 4.15385L2.07692 11.1143L3.40892 10.1451C4.26934 9.51991 5.43685 9.5289 6.28754 10.1672L7.57246 11.1309L10.8512 8.32016C11.7843 7.52111 13.1676 7.54669 14.0705 8.37969L15.9231 10.0897L15.9231 4.15385C15.9231 3.00679 14.9932 2.07692 13.8462 2.07692M18 12.4588L18 4.15385C18 1.85974 16.1403 0 13.8462 0L4.15385 0C1.85974 0 0 1.85974 0 4.15385L0 13.8462C3.30118e-07 16.1403 1.85974 18 4.15385 18L13.8462 18C16.1403 18 18 16.1403 18 13.8462L18 12.4588ZM15.9231 12.9157L12.6623 9.90554C12.5333 9.78671 12.3358 9.78314 12.2026 9.89723L8.29108 13.2508L7.65831 13.7935L6.99231 13.2937L5.04 11.8302C4.91867 11.7398 4.75269 11.7386 4.63015 11.8274L2.07692 13.6814L2.07692 13.8462C2.07692 14.9932 3.00679 15.9231 4.15385 15.9231L13.8462 15.9231C14.9932 15.9231 15.9231 14.9932 15.9231 13.8462L15.9231 12.9157ZM8.30769 6.23077C8.30769 7.37782 7.37782 8.30769 6.23077 8.30769C5.08372 8.30769 4.15385 7.37782 4.15385 6.23077C4.15385 5.08372 5.08372 4.15385 6.23077 4.15385C7.37782 4.15385 8.30769 5.08372 8.30769 6.23077"
fill="#E5DFD5"
fill-rule="evenodd" />
</g>
</svg>
</template>

View file

@ -1,15 +1,15 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g>
<path
d="M2.68978 6.95235C3.0999 6.55662 3.75151 6.56259 4.1543 6.96577C4.5571 7.36895 4.56246 8.02056 4.16634 8.43031C4.16634 8.43031 3.15364 9.443 3.15364 9.443C1.68651 10.9393 1.69832 13.3381 3.18012 14.8199C4.66192 16.3017 7.06072 16.3135 8.55703 14.8464C8.55703 14.8464 9.56973 13.8337 9.56973 13.8337C9.98137 13.4501 10.6228 13.4614 11.0207 13.8593C11.4185 14.2571 11.4299 14.8986 11.0463 15.3102C11.0463 15.3102 10.035 16.3229 10.035 16.3229C7.71847 18.5799 4.01808 18.5559 1.73113 16.2689C-0.555826 13.982 -0.579909 10.2816 1.67708 7.96504C1.67708 7.96504 2.68978 6.95235 2.68978 6.95235ZM13.8337 9.56973C13.4501 9.98138 13.4614 10.6228 13.8593 11.0207C14.2571 11.4185 14.8986 11.4299 15.3103 11.0463C15.3103 11.0463 16.323 10.035 16.323 10.035C18.58 7.71847 18.5559 4.01808 16.2689 1.73113C13.982 -0.555826 10.2816 -0.579908 7.96505 1.67708C7.96505 1.67708 6.95235 2.68978 6.95235 2.68978C6.55662 3.0999 6.56259 3.75151 6.96577 4.15431C7.36895 4.55711 8.02056 4.56247 8.43031 4.16635C8.43031 4.16635 9.44301 3.15365 9.44301 3.15365C10.9393 1.68652 13.3381 1.69833 14.8199 3.18013C16.3017 4.66192 16.3135 7.06073 14.8464 8.55704C14.8464 8.55704 13.8337 9.56973 13.8337 9.56973ZM12.5242 6.9523C12.8038 6.69186 12.9188 6.29961 12.8243 5.92945C12.7297 5.55928 12.4407 5.27024 12.0705 5.1757C11.7004 5.08117 11.3081 5.19623 11.0477 5.47574C11.0477 5.47574 5.47574 11.0477 5.47574 11.0477C5.19623 11.3081 5.08117 11.7004 5.1757 12.0705C5.27024 12.4407 5.55928 12.7297 5.92945 12.8243C6.29961 12.9188 6.69186 12.8037 6.9523 12.5242C6.9523 12.5242 12.5242 6.9523 12.5242 6.9523Z"
fill="#E5DFD5"
fill-rule="evenodd" />
</g>
</svg>
</template>
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g>
<path
d="M2.68978 6.95235C3.0999 6.55662 3.75151 6.56259 4.1543 6.96577C4.5571 7.36895 4.56246 8.02056 4.16634 8.43031C4.16634 8.43031 3.15364 9.443 3.15364 9.443C1.68651 10.9393 1.69832 13.3381 3.18012 14.8199C4.66192 16.3017 7.06072 16.3135 8.55703 14.8464C8.55703 14.8464 9.56973 13.8337 9.56973 13.8337C9.98137 13.4501 10.6228 13.4614 11.0207 13.8593C11.4185 14.2571 11.4299 14.8986 11.0463 15.3102C11.0463 15.3102 10.035 16.3229 10.035 16.3229C7.71847 18.5799 4.01808 18.5559 1.73113 16.2689C-0.555826 13.982 -0.579909 10.2816 1.67708 7.96504C1.67708 7.96504 2.68978 6.95235 2.68978 6.95235ZM13.8337 9.56973C13.4501 9.98138 13.4614 10.6228 13.8593 11.0207C14.2571 11.4185 14.8986 11.4299 15.3103 11.0463C15.3103 11.0463 16.323 10.035 16.323 10.035C18.58 7.71847 18.5559 4.01808 16.2689 1.73113C13.982 -0.555826 10.2816 -0.579908 7.96505 1.67708C7.96505 1.67708 6.95235 2.68978 6.95235 2.68978C6.55662 3.0999 6.56259 3.75151 6.96577 4.15431C7.36895 4.55711 8.02056 4.56247 8.43031 4.16635C8.43031 4.16635 9.44301 3.15365 9.44301 3.15365C10.9393 1.68652 13.3381 1.69833 14.8199 3.18013C16.3017 4.66192 16.3135 7.06073 14.8464 8.55704C14.8464 8.55704 13.8337 9.56973 13.8337 9.56973ZM12.5242 6.9523C12.8038 6.69186 12.9188 6.29961 12.8243 5.92945C12.7297 5.55928 12.4407 5.27024 12.0705 5.1757C11.7004 5.08117 11.3081 5.19623 11.0477 5.47574C11.0477 5.47574 5.47574 11.0477 5.47574 11.0477C5.19623 11.3081 5.08117 11.7004 5.1757 12.0705C5.27024 12.4407 5.55928 12.7297 5.92945 12.8243C6.29961 12.9188 6.69186 12.8037 6.9523 12.5242C6.9523 12.5242 12.5242 6.9523 12.5242 6.9523Z"
fill="#E5DFD5"
fill-rule="evenodd" />
</g>
</svg>
</template>

View file

@ -1,7 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd"
d="M10 1.5A.75.75 0 0 0 10 3h1.94L6.97 7.97a.75.75 0 0 0 1.06 1.06L13 4.06V6a.75.75 0 0 0 1.5 0V2.25a.75.75 0 0 0-.75-.75zM7.5 3.25a.75.75 0 0 0-.75-.75H4.5a3 3 0 0 0-3 3v6a3 3 0 0 0 3 3h6a3 3 0 0 0 3-3V9.25a.75.75 0 0 0-1.5 0v2.25a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 3 11.5v-6A1.5 1.5 0 0 1 4.5 4h2.25a.75.75 0 0 0 .75-.75"
clip-rule="evenodd" />
</svg>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd"
d="M10 1.5A.75.75 0 0 0 10 3h1.94L6.97 7.97a.75.75 0 0 0 1.06 1.06L13 4.06V6a.75.75 0 0 0 1.5 0V2.25a.75.75 0 0 0-.75-.75zM7.5 3.25a.75.75 0 0 0-.75-.75H4.5a3 3 0 0 0-3 3v6a3 3 0 0 0 3 3h6a3 3 0 0 0 3-3V9.25a.75.75 0 0 0-1.5 0v2.25a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 3 11.5v-6A1.5 1.5 0 0 1 4.5 4h2.25a.75.75 0 0 0 .75-.75"
clip-rule="evenodd" />
</svg>
</template>

View file

@ -1,7 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd"
d="M11.423 1A3.577 3.577 0 0 1 15 4.577c0 .27-.108.53-.3.722l-.528.529l-1.971 1.971l-5.059 5.059a3 3 0 0 1-1.533.82l-2.638.528a1 1 0 0 1-1.177-1.177l.528-2.638a3 3 0 0 1 .82-1.533l5.059-5.059l2.5-2.5c.191-.191.451-.299.722-.299m-2.31 4.009l-4.91 4.91a1.5 1.5 0 0 0-.41.766l-.38 1.903l1.902-.38a1.5 1.5 0 0 0 .767-.41l4.91-4.91a2.08 2.08 0 0 0-1.88-1.88m3.098.658a3.6 3.6 0 0 0-1.878-1.879l1.28-1.28c.995.09 1.788.884 1.878 1.88z"
clip-rule="evenodd" />
</svg>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd"
d="M11.423 1A3.577 3.577 0 0 1 15 4.577c0 .27-.108.53-.3.722l-.528.529l-1.971 1.971l-5.059 5.059a3 3 0 0 1-1.533.82l-2.638.528a1 1 0 0 1-1.177-1.177l.528-2.638a3 3 0 0 1 .82-1.533l5.059-5.059l2.5-2.5c.191-.191.451-.299.722-.299m-2.31 4.009l-4.91 4.91a1.5 1.5 0 0 0-.41.766l-.38 1.903l1.902-.38a1.5 1.5 0 0 0 .767-.41l4.91-4.91a2.08 2.08 0 0 0-1.88-1.88m3.098.658a3.6 3.6 0 0 0-1.878-1.879l1.28-1.28c.995.09 1.788.884 1.878 1.88z"
clip-rule="evenodd" />
</svg>
</template>

View file

@ -1,7 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd"
d="M8 1.5a6.5 6.5 0 1 0 6.445 7.348a.75.75 0 1 0-1.487-.194A5.001 5.001 0 1 1 11.57 4.5h-1.32a.75.75 0 0 0 0 1.5h3a.75.75 0 0 0 .75-.75v-3a.75.75 0 0 0-1.5 0v1.06A6.48 6.48 0 0 0 8 1.5"
clip-rule="evenodd" />
</svg>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd"
d="M8 1.5a6.5 6.5 0 1 0 6.445 7.348a.75.75 0 1 0-1.487-.194A5.001 5.001 0 1 1 11.57 4.5h-1.32a.75.75 0 0 0 0 1.5h3a.75.75 0 0 0 .75-.75v-3a.75.75 0 0 0-1.5 0v1.06A6.48 6.48 0 0 0 8 1.5"
clip-rule="evenodd" />
</svg>
</template>

View file

@ -1,7 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd"
d="M3.279 2.544A.75.75 0 0 1 4 2h8a.75.75 0 0 1 .721.544l.5 1.75a.75.75 0 1 1-1.442.412L11.434 3.5H8.75l-.004 9H9.5a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5h.746l.004-9H4.566L4.22 4.706a.75.75 0 1 1-1.442-.412z"
clip-rule="evenodd" />
</svg>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd"
d="M3.279 2.544A.75.75 0 0 1 4 2h8a.75.75 0 0 1 .721.544l.5 1.75a.75.75 0 1 1-1.442.412L11.434 3.5H8.75l-.004 9H9.5a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5h.746l.004-9H4.566L4.22 4.706a.75.75 0 1 1-1.442-.412z"
clip-rule="evenodd" />
</svg>
</template>

View file

@ -1,16 +1,16 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g>
<path
d="M3.75 16.0714L11.25 16.0714C12.2855 16.0714 13.125 15.208 13.125 14.1429L13.125 8.02672C13.1248 7.5147 12.9269 7.02399 12.5748 6.66239L8.52375 2.493C8.17225 2.13169 7.69568 1.92868 7.19875 1.92857L3.75 1.92857C2.71447 1.92857 1.875 2.79202 1.875 3.85714L1.875 14.1429C1.875 15.208 2.71447 16.0714 3.75 16.0714M15 8.02672C15.0003 7.00424 14.6053 6.02271 13.9018 5.29904L9.85 1.13143C9.1465 0.406921 8.19178 -0.000123228 7.19625 0L3.75 0C1.67893 0 0 1.7269 0 3.85714L0 14.1429C2.38419e-07 16.2731 1.67893 18 3.75 18L11.25 18C13.3211 18 15 16.2731 15 14.1429L15 8.02672ZM3.75 9.32143C3.75 8.78887 4.16973 8.35714 4.6875 8.35714L10.3125 8.35714C10.8303 8.35714 11.25 8.78887 11.25 9.32143C11.25 9.85399 10.8303 10.2857 10.3125 10.2857L4.6875 10.2857C4.16973 10.2857 3.75 9.85399 3.75 9.32143M4.6875 12.2143C4.35256 12.2143 4.04307 12.3981 3.8756 12.6964C3.70813 12.9948 3.70813 13.3624 3.8756 13.6607C4.04307 13.9591 4.35256 14.1429 4.6875 14.1429L7.8125 14.1429C8.14744 14.1429 8.45693 13.9591 8.6244 13.6607C8.79187 13.3624 8.79187 12.9948 8.6244 12.6964C8.45693 12.3981 8.14744 12.2143 7.8125 12.2143L4.6875 12.2143Z"
fill="#E5DFD5"
fill-rule="evenodd"
transform="translate(1.5 0)" />
</g>
</svg>
</template>
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g>
<path
d="M3.75 16.0714L11.25 16.0714C12.2855 16.0714 13.125 15.208 13.125 14.1429L13.125 8.02672C13.1248 7.5147 12.9269 7.02399 12.5748 6.66239L8.52375 2.493C8.17225 2.13169 7.69568 1.92868 7.19875 1.92857L3.75 1.92857C2.71447 1.92857 1.875 2.79202 1.875 3.85714L1.875 14.1429C1.875 15.208 2.71447 16.0714 3.75 16.0714M15 8.02672C15.0003 7.00424 14.6053 6.02271 13.9018 5.29904L9.85 1.13143C9.1465 0.406921 8.19178 -0.000123228 7.19625 0L3.75 0C1.67893 0 0 1.7269 0 3.85714L0 14.1429C2.38419e-07 16.2731 1.67893 18 3.75 18L11.25 18C13.3211 18 15 16.2731 15 14.1429L15 8.02672ZM3.75 9.32143C3.75 8.78887 4.16973 8.35714 4.6875 8.35714L10.3125 8.35714C10.8303 8.35714 11.25 8.78887 11.25 9.32143C11.25 9.85399 10.8303 10.2857 10.3125 10.2857L4.6875 10.2857C4.16973 10.2857 3.75 9.85399 3.75 9.32143M4.6875 12.2143C4.35256 12.2143 4.04307 12.3981 3.8756 12.6964C3.70813 12.9948 3.70813 13.3624 3.8756 13.6607C4.04307 13.9591 4.35256 14.1429 4.6875 14.1429L7.8125 14.1429C8.14744 14.1429 8.45693 13.9591 8.6244 13.6607C8.79187 13.3624 8.79187 12.9948 8.6244 12.6964C8.45693 12.3981 8.14744 12.2143 7.8125 12.2143L4.6875 12.2143Z"
fill="#E5DFD5"
fill-rule="evenodd"
transform="translate(1.5 0)" />
</g>
</svg>
</template>

View file

@ -1,7 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd"
d="M11 13.5H5A1.5 1.5 0 0 1 3.5 12V4A1.5 1.5 0 0 1 5 2.5h.5v.75c0 .414.336.75.75.75H7v2h-.75a.75.75 0 0 0-.75.75v.5c0 .414.336.75.75.75H7v2h-.75a.75.75 0 0 0-.75.75v.5c0 .414.336.75.75.75H7v-2h.75a.75.75 0 0 0 .75-.75v-.5A.75.75 0 0 0 7.75 8H7V6h.75a.75.75 0 0 0 .75-.75v-.5A.75.75 0 0 0 7.75 4H7V2.5h.757a1.5 1.5 0 0 1 1.061.44l3.243 3.242a1.5 1.5 0 0 1 .439 1.06V12a1.5 1.5 0 0 1-1.5 1.5m2.121-8.379A3 3 0 0 1 14 7.243V12a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V4a3 3 0 0 1 3-3h2.757a3 3 0 0 1 2.122.879z"
clip-rule="evenodd" />
</svg>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd"
d="M11 13.5H5A1.5 1.5 0 0 1 3.5 12V4A1.5 1.5 0 0 1 5 2.5h.5v.75c0 .414.336.75.75.75H7v2h-.75a.75.75 0 0 0-.75.75v.5c0 .414.336.75.75.75H7v2h-.75a.75.75 0 0 0-.75.75v.5c0 .414.336.75.75.75H7v-2h.75a.75.75 0 0 0 .75-.75v-.5A.75.75 0 0 0 7.75 8H7V6h.75a.75.75 0 0 0 .75-.75v-.5A.75.75 0 0 0 7.75 4H7V2.5h.757a1.5 1.5 0 0 1 1.061.44l3.243 3.242a1.5 1.5 0 0 1 .439 1.06V12a1.5 1.5 0 0 1-1.5 1.5m2.121-8.379A3 3 0 0 1 14 7.243V12a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V4a3 3 0 0 1 3-3h2.757a3 3 0 0 1 2.122.879z"
clip-rule="evenodd" />
</svg>
</template>

View file

@ -1,39 +1,39 @@
<template>
<svg
width="24px"
height="20px"
viewBox="0 0 24 20"
version="1.1"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg">
<defs>
<path d="M0 0L24 0L24 20L0 20L0 0Z" id="path_1" />
<clipPath id="clip_1">
<use
xlink:href="#path_1"
clip-rule="evenodd"
fill-rule="evenodd"
transform="translate(0, -2.133523)" />
</clipPath>
</defs>
<g id="cmd">
<path
d="M-751 -2016L-751 -2016L-751 -1996L-775 -1996L-775 -2016L-751 -2016Z"
id="cmd"
fill="none"
stroke="none" />
<path
d="M12 0L17.6 0C19.8402 0 20.9603 0 21.816 0.435974Q22.3804 0.723593 22.8284 1.17157Q23.2764 1.61955 23.564 2.18404C24 3.03969 24 4.15979 24 6.4L24 13.6C24 15.8402 24 16.9603 23.564 17.816Q23.2764 18.3804 22.8284 18.8284Q22.3804 19.2764 21.816 19.564C20.9603 20 19.8402 20 17.6 20L6.4 20C4.15979 20 3.03969 20 2.18404 19.564Q1.61955 19.2764 1.17157 18.8284Q0.723594 18.3804 0.435974 17.816C0 16.9603 0 15.8402 0 13.6L0 6.4C0 4.15979 0 3.03969 0.435974 2.18404Q0.723594 1.61955 1.17157 1.17157Q1.61955 0.723594 2.18404 0.435974C3.03969 0 4.15979 0 6.4 0L12 0Z"
id="Rectangle"
fill="#FFFFFF"
fill-opacity="0.050980393"
stroke="none" />
<g id="⌘" clip-path="url(#clip_1)" transform="translate(0 2.133523)">
<g transform="translate(5.5692472, 0)" id="⌘" fill="#E5DFD5">
<path
d="M3.55007 12.8061Q2.98224 12.8061 2.51598 12.5268Q2.04972 12.2475 1.77042 11.7789Q1.49112 11.3104 1.49112 10.7472Q1.49112 10.1747 1.77042 9.70614Q2.04972 9.23757 2.51598 8.95827Q2.98224 8.67898 3.55007 8.67898L4.5657 8.67898L4.5657 7.04474L3.55007 7.04474Q2.98224 7.04474 2.51598 6.76775Q2.04972 6.49077 1.77042 6.02219Q1.49112 5.55362 1.49112 4.98579Q1.49112 4.41797 1.77042 3.9494Q2.04972 3.48082 2.51598 3.20383Q2.98224 2.92685 3.55007 2.92684Q4.1179 2.92684 4.58647 3.20383Q5.05504 3.48082 5.33434 3.9494Q5.61364 4.41797 5.61364 4.98579L5.61364 5.99219L7.25249 5.99219L7.25249 4.98579Q7.25249 4.41797 7.52947 3.9494Q7.80646 3.48082 8.27504 3.20383Q8.74361 2.92685 9.31144 2.92684Q9.87926 2.92684 10.3455 3.20383Q10.8118 3.48082 11.0888 3.9494Q11.3658 4.41797 11.3658 4.98579Q11.3658 5.55362 11.0888 6.02219Q10.8118 6.49077 10.3455 6.76775Q9.87926 7.04474 9.31144 7.04474L8.30043 7.04474L8.30043 8.67898L9.31144 8.67898Q9.87926 8.67898 10.3455 8.95827Q10.8118 9.23757 11.0888 9.70614Q11.3658 10.1747 11.3658 10.7472Q11.3658 11.3104 11.0888 11.7789Q10.8118 12.2475 10.3455 12.5268Q9.87926 12.8061 9.31144 12.8061Q8.74361 12.8061 8.27504 12.5268Q7.80646 12.2475 7.52947 11.7789Q7.25249 11.3104 7.25249 10.7472L7.25249 9.73153L5.61364 9.73153L5.61364 10.7472Q5.61364 11.3104 5.33434 11.7789Q5.05504 12.2475 4.58647 12.5268Q4.1179 12.8061 3.55007 12.8061ZM3.55007 11.7536Q3.97017 11.7536 4.26563 11.4604Q4.56108 11.1673 4.5657 10.7472L4.5657 9.73153L3.55007 9.73153Q3.12997 9.73615 2.83452 10.0293Q2.53906 10.3224 2.53906 10.7472Q2.53906 11.1673 2.83452 11.4604Q3.12997 11.7536 3.55007 11.7536ZM9.31144 11.7536Q9.72692 11.7536 10.0224 11.4604Q10.3178 11.1673 10.3178 10.7472Q10.3178 10.3224 10.0224 10.0293Q9.72692 9.73615 9.31144 9.73153L8.30043 9.73153L8.30043 10.7472Q8.29581 11.1673 8.59357 11.4604Q8.89133 11.7536 9.31144 11.7536ZM3.55007 5.99219L4.5657 5.99219L4.5657 4.98579Q4.56108 4.56569 4.26563 4.27255Q3.97017 3.9794 3.55007 3.9794Q3.12997 3.9794 2.83452 4.27255Q2.53906 4.56569 2.53906 4.98579Q2.53906 5.40589 2.83452 5.70135Q3.12997 5.9968 3.55007 5.99219ZM8.30043 5.99219L9.31144 5.99219Q9.72692 5.9968 10.0224 5.70135Q10.3178 5.40589 10.3178 4.98579Q10.3178 4.56569 10.0224 4.27255Q9.72692 3.9794 9.31144 3.9794Q8.89133 3.9794 8.59357 4.27255Q8.29581 4.56569 8.30043 4.98579L8.30043 5.99219ZM5.61364 8.67898L7.25249 8.67898L7.25249 7.04474L5.61364 7.04474L5.61364 8.67898Z" />
</g>
</g>
</g>
</svg>
</template>
<template>
<svg
width="24px"
height="20px"
viewBox="0 0 24 20"
version="1.1"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg">
<defs>
<path d="M0 0L24 0L24 20L0 20L0 0Z" id="path_1" />
<clipPath id="clip_1">
<use
xlink:href="#path_1"
clip-rule="evenodd"
fill-rule="evenodd"
transform="translate(0, -2.133523)" />
</clipPath>
</defs>
<g id="cmd">
<path
d="M-751 -2016L-751 -2016L-751 -1996L-775 -1996L-775 -2016L-751 -2016Z"
id="cmd"
fill="none"
stroke="none" />
<path
d="M12 0L17.6 0C19.8402 0 20.9603 0 21.816 0.435974Q22.3804 0.723593 22.8284 1.17157Q23.2764 1.61955 23.564 2.18404C24 3.03969 24 4.15979 24 6.4L24 13.6C24 15.8402 24 16.9603 23.564 17.816Q23.2764 18.3804 22.8284 18.8284Q22.3804 19.2764 21.816 19.564C20.9603 20 19.8402 20 17.6 20L6.4 20C4.15979 20 3.03969 20 2.18404 19.564Q1.61955 19.2764 1.17157 18.8284Q0.723594 18.3804 0.435974 17.816C0 16.9603 0 15.8402 0 13.6L0 6.4C0 4.15979 0 3.03969 0.435974 2.18404Q0.723594 1.61955 1.17157 1.17157Q1.61955 0.723594 2.18404 0.435974C3.03969 0 4.15979 0 6.4 0L12 0Z"
id="Rectangle"
fill="#FFFFFF"
fill-opacity="0.050980393"
stroke="none" />
<g id="⌘" clip-path="url(#clip_1)" transform="translate(0 2.133523)">
<g transform="translate(5.5692472, 0)" id="⌘" fill="#E5DFD5">
<path
d="M3.55007 12.8061Q2.98224 12.8061 2.51598 12.5268Q2.04972 12.2475 1.77042 11.7789Q1.49112 11.3104 1.49112 10.7472Q1.49112 10.1747 1.77042 9.70614Q2.04972 9.23757 2.51598 8.95827Q2.98224 8.67898 3.55007 8.67898L4.5657 8.67898L4.5657 7.04474L3.55007 7.04474Q2.98224 7.04474 2.51598 6.76775Q2.04972 6.49077 1.77042 6.02219Q1.49112 5.55362 1.49112 4.98579Q1.49112 4.41797 1.77042 3.9494Q2.04972 3.48082 2.51598 3.20383Q2.98224 2.92685 3.55007 2.92684Q4.1179 2.92684 4.58647 3.20383Q5.05504 3.48082 5.33434 3.9494Q5.61364 4.41797 5.61364 4.98579L5.61364 5.99219L7.25249 5.99219L7.25249 4.98579Q7.25249 4.41797 7.52947 3.9494Q7.80646 3.48082 8.27504 3.20383Q8.74361 2.92685 9.31144 2.92684Q9.87926 2.92684 10.3455 3.20383Q10.8118 3.48082 11.0888 3.9494Q11.3658 4.41797 11.3658 4.98579Q11.3658 5.55362 11.0888 6.02219Q10.8118 6.49077 10.3455 6.76775Q9.87926 7.04474 9.31144 7.04474L8.30043 7.04474L8.30043 8.67898L9.31144 8.67898Q9.87926 8.67898 10.3455 8.95827Q10.8118 9.23757 11.0888 9.70614Q11.3658 10.1747 11.3658 10.7472Q11.3658 11.3104 11.0888 11.7789Q10.8118 12.2475 10.3455 12.5268Q9.87926 12.8061 9.31144 12.8061Q8.74361 12.8061 8.27504 12.5268Q7.80646 12.2475 7.52947 11.7789Q7.25249 11.3104 7.25249 10.7472L7.25249 9.73153L5.61364 9.73153L5.61364 10.7472Q5.61364 11.3104 5.33434 11.7789Q5.05504 12.2475 4.58647 12.5268Q4.1179 12.8061 3.55007 12.8061ZM3.55007 11.7536Q3.97017 11.7536 4.26563 11.4604Q4.56108 11.1673 4.5657 10.7472L4.5657 9.73153L3.55007 9.73153Q3.12997 9.73615 2.83452 10.0293Q2.53906 10.3224 2.53906 10.7472Q2.53906 11.1673 2.83452 11.4604Q3.12997 11.7536 3.55007 11.7536ZM9.31144 11.7536Q9.72692 11.7536 10.0224 11.4604Q10.3178 11.1673 10.3178 10.7472Q10.3178 10.3224 10.0224 10.0293Q9.72692 9.73615 9.31144 9.73153L8.30043 9.73153L8.30043 10.7472Q8.29581 11.1673 8.59357 11.4604Q8.89133 11.7536 9.31144 11.7536ZM3.55007 5.99219L4.5657 5.99219L4.5657 4.98579Q4.56108 4.56569 4.26563 4.27255Q3.97017 3.9794 3.55007 3.9794Q3.12997 3.9794 2.83452 4.27255Q2.53906 4.56569 2.53906 4.98579Q2.53906 5.40589 2.83452 5.70135Q3.12997 5.9968 3.55007 5.99219ZM8.30043 5.99219L9.31144 5.99219Q9.72692 5.9968 10.0224 5.70135Q10.3178 5.40589 10.3178 4.98579Q10.3178 4.56569 10.0224 4.27255Q9.72692 3.9794 9.31144 3.9794Q8.89133 3.9794 8.59357 4.27255Q8.29581 4.56569 8.30043 4.98579L8.30043 5.99219ZM5.61364 8.67898L7.25249 8.67898L7.25249 7.04474L5.61364 7.04474L5.61364 8.67898Z" />
</g>
</g>
</g>
</svg>
</template>

View file

@ -1,41 +1,41 @@
<template>
<svg
width="24px"
height="20px"
viewBox="0 0 24 20"
version="1.1"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg">
<g id="Enter" fill-opacity="1">
<path
d="M-659 -2016L-659 -2016L-659 -1996L-683 -1996L-683 -2016L-659 -2016Z"
id="Enter"
fill="none"
stroke="none" />
<path
d="M12 0L17.6 0C19.8402 0 20.9603 0 21.816 0.435974Q22.3804 0.723593 22.8284 1.17157Q23.2764 1.61955 23.564 2.18404C24 3.03969 24 4.15979 24 6.4L24 13.6C24 15.8402 24 16.9603 23.564 17.816Q23.2764 18.3804 22.8284 18.8284Q22.3804 19.2764 21.816 19.564C20.9603 20 19.8402 20 17.6 20L6.4 20C4.15979 20 3.03969 20 2.18404 19.564Q1.61955 19.2764 1.17157 18.8284Q0.723594 18.3804 0.435974 17.816C0 16.9603 0 15.8402 0 13.6L0 6.4C0 4.15979 0 3.03969 0.435974 2.18404Q0.723594 1.61955 1.17157 1.17157Q1.61955 0.723594 2.18404 0.435974C3.03969 0 4.15979 0 6.4 0L12 0Z"
id="Rectangle"
fill="#FFFFFF"
fill-opacity="0.050980393"
stroke="none" />
<path
d="M16.0597 5.48914L16.0597 10.5L7.5 10.5"
id="Vector"
fill="none"
fill-rule="evenodd"
stroke="#E5DFD5"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M9.5 8.5L9.5 12.5035L7 10.5L9.5 8.5Z"
id="Vector"
fill="#E5DFD5"
fill-rule="evenodd"
stroke="#E5DFD5"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round" />
</g>
</svg>
</template>
<template>
<svg
width="24px"
height="20px"
viewBox="0 0 24 20"
version="1.1"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg">
<g id="Enter" fill-opacity="1">
<path
d="M-659 -2016L-659 -2016L-659 -1996L-683 -1996L-683 -2016L-659 -2016Z"
id="Enter"
fill="none"
stroke="none" />
<path
d="M12 0L17.6 0C19.8402 0 20.9603 0 21.816 0.435974Q22.3804 0.723593 22.8284 1.17157Q23.2764 1.61955 23.564 2.18404C24 3.03969 24 4.15979 24 6.4L24 13.6C24 15.8402 24 16.9603 23.564 17.816Q23.2764 18.3804 22.8284 18.8284Q22.3804 19.2764 21.816 19.564C20.9603 20 19.8402 20 17.6 20L6.4 20C4.15979 20 3.03969 20 2.18404 19.564Q1.61955 19.2764 1.17157 18.8284Q0.723594 18.3804 0.435974 17.816C0 16.9603 0 15.8402 0 13.6L0 6.4C0 4.15979 0 3.03969 0.435974 2.18404Q0.723594 1.61955 1.17157 1.17157Q1.61955 0.723594 2.18404 0.435974C3.03969 0 4.15979 0 6.4 0L12 0Z"
id="Rectangle"
fill="#FFFFFF"
fill-opacity="0.050980393"
stroke="none" />
<path
d="M16.0597 5.48914L16.0597 10.5L7.5 10.5"
id="Vector"
fill="none"
fill-rule="evenodd"
stroke="#E5DFD5"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M9.5 8.5L9.5 12.5035L7 10.5L9.5 8.5Z"
id="Vector"
fill="#E5DFD5"
fill-rule="evenodd"
stroke="#E5DFD5"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round" />
</g>
</svg>
</template>

View file

@ -1,30 +1,30 @@
<template>
<div
class="key-container"
:style="{
backgroundColor: 'var(--border)',
padding: '0 7px',
height: '20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '4px',
minWidth: '22px'
}"
>
<span
:style="{
color: '#E5E0D5',
fontSize: '12px'
}"
>
{{ input }}
</span>
</div>
</template>
<script setup lang="ts">
defineProps<{
input: string;
}>();
</script>
<template>
<div
class="key-container"
:style="{
backgroundColor: 'var(--border)',
padding: '0 7px',
height: '20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '4px',
minWidth: '22px'
}"
>
<span
:style="{
color: '#E5E0D5',
fontSize: '12px'
}"
>
{{ input }}
</span>
</div>
</template>
<script setup lang="ts">
defineProps<{
input: string;
}>();
</script>

View file

@ -1,12 +1,12 @@
<template>
<svg width="24" height="20" viewBox="0 0 24 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<path
d="M12 0L17.6 0C19.8402 0 20.9603 0 21.816 0.435974Q22.3804 0.723593 22.8284 1.17157Q23.2764 1.61955 23.564 2.18404C24 3.03969 24 4.15979 24 6.4L24 13.6C24 15.8402 24 16.9603 23.564 17.816Q23.2764 18.3804 22.8284 18.8284Q22.3804 19.2764 21.816 19.564C20.9603 20 19.8402 20 17.6 20L6.4 20C4.15979 20 3.03969 20 2.18404 19.564Q1.61955 19.2764 1.17157 18.8284Q0.723594 18.3804 0.435974 17.816C0 16.9603 0 15.8402 0 13.6L0 6.4C0 4.15979 0 3.03969 0.435974 2.18404Q0.723594 1.61955 1.17157 1.17157Q1.61955 0.723594 2.18404 0.435974C3.03969 0 4.15979 0 6.4 0L12 0Z"
fill="#FFFFFF" fill-opacity="0.051" />
<path
d="M4.9427 0.0799475L0.154716 5.27144Q0.144837 5.28216 0.138427 5.29524Q0.132016 5.30833 0.129608 5.3227Q0.127199 5.33707 0.128993 5.35153Q0.130787 5.36599 0.136635 5.37934Q0.142482 5.39269 0.151896 5.40381Q0.16131 5.41493 0.173507 5.42291Q0.185705 5.43088 0.19967 5.43504Q0.213636 5.4392 0.228208 5.4392L3.06459 5.4392Q3.08448 5.4392 3.10285 5.44681Q3.12123 5.45442 3.13529 5.46848Q3.14935 5.48254 3.15696 5.50092Q3.16457 5.51929 3.16457 5.53917L3.16457 9.90003Q3.16457 9.91991 3.17218 9.93828Q3.17979 9.95666 3.19385 9.97072Q3.20791 9.98478 3.22629 9.99239Q3.24466 10 3.26455 10L6.74521 10Q6.7651 10 6.78347 9.99239Q6.80184 9.98478 6.8159 9.97072Q6.82997 9.95666 6.83758 9.93828Q6.84519 9.91991 6.84519 9.90003L6.84519 5.53917Q6.84519 5.51929 6.8528 5.50091Q6.86041 5.48254 6.87447 5.46848Q6.88853 5.45442 6.9069 5.44681Q6.92527 5.4392 6.94516 5.4392L9.77281 5.4392Q9.78736 5.4392 9.8013 5.43505Q9.81525 5.4309 9.82744 5.42295Q9.83962 5.415 9.84904 5.4039Q9.85845 5.39281 9.86431 5.37949Q9.87017 5.36617 9.87199 5.35173Q9.87382 5.3373 9.87145 5.32294Q9.86908 5.30858 9.86271 5.2955Q9.85635 5.28241 9.84652 5.27168L5.0899 0.0801888Q5.07573 0.064719 5.05653 0.0562526Q5.03734 0.0477862 5.01635 0.0477518Q4.99537 0.0477174 4.97615 0.0561208Q4.95692 0.0645242 4.9427 0.0799475Z"
fill="none" stroke-width="1.3" stroke="#E5DFD5" transform="translate(7 5)" />
</g>
</svg>
<template>
<svg width="24" height="20" viewBox="0 0 24 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<path
d="M12 0L17.6 0C19.8402 0 20.9603 0 21.816 0.435974Q22.3804 0.723593 22.8284 1.17157Q23.2764 1.61955 23.564 2.18404C24 3.03969 24 4.15979 24 6.4L24 13.6C24 15.8402 24 16.9603 23.564 17.816Q23.2764 18.3804 22.8284 18.8284Q22.3804 19.2764 21.816 19.564C20.9603 20 19.8402 20 17.6 20L6.4 20C4.15979 20 3.03969 20 2.18404 19.564Q1.61955 19.2764 1.17157 18.8284Q0.723594 18.3804 0.435974 17.816C0 16.9603 0 15.8402 0 13.6L0 6.4C0 4.15979 0 3.03969 0.435974 2.18404Q0.723594 1.61955 1.17157 1.17157Q1.61955 0.723594 2.18404 0.435974C3.03969 0 4.15979 0 6.4 0L12 0Z"
fill="#FFFFFF" fill-opacity="0.051" />
<path
d="M4.9427 0.0799475L0.154716 5.27144Q0.144837 5.28216 0.138427 5.29524Q0.132016 5.30833 0.129608 5.3227Q0.127199 5.33707 0.128993 5.35153Q0.130787 5.36599 0.136635 5.37934Q0.142482 5.39269 0.151896 5.40381Q0.16131 5.41493 0.173507 5.42291Q0.185705 5.43088 0.19967 5.43504Q0.213636 5.4392 0.228208 5.4392L3.06459 5.4392Q3.08448 5.4392 3.10285 5.44681Q3.12123 5.45442 3.13529 5.46848Q3.14935 5.48254 3.15696 5.50092Q3.16457 5.51929 3.16457 5.53917L3.16457 9.90003Q3.16457 9.91991 3.17218 9.93828Q3.17979 9.95666 3.19385 9.97072Q3.20791 9.98478 3.22629 9.99239Q3.24466 10 3.26455 10L6.74521 10Q6.7651 10 6.78347 9.99239Q6.80184 9.98478 6.8159 9.97072Q6.82997 9.95666 6.83758 9.93828Q6.84519 9.91991 6.84519 9.90003L6.84519 5.53917Q6.84519 5.51929 6.8528 5.50091Q6.86041 5.48254 6.87447 5.46848Q6.88853 5.45442 6.9069 5.44681Q6.92527 5.4392 6.94516 5.4392L9.77281 5.4392Q9.78736 5.4392 9.8013 5.43505Q9.81525 5.4309 9.82744 5.42295Q9.83962 5.415 9.84904 5.4039Q9.85845 5.39281 9.86431 5.37949Q9.87017 5.36617 9.87199 5.35173Q9.87382 5.3373 9.87145 5.32294Q9.86908 5.30858 9.86271 5.2955Q9.85635 5.28241 9.84652 5.27168L5.0899 0.0801888Q5.07573 0.064719 5.05653 0.0562526Q5.03734 0.0477862 5.01635 0.0477518Q4.99537 0.0477174 4.97615 0.0561208Q4.95692 0.0645242 4.9427 0.0799475Z"
fill="none" stroke-width="1.3" stroke="#E5DFD5" transform="translate(7 5)" />
</g>
</svg>
</template>

View file

@ -1,23 +1,23 @@
<template>
<div class="noise"></div>
</template>
<style scoped lang="scss">
.noise {
position: absolute;
overflow: hidden;
top: 1px;
left: 1px;
width: calc(100vw - 2px);
height: calc(100vh - 2px);
pointer-events: none;
user-select: none;
background-image: url("/noise.webp");
background-repeat: repeat;
image-rendering: pixelated;
overflow: hidden;
mix-blend-mode: multiply;
opacity: .5;
border-radius: 12px;
}
</style>
<template>
<div class="noise"></div>
</template>
<style scoped lang="scss">
.noise {
position: absolute;
overflow: hidden;
top: 1px;
left: 1px;
width: calc(100vw - 2px);
height: calc(100vh - 2px);
pointer-events: none;
user-select: none;
background-image: url("/noise.webp");
background-repeat: repeat;
image-rendering: pixelated;
overflow: hidden;
mix-blend-mode: multiply;
opacity: .5;
border-radius: 12px;
}
</style>

View file

@ -1,122 +1,122 @@
<template>
<div
:class="['result', { selected }]"
@click="$emit('select')"
:ref="el => { if (selected && el) $emit('setRef', el as HTMLElement) }">
<template v-if="item.content_type === 'image'">
<img
v-if="imageUrl"
:src="imageUrl"
alt="Image"
class="image"
@error="$emit('imageError')" />
<IconsImage v-else class="icon" />
</template>
<template v-else-if="hasFavicon(item.favicon ?? '')">
<img
v-if="item.favicon"
:src="getFaviconFromDb(item.favicon)"
alt="Favicon"
class="favicon"
@error="
($event.target as HTMLImageElement).src = '/public/icons/Link.svg'
" />
<IconsLink v-else class="icon" />
</template>
<IconsFile
class="icon"
v-else-if="item.content_type === ContentType.File" />
<IconsText
class="icon"
v-else-if="item.content_type === ContentType.Text" />
<svg
v-else-if="item.content_type === ContentType.Color"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g>
<rect width="18" height="18" />
<path
d="M9 18C12.2154 18 15.1865 16.2846 16.7942 13.5C18.4019 10.7154 18.4019 7.28461 16.7942 4.5C15.1865 1.71539 12.2154 -1.22615e-06 9 0C5.78461 0 2.81347 1.71539 1.20577 4.5C-0.401925 7.28461 -0.401923 10.7154 1.20577 13.5C2.81347 16.2846 5.78461 18 9 18Z"
fill="#E5DFD5" />
<path
d="M9 16C7.14348 16 5.36301 15.2625 4.05025 13.9497C2.7375 12.637 2 10.8565 2 9C2 7.14348 2.7375 5.36301 4.05025 4.05025C5.36301 2.7375 7.14348 2 9 2C10.8565 2 12.637 2.7375 13.9497 4.05025C15.2625 5.36301 16 7.14348 16 9C16 10.8565 15.2625 12.637 13.9497 13.9497C12.637 15.2625 10.8565 16 9 16Z"
:fill="item.content" />
</g>
</svg>
<IconsCode
class="icon"
v-else-if="item.content_type === ContentType.Code" />
<span v-if="item.content_type === ContentType.Image">
Image ({{ dimensions || "Loading..." }})
</span>
<span v-else>{{ truncateContent(item.content) }}</span>
</div>
</template>
<script setup lang="ts">
import { ContentType } from "~/types/types";
import type { HistoryItem } from "~/types/types";
defineProps<{
item: HistoryItem;
selected: boolean;
imageUrl?: string;
dimensions?: string;
}>();
defineEmits<{
(e: "select"): void;
(e: "imageError"): void;
(e: "setRef", el: HTMLElement): void;
}>();
const hasFavicon = (str: string): boolean => {
return str.trim() !== "";
};
const getFaviconFromDb = (favicon: string): string => {
return `data:image/png;base64,${favicon}`;
};
const truncateContent = (content: string): string => {
const maxWidth = 284;
const charWidth = 9;
const maxChars = Math.floor(maxWidth / charWidth);
return content.length > maxChars
? content.slice(0, maxChars - 3) + "..."
: content;
};
</script>
<style scoped lang="scss">
.result {
display: flex;
gap: 12px;
padding: 11px;
border-radius: 10px;
cursor: pointer;
align-items: center;
&.selected {
background-color: var(--border);
}
.favicon,
.image,
.icon {
width: 18px;
height: 18px;
}
span {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text);
}
}
</style>
<template>
<div
:class="['result', { selected }]"
@click="$emit('select')"
:ref="el => { if (selected && el) $emit('setRef', el as HTMLElement) }">
<template v-if="item.content_type === 'image'">
<img
v-if="imageUrl"
:src="imageUrl"
alt="Image"
class="image"
@error="$emit('imageError')" />
<IconsImage v-else class="icon" />
</template>
<template v-else-if="hasFavicon(item.favicon ?? '')">
<img
v-if="item.favicon"
:src="getFaviconFromDb(item.favicon)"
alt="Favicon"
class="favicon"
@error="
($event.target as HTMLImageElement).src = '/public/icons/Link.svg'
" />
<IconsLink v-else class="icon" />
</template>
<IconsFile
class="icon"
v-else-if="item.content_type === ContentType.File" />
<IconsText
class="icon"
v-else-if="item.content_type === ContentType.Text" />
<svg
v-else-if="item.content_type === ContentType.Color"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g>
<rect width="18" height="18" />
<path
d="M9 18C12.2154 18 15.1865 16.2846 16.7942 13.5C18.4019 10.7154 18.4019 7.28461 16.7942 4.5C15.1865 1.71539 12.2154 -1.22615e-06 9 0C5.78461 0 2.81347 1.71539 1.20577 4.5C-0.401925 7.28461 -0.401923 10.7154 1.20577 13.5C2.81347 16.2846 5.78461 18 9 18Z"
fill="#E5DFD5" />
<path
d="M9 16C7.14348 16 5.36301 15.2625 4.05025 13.9497C2.7375 12.637 2 10.8565 2 9C2 7.14348 2.7375 5.36301 4.05025 4.05025C5.36301 2.7375 7.14348 2 9 2C10.8565 2 12.637 2.7375 13.9497 4.05025C15.2625 5.36301 16 7.14348 16 9C16 10.8565 15.2625 12.637 13.9497 13.9497C12.637 15.2625 10.8565 16 9 16Z"
:fill="item.content" />
</g>
</svg>
<IconsCode
class="icon"
v-else-if="item.content_type === ContentType.Code" />
<span v-if="item.content_type === ContentType.Image">
Image ({{ dimensions || "Loading..." }})
</span>
<span v-else>{{ truncateContent(item.content) }}</span>
</div>
</template>
<script setup lang="ts">
import { ContentType } from "~/types/types";
import type { HistoryItem } from "~/types/types";
defineProps<{
item: HistoryItem;
selected: boolean;
imageUrl?: string;
dimensions?: string;
}>();
defineEmits<{
(e: "select"): void;
(e: "imageError"): void;
(e: "setRef", el: HTMLElement): void;
}>();
const hasFavicon = (str: string): boolean => {
return str.trim() !== "";
};
const getFaviconFromDb = (favicon: string): string => {
return `data:image/png;base64,${favicon}`;
};
const truncateContent = (content: string): string => {
const maxWidth = 284;
const charWidth = 9;
const maxChars = Math.floor(maxWidth / charWidth);
return content.length > maxChars
? content.slice(0, maxChars - 3) + "..."
: content;
};
</script>
<style scoped lang="scss">
.result {
display: flex;
gap: 12px;
padding: 11px;
border-radius: 10px;
cursor: pointer;
align-items: center;
&.selected {
background-color: var(--border);
}
.favicon,
.image,
.icon {
width: 18px;
height: 18px;
}
span {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text);
}
}
</style>

View file

@ -1,57 +1,57 @@
<template>
<div class="topbar">
<input
ref="searchInput"
v-model="searchQuery"
@input="onInputChange"
class="search"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
type="text"
placeholder="Type to filter entries..." />
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const searchQuery = ref("");
const searchInput = ref<HTMLInputElement | null>(null);
const emit = defineEmits<{
(e: "search", query: string): void;
(e: "searchStarted"): void;
(e: "focus"): void;
}>();
const onInputChange = () => {
emit("searchStarted");
emit("search", searchQuery.value);
};
defineExpose({ searchInput });
</script>
<style lang="scss">
.topbar {
width: 100%;
min-height: 56px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding-inline: 16px;
z-index: 100;
.search {
width: 100%;
height: 100%;
font-size: 18px;
color: var(--text);
background-color: transparent;
outline: none;
border: none;
font-family: SFRoundedMedium;
}
}
</style>
<template>
<div class="topbar">
<input
ref="searchInput"
v-model="searchQuery"
@input="onInputChange"
class="search"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
type="text"
placeholder="Type to filter entries..." />
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const searchQuery = ref("");
const searchInput = ref<HTMLInputElement | null>(null);
const emit = defineEmits<{
(e: "search", query: string): void;
(e: "searchStarted"): void;
(e: "focus"): void;
}>();
const onInputChange = () => {
emit("searchStarted");
emit("search", searchQuery.value);
};
defineExpose({ searchInput });
</script>
<style lang="scss">
.topbar {
width: 100%;
min-height: 56px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding-inline: 16px;
z-index: 100;
.search {
width: 100%;
height: 100%;
font-size: 18px;
color: var(--text);
background-color: transparent;
outline: none;
border: none;
font-family: SFRoundedMedium;
}
}
</style>

View file

@ -1,222 +1,222 @@
import { invoke } from "@tauri-apps/api/core";
import { HistoryItem } from "../types/types";
const { $history } = useNuxtApp();
const { hideApp } = useAppControl();
export function useActions() {
const isProcessing = ref(false);
const handleAction = async (action: string, item?: HistoryItem) => {
if (!item && action !== "settings" && action !== "delete-all") return;
isProcessing.value = true;
try {
switch (action) {
case "paste-to-app":
await pasteToCurrentApp(item);
break;
case "copy":
// await copyToClipboard(item);
break;
case "delete":
await deleteEntry(item);
break;
case "delete-all":
// await deleteAllEntries();
break;
case "settings":
openSettings();
break;
case "paste-plain":
// await pasteAsPlainText(item);
break;
case "edit-text":
// openTextEditor(item);
break;
case "rotate-image":
// await rotateImage(item);
break;
case "resize-image":
// openImageResizer(item);
break;
case "compress-image":
// await compressImage(item);
break;
case "open-file":
// await openFile(item);
break;
case "compress-file":
// await compressFile(item);
break;
case "open-link":
// await openInBrowser(item);
break;
case "copy-hex":
// await copyColorFormat(item, "hex");
break;
case "copy-rgba":
// await copyColorFormat(item, "rgba");
break;
case "copy-hsla":
// await copyColorFormat(item, "hsla");
break;
default:
console.warn(`Action ${action} not implemented`);
}
} catch (error) {
console.error(`Error executing action ${action}:`, error);
} finally {
isProcessing.value = false;
}
};
const pasteToCurrentApp = async (item?: HistoryItem) => {
if (!item) return;
let content = item.content;
let contentType: string = item.content_type;
if (contentType === "image") {
try {
content = await $history.readImage({ filename: content });
} catch (error) {
console.error("Error reading image file:", error);
return;
}
}
await hideApp();
await $history.writeAndPaste({ content, contentType });
};
// const copyToClipboard = async (item?: HistoryItem) => {
// if (!item) return;
// try {
// switch (item.content_type) {
// case ContentType.Text:
// case ContentType.Link:
// case ContentType.Code:
// await writeText(item.content);
// break;
// case ContentType.Image:
// await invoke("copy_image_to_clipboard", { path: item.file_path });
// break;
// case ContentType.File:
// await invoke("copy_file_reference", { path: item.file_path });
// break;
// case ContentType.Color:
// await writeText(item.content);
// break;
// default:
// console.warn(`Copying type ${item.content_type} not implemented`);
// }
// } catch (error) {
// console.error("Failed to copy to clipboard:", error);
// }
// };
const deleteEntry = async (item?: HistoryItem) => {
if (!item) return;
try {
await invoke("delete_history_item", { id: item.id });
} catch (error) {
console.error("Failed to delete entry:", error);
}
};
// const deleteAllEntries = async () => {
// try {
// await invoke('delete_all_history');
// } catch (error) {
// console.error('Failed to delete all entries:', error);
// }
// };
const openSettings = () => {
navigateTo("/settings");
};
// const pasteAsPlainText = async (item?: HistoryItem) => {
// if (!item) return;
// try {
// await invoke('paste_as_plain_text', { content: item.content });
// } catch (error) {
// console.error('Failed to paste as plain text:', error);
// }
// };
// const openTextEditor = (item?: HistoryItem) => {
// if (!item) return;
// // Implement logic to open text editor with the content
// // This might use Nuxt router or a modal based on your app architecture
// };
// const rotateImage = async (item?: HistoryItem) => {
// if (!item || item.content_type !== ContentType.Image) return;
// try {
// await invoke('rotate_image', { path: item.file_path });
// } catch (error) {
// console.error('Failed to rotate image:', error);
// }
// };
// const openImageResizer = (item?: HistoryItem) => {
// if (!item || item.content_type !== ContentType.Image) return;
// // Implement logic to open image resizer UI for this image
// };
// const compressImage = async (item?: HistoryItem) => {
// if (!item || item.content_type !== ContentType.Image) return;
// try {
// await invoke('compress_image', { path: item.file_path });
// } catch (error) {
// console.error('Failed to compress image:', error);
// }
// };
// const openFile = async (item?: HistoryItem) => {
// if (!item || item.content_type !== ContentType.File) return;
// try {
// await invoke('open_file', { path: item.file_path });
// } catch (error) {
// console.error('Failed to open file:', error);
// }
// };
// const compressFile = async (item?: HistoryItem) => {
// if (!item || item.content_type !== ContentType.File) return;
// try {
// await invoke('compress_file', { path: item.file_path });
// } catch (error) {
// console.error('Failed to compress file:', error);
// }
// };
// const openInBrowser = async (item?: HistoryItem) => {
// if (!item || item.content_type !== ContentType.Link) return;
// try {
// await invoke('open_url', { url: item.content });
// } catch (error) {
// console.error('Failed to open URL in browser:', error);
// }
// };
// const copyColorFormat = async (item?: HistoryItem, format: 'hex' | 'rgba' | 'hsla' = 'hex') => {
// if (!item || item.content_type !== ContentType.Color) return;
// try {
// const formattedColor = await invoke('get_color_format', {
// color: item.content,
// format
// });
// await writeText(formattedColor as string);
// } catch (error) {
// console.error(`Failed to copy color as ${format}:`, error);
// }
// };
return {
handleAction,
isProcessing,
};
}
import { invoke } from "@tauri-apps/api/core";
import { HistoryItem } from "../types/types";
const { $history } = useNuxtApp();
const { hideApp } = useAppControl();
export function useActions() {
const isProcessing = ref(false);
const handleAction = async (action: string, item?: HistoryItem) => {
if (!item && action !== "settings" && action !== "delete-all") return;
isProcessing.value = true;
try {
switch (action) {
case "paste-to-app":
await pasteToCurrentApp(item);
break;
case "copy":
// await copyToClipboard(item);
break;
case "delete":
await deleteEntry(item);
break;
case "delete-all":
// await deleteAllEntries();
break;
case "settings":
openSettings();
break;
case "paste-plain":
// await pasteAsPlainText(item);
break;
case "edit-text":
// openTextEditor(item);
break;
case "rotate-image":
// await rotateImage(item);
break;
case "resize-image":
// openImageResizer(item);
break;
case "compress-image":
// await compressImage(item);
break;
case "open-file":
// await openFile(item);
break;
case "compress-file":
// await compressFile(item);
break;
case "open-link":
// await openInBrowser(item);
break;
case "copy-hex":
// await copyColorFormat(item, "hex");
break;
case "copy-rgba":
// await copyColorFormat(item, "rgba");
break;
case "copy-hsla":
// await copyColorFormat(item, "hsla");
break;
default:
console.warn(`Action ${action} not implemented`);
}
} catch (error) {
console.error(`Error executing action ${action}:`, error);
} finally {
isProcessing.value = false;
}
};
const pasteToCurrentApp = async (item?: HistoryItem) => {
if (!item) return;
let content = item.content;
let contentType: string = item.content_type;
if (contentType === "image") {
try {
content = await $history.readImage({ filename: content });
} catch (error) {
console.error("Error reading image file:", error);
return;
}
}
await hideApp();
await $history.writeAndPaste({ content, contentType });
};
// const copyToClipboard = async (item?: HistoryItem) => {
// if (!item) return;
// try {
// switch (item.content_type) {
// case ContentType.Text:
// case ContentType.Link:
// case ContentType.Code:
// await writeText(item.content);
// break;
// case ContentType.Image:
// await invoke("copy_image_to_clipboard", { path: item.file_path });
// break;
// case ContentType.File:
// await invoke("copy_file_reference", { path: item.file_path });
// break;
// case ContentType.Color:
// await writeText(item.content);
// break;
// default:
// console.warn(`Copying type ${item.content_type} not implemented`);
// }
// } catch (error) {
// console.error("Failed to copy to clipboard:", error);
// }
// };
const deleteEntry = async (item?: HistoryItem) => {
if (!item) return;
try {
await invoke("delete_history_item", { id: item.id });
} catch (error) {
console.error("Failed to delete entry:", error);
}
};
// const deleteAllEntries = async () => {
// try {
// await invoke('delete_all_history');
// } catch (error) {
// console.error('Failed to delete all entries:', error);
// }
// };
const openSettings = () => {
navigateTo("/settings");
};
// const pasteAsPlainText = async (item?: HistoryItem) => {
// if (!item) return;
// try {
// await invoke('paste_as_plain_text', { content: item.content });
// } catch (error) {
// console.error('Failed to paste as plain text:', error);
// }
// };
// const openTextEditor = (item?: HistoryItem) => {
// if (!item) return;
// // Implement logic to open text editor with the content
// // This might use Nuxt router or a modal based on your app architecture
// };
// const rotateImage = async (item?: HistoryItem) => {
// if (!item || item.content_type !== ContentType.Image) return;
// try {
// await invoke('rotate_image', { path: item.file_path });
// } catch (error) {
// console.error('Failed to rotate image:', error);
// }
// };
// const openImageResizer = (item?: HistoryItem) => {
// if (!item || item.content_type !== ContentType.Image) return;
// // Implement logic to open image resizer UI for this image
// };
// const compressImage = async (item?: HistoryItem) => {
// if (!item || item.content_type !== ContentType.Image) return;
// try {
// await invoke('compress_image', { path: item.file_path });
// } catch (error) {
// console.error('Failed to compress image:', error);
// }
// };
// const openFile = async (item?: HistoryItem) => {
// if (!item || item.content_type !== ContentType.File) return;
// try {
// await invoke('open_file', { path: item.file_path });
// } catch (error) {
// console.error('Failed to open file:', error);
// }
// };
// const compressFile = async (item?: HistoryItem) => {
// if (!item || item.content_type !== ContentType.File) return;
// try {
// await invoke('compress_file', { path: item.file_path });
// } catch (error) {
// console.error('Failed to compress file:', error);
// }
// };
// const openInBrowser = async (item?: HistoryItem) => {
// if (!item || item.content_type !== ContentType.Link) return;
// try {
// await invoke('open_url', { url: item.content });
// } catch (error) {
// console.error('Failed to open URL in browser:', error);
// }
// };
// const copyColorFormat = async (item?: HistoryItem, format: 'hex' | 'rgba' | 'hsla' = 'hex') => {
// if (!item || item.content_type !== ContentType.Color) return;
// try {
// const formattedColor = await invoke('get_color_format', {
// color: item.content,
// format
// });
// await writeText(formattedColor as string);
// } catch (error) {
// console.error(`Failed to copy color as ${format}:`, error);
// }
// };
return {
handleAction,
isProcessing,
};
}

View file

@ -1,12 +1,12 @@
import { app, window } from "@tauri-apps/api";
export function useAppControl() {
const hideApp = async (): Promise<void> => {
await app.hide();
await window.getCurrentWindow().hide();
};
return {
hideApp
};
import { app, window } from "@tauri-apps/api";
export function useAppControl() {
const hideApp = async (): Promise<void> => {
await app.hide();
await window.getCurrentWindow().hide();
};
return {
hideApp
};
}

View file

@ -1,24 +1,24 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: false },
compatibilityDate: "2024-07-04",
ssr: false,
app: {
head: {
charset: "utf-8",
viewport:
"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0",
},
},
vite: {
css: {
preprocessorOptions: {
scss: {
api: "modern-compiler",
},
},
},
},
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: false },
compatibilityDate: "2024-07-04",
ssr: false,
app: {
head: {
charset: "utf-8",
viewport:
"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0",
},
},
vite: {
css: {
preprocessorOptions: {
scss: {
api: "modern-compiler",
},
},
},
},
});

View file

@ -1,28 +1,28 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "tauri build",
"dev": "tauri dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@tauri-apps/api": "2.3.0",
"@tauri-apps/cli": "2.3.1",
"@tauri-apps/plugin-autostart": "2.2.0",
"@tauri-apps/plugin-os": "2.2.1",
"nuxt": "3.16.0",
"overlayscrollbars": "2.11.1",
"overlayscrollbars-vue": "0.5.9",
"sass-embedded": "1.85.1",
"uuid": "11.1.0",
"vue": "3.5.13",
"@waradu/keyboard": "4.2.0"
},
"overrides": {
"chokidar": "^3.6.0"
}
}
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "tauri build",
"dev": "tauri dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@tauri-apps/api": "2.3.0",
"@tauri-apps/cli": "2.3.1",
"@tauri-apps/plugin-autostart": "2.2.0",
"@tauri-apps/plugin-os": "2.2.1",
"nuxt": "3.16.0",
"overlayscrollbars": "2.11.1",
"overlayscrollbars-vue": "0.5.9",
"sass-embedded": "1.85.1",
"uuid": "11.1.0",
"vue": "3.5.13",
"@waradu/keyboard": "4.2.0"
},
"overrides": {
"chokidar": "^3.6.0"
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,209 +1,209 @@
<template>
<main>
<div class="top-bar">
<NuxtLink to="/" class="back">
<img src="../public/back_arrow.svg" />
<p>Back</p>
</NuxtLink>
</div>
<div class="settings-container">
<div class="settings">
<div class="names">
<p style="line-height: 14px">Startup</p>
<p style="line-height: 34px">Qopy Hotkey</p>
</div>
<div class="actions">
<div class="launch">
<input
type="checkbox"
id="launch"
v-model="autostart"
@change="toggleAutostart" />
<label for="launch" class="checkmark">
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g>
<rect width="14" height="14" />
<path
id="Path"
d="M0 2.00696L2.25015 4.25L6 0"
fill="none"
stroke-width="1.5"
stroke="#E5DFD5"
stroke-linecap="round"
stroke-linejoin="round"
transform="translate(4 5)" />
</g>
</svg>
</label>
<p for="launch">Launch Qopy at login</p>
</div>
<div
@blur="onBlur"
@focus="onFocus"
class="keybind-input"
ref="keybindInput"
tabindex="0"
:class="{ 'empty-keybind': showEmptyKeybindError }">
<span class="key" v-if="keybind.length === 0">Click here</span>
<template v-else>
<span
:key="index"
class="key"
:class="{ modifier: isModifier(key) }"
v-for="(key, index) in keybind">
{{ keyToLabel(key) }}
</span>
</template>
</div>
</div>
</div>
</div>
<BottomBar
:primary-action="{
text: 'Save',
icon: IconsEnter,
onClick: saveKeybind,
showModifier: true,
}" />
</main>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref, watch } from "vue";
import { platform } from "@tauri-apps/plugin-os";
import { useRouter } from "vue-router";
import { KeyValues, KeyLabels } from "../types/keys";
import { disable, enable } from "@tauri-apps/plugin-autostart";
import { useNuxtApp } from "#app";
import BottomBar from "../components/BottomBar.vue";
import IconsEnter from "~/components/Keys/Enter.vue";
const activeModifiers = reactive<Set<KeyValues>>(new Set());
const isKeybindInputFocused = ref(false);
const keybind = ref<KeyValues[]>([]);
const keybindInput = ref<HTMLElement | null>(null);
const blurredByEscape = ref(false);
const router = useRouter();
const showEmptyKeybindError = ref(false);
const autostart = ref(false);
const { $settings, $keyboard } = useNuxtApp();
const listeners: Array<() => void> = [];
const modifierKeySet = new Set([
KeyValues.AltLeft,
KeyValues.AltRight,
KeyValues.ControlLeft,
KeyValues.ControlRight,
KeyValues.MetaLeft,
KeyValues.MetaRight,
KeyValues.ShiftLeft,
KeyValues.ShiftRight,
]);
const isModifier = (key: KeyValues): boolean => {
return modifierKeySet.has(key);
};
const keyToLabel = (key: KeyValues): string => {
return KeyLabels[key] || key;
};
const updateKeybindDisplay = () => {
const modifiers = Array.from(activeModifiers);
const nonModifiers = keybind.value.filter((key) => !isModifier(key));
const sortedModifiers = modifiers.sort();
keybind.value = [...sortedModifiers, ...nonModifiers];
};
const onBlur = () => {
isKeybindInputFocused.value = false;
showEmptyKeybindError.value = false;
};
const onFocus = () => {
isKeybindInputFocused.value = true;
blurredByEscape.value = false;
activeModifiers.clear();
keybind.value = [];
showEmptyKeybindError.value = false;
const unlistenAll = $keyboard.listen([$keyboard.Key.All], (event: KeyboardEvent) => {
event.preventDefault();
event.stopPropagation();
const key = event.code as KeyValues;
if (key === KeyValues.Escape) {
blurredByEscape.value = true;
keybindInput.value?.blur();
return;
}
if (isModifier(key)) {
activeModifiers.add(key);
} else {
const nonModifierKey = keybind.value.find(k => !isModifier(k));
if (!nonModifierKey || nonModifierKey === key) {
keybind.value = Array.from(activeModifiers);
if (nonModifierKey !== key) keybind.value.push(key);
} else {
keybind.value = [ ...Array.from(activeModifiers), key];
}
}
updateKeybindDisplay();
showEmptyKeybindError.value = false;
}, { prevent: true });
listeners.push(unlistenAll);
};
const saveKeybind = async () => {
const finalKeybind = keybind.value.filter(k => k);
if (finalKeybind.length > 0) {
await $settings.saveSetting("keybind", JSON.stringify(finalKeybind));
router.push("/");
} else {
showEmptyKeybindError.value = true;
}
};
const toggleAutostart = async () => {
if (autostart.value === true) {
await enable();
} else {
await disable();
}
await $settings.saveSetting("autostart", autostart.value ? "true" : "false");
};
onMounted(async () => {
autostart.value = (await $settings.getSetting("autostart")) === "true";
const metaOrCtrlKey = $keyboard.currentOS === "macos" ? $keyboard.Key.Meta : $keyboard.Key.Control;
listeners.push(
$keyboard.listen([metaOrCtrlKey, $keyboard.Key.Enter], saveKeybind, { prevent: true, ignoreIfEditable: true })
);
listeners.push(
$keyboard.listen([$keyboard.Key.Escape], () => {
if (!isKeybindInputFocused.value && !blurredByEscape.value) {
router.push("/");
}
if(blurredByEscape.value) blurredByEscape.value = false;
}, { prevent: true })
);
});
onUnmounted(() => {
listeners.forEach(unlisten => unlisten());
listeners.length = 0;
});
</script>
<style scoped lang="scss">
@use "/styles/settings.scss";
</style>
<template>
<main>
<div class="top-bar">
<NuxtLink to="/" class="back">
<img src="../public/back_arrow.svg" />
<p>Back</p>
</NuxtLink>
</div>
<div class="settings-container">
<div class="settings">
<div class="names">
<p style="line-height: 14px">Startup</p>
<p style="line-height: 34px">Qopy Hotkey</p>
</div>
<div class="actions">
<div class="launch">
<input
type="checkbox"
id="launch"
v-model="autostart"
@change="toggleAutostart" />
<label for="launch" class="checkmark">
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g>
<rect width="14" height="14" />
<path
id="Path"
d="M0 2.00696L2.25015 4.25L6 0"
fill="none"
stroke-width="1.5"
stroke="#E5DFD5"
stroke-linecap="round"
stroke-linejoin="round"
transform="translate(4 5)" />
</g>
</svg>
</label>
<p for="launch">Launch Qopy at login</p>
</div>
<div
@blur="onBlur"
@focus="onFocus"
class="keybind-input"
ref="keybindInput"
tabindex="0"
:class="{ 'empty-keybind': showEmptyKeybindError }">
<span class="key" v-if="keybind.length === 0">Click here</span>
<template v-else>
<span
:key="index"
class="key"
:class="{ modifier: isModifier(key) }"
v-for="(key, index) in keybind">
{{ keyToLabel(key) }}
</span>
</template>
</div>
</div>
</div>
</div>
<BottomBar
:primary-action="{
text: 'Save',
icon: IconsEnter,
onClick: saveKeybind,
showModifier: true,
}" />
</main>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref, watch } from "vue";
import { platform } from "@tauri-apps/plugin-os";
import { useRouter } from "vue-router";
import { KeyValues, KeyLabels } from "../types/keys";
import { disable, enable } from "@tauri-apps/plugin-autostart";
import { useNuxtApp } from "#app";
import BottomBar from "../components/BottomBar.vue";
import IconsEnter from "~/components/Keys/Enter.vue";
const activeModifiers = reactive<Set<KeyValues>>(new Set());
const isKeybindInputFocused = ref(false);
const keybind = ref<KeyValues[]>([]);
const keybindInput = ref<HTMLElement | null>(null);
const blurredByEscape = ref(false);
const router = useRouter();
const showEmptyKeybindError = ref(false);
const autostart = ref(false);
const { $settings, $keyboard } = useNuxtApp();
const listeners: Array<() => void> = [];
const modifierKeySet = new Set([
KeyValues.AltLeft,
KeyValues.AltRight,
KeyValues.ControlLeft,
KeyValues.ControlRight,
KeyValues.MetaLeft,
KeyValues.MetaRight,
KeyValues.ShiftLeft,
KeyValues.ShiftRight,
]);
const isModifier = (key: KeyValues): boolean => {
return modifierKeySet.has(key);
};
const keyToLabel = (key: KeyValues): string => {
return KeyLabels[key] || key;
};
const updateKeybindDisplay = () => {
const modifiers = Array.from(activeModifiers);
const nonModifiers = keybind.value.filter((key) => !isModifier(key));
const sortedModifiers = modifiers.sort();
keybind.value = [...sortedModifiers, ...nonModifiers];
};
const onBlur = () => {
isKeybindInputFocused.value = false;
showEmptyKeybindError.value = false;
};
const onFocus = () => {
isKeybindInputFocused.value = true;
blurredByEscape.value = false;
activeModifiers.clear();
keybind.value = [];
showEmptyKeybindError.value = false;
const unlistenAll = $keyboard.listen([$keyboard.Key.All], (event: KeyboardEvent) => {
event.preventDefault();
event.stopPropagation();
const key = event.code as KeyValues;
if (key === KeyValues.Escape) {
blurredByEscape.value = true;
keybindInput.value?.blur();
return;
}
if (isModifier(key)) {
activeModifiers.add(key);
} else {
const nonModifierKey = keybind.value.find(k => !isModifier(k));
if (!nonModifierKey || nonModifierKey === key) {
keybind.value = Array.from(activeModifiers);
if (nonModifierKey !== key) keybind.value.push(key);
} else {
keybind.value = [ ...Array.from(activeModifiers), key];
}
}
updateKeybindDisplay();
showEmptyKeybindError.value = false;
}, { prevent: true });
listeners.push(unlistenAll);
};
const saveKeybind = async () => {
const finalKeybind = keybind.value.filter(k => k);
if (finalKeybind.length > 0) {
await $settings.saveSetting("keybind", JSON.stringify(finalKeybind));
router.push("/");
} else {
showEmptyKeybindError.value = true;
}
};
const toggleAutostart = async () => {
if (autostart.value === true) {
await enable();
} else {
await disable();
}
await $settings.saveSetting("autostart", autostart.value ? "true" : "false");
};
onMounted(async () => {
autostart.value = (await $settings.getSetting("autostart")) === "true";
const metaOrCtrlKey = $keyboard.currentOS === "macos" ? $keyboard.Key.Meta : $keyboard.Key.Control;
listeners.push(
$keyboard.listen([metaOrCtrlKey, $keyboard.Key.Enter], saveKeybind, { prevent: true, ignoreIfEditable: true })
);
listeners.push(
$keyboard.listen([$keyboard.Key.Escape], () => {
if (!isKeybindInputFocused.value && !blurredByEscape.value) {
router.push("/");
}
if(blurredByEscape.value) blurredByEscape.value = false;
}, { prevent: true })
);
});
onUnmounted(() => {
listeners.forEach(unlisten => unlisten());
listeners.length = 0;
});
</script>
<style scoped lang="scss">
@use "/styles/settings.scss";
</style>

View file

@ -1,61 +1,61 @@
import { invoke } from "@tauri-apps/api/core";
import type { HistoryItem } from "~/types/types";
export default defineNuxtPlugin(() => {
return {
provide: {
history: {
async getHistory(): Promise<HistoryItem[]> {
return await invoke<HistoryItem[]>("get_history");
},
async addHistoryItem(item: HistoryItem): Promise<void> {
await invoke<void>("add_history_item", { item });
},
async searchHistory(query: string): Promise<HistoryItem[]> {
try {
return await invoke<HistoryItem[]>("search_history", { query });
} catch (error) {
console.error("Error searching history:", error);
return [];
}
},
async loadHistoryChunk(
offset: number,
limit: number
): Promise<HistoryItem[]> {
try {
return await invoke<HistoryItem[]>("load_history_chunk", {
offset,
limit,
});
} catch (error) {
console.error("Error loading history chunk:", error);
return [];
}
},
async deleteHistoryItem(id: string): Promise<void> {
await invoke<void>("delete_history_item", { id });
},
async clearHistory(): Promise<void> {
await invoke<void>("clear_history");
},
async writeAndPaste(data: {
content: string;
contentType: string;
}): Promise<void> {
await invoke<void>("write_and_paste", data);
},
async readImage(data: { filename: string }): Promise<string> {
return await invoke<string>("read_image", data);
},
},
},
};
});
import { invoke } from "@tauri-apps/api/core";
import type { HistoryItem } from "~/types/types";
export default defineNuxtPlugin(() => {
return {
provide: {
history: {
async getHistory(): Promise<HistoryItem[]> {
return await invoke<HistoryItem[]>("get_history");
},
async addHistoryItem(item: HistoryItem): Promise<void> {
await invoke<void>("add_history_item", { item });
},
async searchHistory(query: string): Promise<HistoryItem[]> {
try {
return await invoke<HistoryItem[]>("search_history", { query });
} catch (error) {
console.error("Error searching history:", error);
return [];
}
},
async loadHistoryChunk(
offset: number,
limit: number
): Promise<HistoryItem[]> {
try {
return await invoke<HistoryItem[]>("load_history_chunk", {
offset,
limit,
});
} catch (error) {
console.error("Error loading history chunk:", error);
return [];
}
},
async deleteHistoryItem(id: string): Promise<void> {
await invoke<void>("delete_history_item", { id });
},
async clearHistory(): Promise<void> {
await invoke<void>("clear_history");
},
async writeAndPaste(data: {
content: string;
contentType: string;
}): Promise<void> {
await invoke<void>("write_and_paste", data);
},
async readImage(data: { filename: string }): Promise<string> {
return await invoke<string>("read_image", data);
},
},
},
};
});

View file

@ -1,27 +1,27 @@
import { platform } from "@tauri-apps/plugin-os";
import { useKeyboard, Key } from "@waradu/keyboard";
export default defineNuxtPlugin(async (nuxtApp) => {
const keyboardInstance = useKeyboard();
let currentOS = "windows";
try {
const osName = await Promise.resolve(platform());
currentOS = osName.toLowerCase().includes("mac") ? "macos" : "windows";
} catch (error) {
console.error("Error detecting platform:", error);
}
// Defer initialization until the app is mounted
nuxtApp.hook('app:mounted', () => {
keyboardInstance.init();
});
nuxtApp.provide('keyboard', {
listen: keyboardInstance.listen.bind(keyboardInstance),
init: keyboardInstance.init.bind(keyboardInstance),
Key,
currentOS,
// Provide a clear method if users need to manually clear all listeners from the instance
clearAll: keyboardInstance.clear ? keyboardInstance.clear.bind(keyboardInstance) : () => { console.warn('@waradu/keyboard instance does not have a clear method'); }
});
});
import { platform } from "@tauri-apps/plugin-os";
import { useKeyboard, Key } from "@waradu/keyboard";
export default defineNuxtPlugin(async (nuxtApp) => {
const keyboardInstance = useKeyboard();
let currentOS = "windows";
try {
const osName = await Promise.resolve(platform());
currentOS = osName.toLowerCase().includes("mac") ? "macos" : "windows";
} catch (error) {
console.error("Error detecting platform:", error);
}
// Defer initialization until the app is mounted
nuxtApp.hook('app:mounted', () => {
keyboardInstance.init();
});
nuxtApp.provide('keyboard', {
listen: keyboardInstance.listen.bind(keyboardInstance),
init: keyboardInstance.init.bind(keyboardInstance),
Key,
currentOS,
// Provide a clear method if users need to manually clear all listeners from the instance
clearAll: keyboardInstance.clear ? keyboardInstance.clear.bind(keyboardInstance) : () => { console.warn('@waradu/keyboard instance does not have a clear method'); }
});
});

View file

@ -1,68 +1,68 @@
import { ref, computed } from 'vue';
import type { HistoryItem } from '~/types/types';
interface GroupedHistory {
label: string;
items: HistoryItem[];
}
const selectedGroupIndex = ref(0);
const selectedItemIndex = ref(0);
const selectedElement = ref<HTMLElement | null>(null);
const useSelectedResult = (groupedHistory: Ref<GroupedHistory[]>) => {
const selectedItem = computed<HistoryItem | undefined>(() => {
const group = groupedHistory.value[selectedGroupIndex.value];
return group?.items[selectedItemIndex.value] ?? undefined;
});
const isSelected = (groupIndex: number, itemIndex: number): boolean => {
return selectedGroupIndex.value === groupIndex && selectedItemIndex.value === itemIndex;
};
const selectNext = (): void => {
const currentGroup = groupedHistory.value[selectedGroupIndex.value];
if (selectedItemIndex.value < currentGroup.items.length - 1) {
selectedItemIndex.value++;
} else if (selectedGroupIndex.value < groupedHistory.value.length - 1) {
selectedGroupIndex.value++;
selectedItemIndex.value = 0;
}
};
const selectPrevious = (): void => {
if (selectedItemIndex.value > 0) {
selectedItemIndex.value--;
} else if (selectedGroupIndex.value > 0) {
selectedGroupIndex.value--;
selectedItemIndex.value = groupedHistory.value[selectedGroupIndex.value].items.length - 1;
}
};
const selectItem = (groupIndex: number, itemIndex: number): void => {
selectedGroupIndex.value = groupIndex;
selectedItemIndex.value = itemIndex;
};
return {
selectedItem,
isSelected,
selectNext,
selectPrevious,
selectItem,
selectedElement
};
};
export default defineNuxtPlugin(() => {
return {
provide: {
selectedResult: {
selectedGroupIndex,
selectedItemIndex,
selectedElement,
useSelectedResult
}
}
};
import { ref, computed } from 'vue';
import type { HistoryItem } from '~/types/types';
interface GroupedHistory {
label: string;
items: HistoryItem[];
}
const selectedGroupIndex = ref(0);
const selectedItemIndex = ref(0);
const selectedElement = ref<HTMLElement | null>(null);
const useSelectedResult = (groupedHistory: Ref<GroupedHistory[]>) => {
const selectedItem = computed<HistoryItem | undefined>(() => {
const group = groupedHistory.value[selectedGroupIndex.value];
return group?.items[selectedItemIndex.value] ?? undefined;
});
const isSelected = (groupIndex: number, itemIndex: number): boolean => {
return selectedGroupIndex.value === groupIndex && selectedItemIndex.value === itemIndex;
};
const selectNext = (): void => {
const currentGroup = groupedHistory.value[selectedGroupIndex.value];
if (selectedItemIndex.value < currentGroup.items.length - 1) {
selectedItemIndex.value++;
} else if (selectedGroupIndex.value < groupedHistory.value.length - 1) {
selectedGroupIndex.value++;
selectedItemIndex.value = 0;
}
};
const selectPrevious = (): void => {
if (selectedItemIndex.value > 0) {
selectedItemIndex.value--;
} else if (selectedGroupIndex.value > 0) {
selectedGroupIndex.value--;
selectedItemIndex.value = groupedHistory.value[selectedGroupIndex.value].items.length - 1;
}
};
const selectItem = (groupIndex: number, itemIndex: number): void => {
selectedGroupIndex.value = groupIndex;
selectedItemIndex.value = itemIndex;
};
return {
selectedItem,
isSelected,
selectNext,
selectPrevious,
selectItem,
selectedElement
};
};
export default defineNuxtPlugin(() => {
return {
provide: {
selectedResult: {
selectedGroupIndex,
selectedItemIndex,
selectedElement,
useSelectedResult
}
}
};
});

View file

@ -1,17 +1,17 @@
import { invoke } from "@tauri-apps/api/core";
export default defineNuxtPlugin(() => {
return {
provide: {
settings: {
async getSetting(key: string): Promise<string> {
return await invoke<string>("get_setting", { key });
},
async saveSetting(key: string, value: string): Promise<void> {
await invoke<void>("save_setting", { key, value });
},
},
},
};
});
import { invoke } from "@tauri-apps/api/core";
export default defineNuxtPlugin(() => {
return {
provide: {
settings: {
async getSetting(key: string): Promise<string> {
return await invoke<string>("get_setting", { key });
},
async saveSetting(key: string, value: string): Promise<void> {
await invoke<void>("save_setting", { key, value });
},
},
},
};
});

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="12" height="8" viewBox="0 0 12 8" fill="none" stroke="none" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
<g id="Arrow" transform="translate(0 0)">
<rect id="Rectangle" width="12" height="8" style="mix-blend-mode:normal;" transform="translate(0 0)" />
<path id="Path" d="M3.10928 8.76192C3.10928 8.76192 3.10928 0.952386 3.10928 0.952386C3.10928 0.682544 3.1892 0.456512 3.34903 0.274291C3.50887 0.0920672 3.70643 0.000639439 3.94173 3.33786e-06C4.17706 -0.000631809 4.37489 0.0907969 4.53526 0.274291C4.69566 0.457782 4.77529 0.683815 4.77418 0.952386C4.77418 0.952386 4.77418 8.76192 4.77418 8.76192C4.77418 8.76192 6.60566 7 6.60566 7C6.75827 6.8254 6.95253 6.73811 7.18839 6.73811C7.42426 6.73811 7.61849 6.8254 7.77111 7C7.92373 7.1746 8 7.39683 8 7.66667C8 7.93651 7.92373 8.15874 7.77111 8.33334C7.77111 8.33334 4.52444 11.7143 4.52444 11.7143C4.35797 11.9048 4.16372 12 3.94173 12C3.71975 12 3.52551 11.9048 3.35902 11.7143C3.35902 11.7143 0.228926 8.33334 0.228926 8.33334C0.076309 8.15874 0 7.93652 0 7.66668C0 7.39683 0.076309 7.1746 0.228926 7C0.381543 6.8254 0.575782 6.73811 0.811648 6.73811C1.04751 6.73811 1.24175 6.8254 1.39437 7C1.39437 7 3.10928 8.76192 3.10928 8.76192Z" style="fill:#ADA9A1;fill-rule:evenodd;mix-blend-mode:normal;" transform="matrix(0 1 -1 0 12 0)" />
</g>
<?xml version="1.0" encoding="utf-8"?>
<svg width="12" height="8" viewBox="0 0 12 8" fill="none" stroke="none" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
<g id="Arrow" transform="translate(0 0)">
<rect id="Rectangle" width="12" height="8" style="mix-blend-mode:normal;" transform="translate(0 0)" />
<path id="Path" d="M3.10928 8.76192C3.10928 8.76192 3.10928 0.952386 3.10928 0.952386C3.10928 0.682544 3.1892 0.456512 3.34903 0.274291C3.50887 0.0920672 3.70643 0.000639439 3.94173 3.33786e-06C4.17706 -0.000631809 4.37489 0.0907969 4.53526 0.274291C4.69566 0.457782 4.77529 0.683815 4.77418 0.952386C4.77418 0.952386 4.77418 8.76192 4.77418 8.76192C4.77418 8.76192 6.60566 7 6.60566 7C6.75827 6.8254 6.95253 6.73811 7.18839 6.73811C7.42426 6.73811 7.61849 6.8254 7.77111 7C7.92373 7.1746 8 7.39683 8 7.66667C8 7.93651 7.92373 8.15874 7.77111 8.33334C7.77111 8.33334 4.52444 11.7143 4.52444 11.7143C4.35797 11.9048 4.16372 12 3.94173 12C3.71975 12 3.52551 11.9048 3.35902 11.7143C3.35902 11.7143 0.228926 8.33334 0.228926 8.33334C0.076309 8.15874 0 7.93652 0 7.66668C0 7.39683 0.076309 7.1746 0.228926 7C0.381543 6.8254 0.575782 6.73811 0.811648 6.73811C1.04751 6.73811 1.24175 6.8254 1.39437 7C1.39437 7 3.10928 8.76192 3.10928 8.76192Z" style="fill:#ADA9A1;fill-rule:evenodd;mix-blend-mode:normal;" transform="matrix(0 1 -1 0 12 0)" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before After
Before After

View file

@ -1,4 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/gen/schemas
# Generated by Cargo
# will have compiled files and executables
/target/
/gen/schemas

2469
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -7,52 +7,52 @@ edition = "2021"
rust-version = "1.70"
[build-dependencies]
tauri-build = { version = "2.0.6", features = [] }
tauri-build = { version = "2.2.0", features = [] }
[dependencies]
tauri = { version = "2.3.1", features = [
tauri = { version = "2.5.1", features = [
"macos-private-api",
"tray-icon",
"image-png",
] }
tauri-plugin-sql = { version = "2.2.0", features = ["sqlite"] }
tauri-plugin-autostart = "2.2.0"
tauri-plugin-autostart = "2.3.0"
tauri-plugin-os = "2.2.1"
tauri-plugin-updater = "2.6.0"
tauri-plugin-dialog = "2.2.0"
tauri-plugin-fs = "2.2.0"
tauri-plugin-updater = "2.7.1"
tauri-plugin-dialog = "2.2.2"
tauri-plugin-fs = "2.3.0"
tauri-plugin-clipboard = "2.1.11"
tauri-plugin-prevent-default = "1.2.1"
tauri-plugin-global-shortcut = "2.2.0"
tauri-plugin-aptabase = { git = "https://github.com/aptabase/tauri-plugin-aptabase", branch = "v2" }
sqlx = { version = "0.8.3", features = [
tauri-plugin-prevent-default = "2.1.1"
tauri-plugin-global-shortcut = "2.2.1"
tauri-plugin-aptabase = "1.0.0"
sqlx = { version = "0.8.6", features = [
"runtime-tokio-native-tls",
"sqlite",
"chrono",
] }
serde = { version = "1.0.219", features = ["derive"] }
tokio = { version = "1.44.1", features = ["full"] }
tokio = { version = "1.45.1", features = ["full"] }
serde_json = "1.0.140"
rdev = "0.5.3"
rand = "0.9.0"
rand = "0.9.1"
base64 = "0.22.1"
image = "0.25.5"
reqwest = { version = "0.12.14", features = ["json", "blocking"] }
image = "0.25.6"
reqwest = { version = "0.12.19", features = ["json", "blocking"] }
url = "2.5.4"
regex = "1.11.1"
sha2 = "0.10.8"
sha2 = "0.10.9"
lazy_static = "1.5.0"
time = "0.3.39"
global-hotkey = "0.6.4"
chrono = { version = "0.4.40", features = ["serde"] }
log = { version = "0.4.26", features = ["std"] }
uuid = "1.16.0"
time = "0.3.41"
global-hotkey = "0.7.0"
chrono = { version = "0.4.41", features = ["serde"] }
log = { version = "0.4.27", features = ["std"] }
uuid = { version = "1.17.0", features = ["v4"]}
include_dir = "0.7.4"
# hyperpolyglot = { git = "https://github.com/0pandadev/hyperpolyglot" }
applications = { git = "https://github.com/HuakunShen/applications-rs", branch = "fix/win-app-detection" }
glob = "0.3.2"
meta_fetcher = "0.1.1"
parking_lot = "0.12.3"
parking_lot = "0.12.4"
[features]
custom-protocol = ["tauri/custom-protocol"]

View file

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

View file

@ -1,34 +1,34 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
"windows": [
"main"
],
"permissions": [
"core:path:default",
"core:event:default",
"core:window:default",
"core:webview:default",
"core:app:default",
"core:resources:default",
"core:image:default",
"core:menu:default",
"core:tray:default",
"sql:allow-load",
"sql:allow-select",
"sql:allow-execute",
"autostart:allow-enable",
"autostart:allow-disable",
"autostart:allow-is-enabled",
"os:allow-os-type",
"core:app:allow-app-hide",
"core:app:allow-app-show",
"core:window:allow-hide",
"core:window:allow-show",
"core:window:allow-set-focus",
"core:window:allow-is-focused",
"core:window:allow-is-visible",
"fs:allow-read"
]
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
"windows": [
"main"
],
"permissions": [
"core:path:default",
"core:event:default",
"core:window:default",
"core:webview:default",
"core:app:default",
"core:resources:default",
"core:image:default",
"core:menu:default",
"core:tray:default",
"sql:allow-load",
"sql:allow-select",
"sql:allow-execute",
"autostart:allow-enable",
"autostart:allow-disable",
"autostart:allow-is-enabled",
"os:allow-os-type",
"core:app:allow-app-hide",
"core:app:allow-app-show",
"core:window:allow-hide",
"core:window:allow-show",
"core:window:allow-set-focus",
"core:window:allow-is-focused",
"core:window:allow-is-visible",
"fs:allow-read"
]
}

View file

@ -1,279 +1,279 @@
use tauri_plugin_aptabase::EventTracker;
use base64::{ engine::general_purpose::STANDARD, Engine };
// use hyperpolyglot;
use lazy_static::lazy_static;
use rdev::{ simulate, EventType, Key };
use regex::Regex;
use sqlx::SqlitePool;
use std::fs;
use std::sync::atomic::{ AtomicBool, Ordering };
use std::{ thread, time::Duration };
use tauri::{ AppHandle, Emitter, Listener, Manager };
use tauri_plugin_clipboard::Clipboard;
use tokio::runtime::Runtime as TokioRuntime;
use url::Url;
use uuid::Uuid;
use crate::db;
use crate::utils::commands::get_app_info;
use crate::utils::favicon::fetch_favicon_as_base64;
use crate::utils::types::{ ContentType, HistoryItem };
lazy_static! {
static ref IS_PROGRAMMATIC_PASTE: AtomicBool = AtomicBool::new(false);
}
#[tauri::command]
pub async fn write_and_paste(
app_handle: AppHandle,
content: String,
content_type: String
) -> Result<(), String> {
let clipboard = app_handle.state::<Clipboard>();
match content_type.as_str() {
"text" => clipboard.write_text(content).map_err(|e| e.to_string())?,
"link" => clipboard.write_text(content).map_err(|e| e.to_string())?,
"color" => clipboard.write_text(content).map_err(|e| e.to_string())?,
"image" => {
clipboard.write_image_base64(content).map_err(|e| e.to_string())?;
}
"files" => {
clipboard
.write_files_uris(
content
.split(", ")
.map(|file| file.to_string())
.collect::<Vec<String>>()
)
.map_err(|e| e.to_string())?;
}
_ => {
return Err("Unsupported content type".to_string());
}
}
IS_PROGRAMMATIC_PASTE.store(true, Ordering::SeqCst);
thread::spawn(|| {
thread::sleep(Duration::from_millis(100));
#[cfg(target_os = "macos")]
let modifier_key = Key::MetaLeft;
#[cfg(not(target_os = "macos"))]
let modifier_key = Key::ControlLeft;
let events = vec![
EventType::KeyPress(modifier_key),
EventType::KeyPress(Key::KeyV),
EventType::KeyRelease(Key::KeyV),
EventType::KeyRelease(modifier_key)
];
for event in events {
if let Err(e) = simulate(&event) {
println!("Simulation error: {:?}", e);
}
thread::sleep(Duration::from_millis(20));
}
});
tokio::spawn(async {
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
IS_PROGRAMMATIC_PASTE.store(false, Ordering::SeqCst);
});
let _ = app_handle.track_event(
"clipboard_paste",
Some(serde_json::json!({
"content_type": content_type
}))
);
Ok(())
}
pub fn setup(app: &AppHandle) {
let app_handle = app.clone();
let runtime = TokioRuntime::new().expect("Failed to create Tokio runtime");
app_handle.clone().listen("plugin:clipboard://clipboard-monitor/update", move |_event| {
let app_handle = app_handle.clone();
runtime.block_on(async move {
if IS_PROGRAMMATIC_PASTE.load(Ordering::SeqCst) {
return;
}
let clipboard = app_handle.state::<Clipboard>();
let available_types = clipboard.available_types().unwrap();
let (app_name, app_icon) = get_app_info();
match get_pool(&app_handle).await {
Ok(pool) => {
if available_types.image {
println!("Handling image change");
if let Ok(image_data) = clipboard.read_image_base64() {
let file_path = save_image_to_file(&app_handle, &image_data).await
.map_err(|e| e.to_string())
.unwrap_or_else(|e| e);
let _ = db::history::add_history_item(
app_handle.clone(),
pool,
HistoryItem::new(
app_name,
ContentType::Image,
file_path,
None,
app_icon,
None
)
).await;
}
} else if available_types.files {
println!("Handling files change");
if let Ok(files) = clipboard.read_files() {
for file in files {
let _ = db::history::add_history_item(
app_handle.clone(),
pool.clone(),
HistoryItem::new(
app_name.clone(),
ContentType::File,
file,
None,
app_icon.clone(),
None
)
).await;
}
}
} else if available_types.text {
println!("Handling text change");
if let Ok(text) = clipboard.read_text() {
let text = text.to_string();
let url_regex = Regex::new(
r"^https?://(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*)$"
).unwrap();
if url_regex.is_match(&text) {
if let Ok(url) = Url::parse(&text) {
let favicon = match fetch_favicon_as_base64(url).await {
Ok(Some(f)) => Some(f),
_ => None,
};
let _ = db::history::add_history_item(
app_handle.clone(),
pool,
HistoryItem::new(
app_name,
ContentType::Link,
text,
favicon,
app_icon,
None
)
).await;
}
} else {
if text.is_empty() {
return;
}
// Temporarily disabled code detection
/*if let Some(detection) = hyperpolyglot::detect_from_text(&text) {
let language = match detection {
hyperpolyglot::Detection::Heuristics(lang) => lang.to_string(),
_ => detection.language().to_string(),
};
let _ = db::history::add_history_item(
pool,
HistoryItem::new(app_name, ContentType::Code, text, None, app_icon, Some(language))
).await;
} else*/ if crate::utils::commands::detect_color(&text) {
let _ = db::history::add_history_item(
app_handle.clone(),
pool,
HistoryItem::new(
app_name,
ContentType::Color,
text,
None,
app_icon,
None
)
).await;
} else {
let _ = db::history::add_history_item(
app_handle.clone(),
pool,
HistoryItem::new(
app_name,
ContentType::Text,
text.clone(),
None,
app_icon,
None
)
).await;
}
}
}
} else {
println!("Unknown clipboard content type");
}
}
Err(e) => {
println!("Failed to get database pool: {}", e);
}
}
let _ = app_handle.track_event(
"clipboard_copied",
Some(
serde_json::json!({
"content_type": if available_types.image { "image" }
else if available_types.files { "files" }
else if available_types.text { "text" }
else { "unknown" }
})
)
);
});
});
}
async fn get_pool(
app_handle: &AppHandle
) -> Result<tauri::State<'_, SqlitePool>, Box<dyn std::error::Error + Send + Sync>> {
Ok(app_handle.state::<SqlitePool>())
}
#[tauri::command]
pub fn start_monitor(app_handle: AppHandle) -> Result<(), String> {
let clipboard = app_handle.state::<Clipboard>();
clipboard.start_monitor(app_handle.clone()).map_err(|e| e.to_string())?;
app_handle
.emit("plugin:clipboard://clipboard-monitor/status", true)
.map_err(|e| e.to_string())?;
Ok(())
}
async fn save_image_to_file(
app_handle: &AppHandle,
base64_data: &str
) -> Result<String, Box<dyn std::error::Error>> {
let app_data_dir = app_handle.path().app_data_dir().unwrap();
let images_dir = app_data_dir.join("images");
fs::create_dir_all(&images_dir)?;
let file_name = format!("{}.png", Uuid::new_v4());
let file_path = images_dir.join(&file_name);
let bytes = STANDARD.decode(base64_data)?;
fs::write(&file_path, bytes)?;
Ok(file_path.to_string_lossy().into_owned())
}
use tauri_plugin_aptabase::EventTracker;
use base64::{ engine::general_purpose::STANDARD, Engine };
// use hyperpolyglot;
use lazy_static::lazy_static;
use rdev::{ simulate, EventType, Key };
use regex::Regex;
use sqlx::SqlitePool;
use std::fs;
use std::sync::atomic::{ AtomicBool, Ordering };
use std::{ thread, time::Duration };
use tauri::{ AppHandle, Emitter, Listener, Manager };
use tauri_plugin_clipboard::Clipboard;
use tokio::runtime::Runtime as TokioRuntime;
use url::Url;
use uuid::Uuid;
use crate::db;
use crate::utils::commands::get_app_info;
use crate::utils::favicon::fetch_favicon_as_base64;
use crate::utils::types::{ ContentType, HistoryItem };
lazy_static! {
static ref IS_PROGRAMMATIC_PASTE: AtomicBool = AtomicBool::new(false);
}
#[tauri::command]
pub async fn write_and_paste(
app_handle: AppHandle,
content: String,
content_type: String
) -> Result<(), String> {
let clipboard = app_handle.state::<Clipboard>();
match content_type.as_str() {
"text" => clipboard.write_text(content).map_err(|e| e.to_string())?,
"link" => clipboard.write_text(content).map_err(|e| e.to_string())?,
"color" => clipboard.write_text(content).map_err(|e| e.to_string())?,
"image" => {
clipboard.write_image_base64(content).map_err(|e| e.to_string())?;
}
"files" => {
clipboard
.write_files_uris(
content
.split(", ")
.map(|file| file.to_string())
.collect::<Vec<String>>()
)
.map_err(|e| e.to_string())?;
}
_ => {
return Err("Unsupported content type".to_string());
}
}
IS_PROGRAMMATIC_PASTE.store(true, Ordering::SeqCst);
thread::spawn(|| {
thread::sleep(Duration::from_millis(100));
#[cfg(target_os = "macos")]
let modifier_key = Key::MetaLeft;
#[cfg(not(target_os = "macos"))]
let modifier_key = Key::ControlLeft;
let events = vec![
EventType::KeyPress(modifier_key),
EventType::KeyPress(Key::KeyV),
EventType::KeyRelease(Key::KeyV),
EventType::KeyRelease(modifier_key)
];
for event in events {
if let Err(e) = simulate(&event) {
println!("Simulation error: {:?}", e);
}
thread::sleep(Duration::from_millis(20));
}
});
tokio::spawn(async {
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
IS_PROGRAMMATIC_PASTE.store(false, Ordering::SeqCst);
});
let _ = app_handle.track_event(
"clipboard_paste",
Some(serde_json::json!({
"content_type": content_type
}))
);
Ok(())
}
pub fn setup(app: &AppHandle) {
let app_handle = app.clone();
let runtime = TokioRuntime::new().expect("Failed to create Tokio runtime");
app_handle.clone().listen("plugin:clipboard://clipboard-monitor/update", move |_event| {
let app_handle = app_handle.clone();
runtime.block_on(async move {
if IS_PROGRAMMATIC_PASTE.load(Ordering::SeqCst) {
return;
}
let clipboard = app_handle.state::<Clipboard>();
let available_types = clipboard.available_types().unwrap();
let (app_name, app_icon) = get_app_info();
match get_pool(&app_handle).await {
Ok(pool) => {
if available_types.image {
println!("Handling image change");
if let Ok(image_data) = clipboard.read_image_base64() {
let file_path = save_image_to_file(&app_handle, &image_data).await
.map_err(|e| e.to_string())
.unwrap_or_else(|e| e);
let _ = db::history::add_history_item(
app_handle.clone(),
pool,
HistoryItem::new(
app_name,
ContentType::Image,
file_path,
None,
app_icon,
None
)
).await;
}
} else if available_types.files {
println!("Handling files change");
if let Ok(files) = clipboard.read_files() {
for file in files {
let _ = db::history::add_history_item(
app_handle.clone(),
pool.clone(),
HistoryItem::new(
app_name.clone(),
ContentType::File,
file,
None,
app_icon.clone(),
None
)
).await;
}
}
} else if available_types.text {
println!("Handling text change");
if let Ok(text) = clipboard.read_text() {
let text = text.to_string();
let url_regex = Regex::new(
r"^https?://(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*)$"
).unwrap();
if url_regex.is_match(&text) {
if let Ok(url) = Url::parse(&text) {
let favicon = match fetch_favicon_as_base64(url).await {
Ok(Some(f)) => Some(f),
_ => None,
};
let _ = db::history::add_history_item(
app_handle.clone(),
pool,
HistoryItem::new(
app_name,
ContentType::Link,
text,
favicon,
app_icon,
None
)
).await;
}
} else {
if text.is_empty() {
return;
}
// Temporarily disabled code detection
/*if let Some(detection) = hyperpolyglot::detect_from_text(&text) {
let language = match detection {
hyperpolyglot::Detection::Heuristics(lang) => lang.to_string(),
_ => detection.language().to_string(),
};
let _ = db::history::add_history_item(
pool,
HistoryItem::new(app_name, ContentType::Code, text, None, app_icon, Some(language))
).await;
} else*/ if crate::utils::commands::detect_color(&text) {
let _ = db::history::add_history_item(
app_handle.clone(),
pool,
HistoryItem::new(
app_name,
ContentType::Color,
text,
None,
app_icon,
None
)
).await;
} else {
let _ = db::history::add_history_item(
app_handle.clone(),
pool,
HistoryItem::new(
app_name,
ContentType::Text,
text.clone(),
None,
app_icon,
None
)
).await;
}
}
}
} else {
println!("Unknown clipboard content type");
}
}
Err(e) => {
println!("Failed to get database pool: {}", e);
}
}
let _ = app_handle.track_event(
"clipboard_copied",
Some(
serde_json::json!({
"content_type": if available_types.image { "image" }
else if available_types.files { "files" }
else if available_types.text { "text" }
else { "unknown" }
})
)
);
});
});
}
async fn get_pool(
app_handle: &AppHandle
) -> Result<tauri::State<'_, SqlitePool>, Box<dyn std::error::Error + Send + Sync>> {
Ok(app_handle.state::<SqlitePool>())
}
#[tauri::command]
pub fn start_monitor(app_handle: AppHandle) -> Result<(), String> {
let clipboard = app_handle.state::<Clipboard>();
clipboard.start_monitor(app_handle.clone()).map_err(|e| e.to_string())?;
app_handle
.emit("plugin:clipboard://clipboard-monitor/status", true)
.map_err(|e| e.to_string())?;
Ok(())
}
async fn save_image_to_file(
app_handle: &AppHandle,
base64_data: &str
) -> Result<String, Box<dyn std::error::Error>> {
let app_data_dir = app_handle.path().app_data_dir().unwrap();
let images_dir = app_data_dir.join("images");
fs::create_dir_all(&images_dir)?;
let file_name = format!("{}.png", Uuid::new_v4());
let file_path = images_dir.join(&file_name);
let bytes = STANDARD.decode(base64_data)?;
fs::write(&file_path, bytes)?;
Ok(file_path.to_string_lossy().into_owned())
}

View file

@ -1,155 +1,155 @@
use crate::utils::commands::center_window_on_current_monitor;
use crate::utils::keys::KeyCode;
use global_hotkey::{
hotkey::{ Code, HotKey, Modifiers },
GlobalHotKeyEvent,
GlobalHotKeyManager,
HotKeyState,
};
use parking_lot::Mutex;
use std::str::FromStr;
use std::sync::Arc;
use tauri::{ AppHandle, Manager, Listener };
use tauri_plugin_aptabase::EventTracker;
#[derive(Default)]
struct HotkeyState {
manager: Option<GlobalHotKeyManager>,
registered_hotkey: Option<HotKey>,
}
unsafe impl Send for HotkeyState {}
pub fn setup(app_handle: tauri::AppHandle) {
let state = Arc::new(Mutex::new(HotkeyState::default()));
let manager = match GlobalHotKeyManager::new() {
Ok(manager) => manager,
Err(err) => {
eprintln!("Failed to initialize hotkey manager: {:?}", err);
return;
}
};
{
let mut hotkey_state = state.lock();
hotkey_state.manager = Some(manager);
}
let rt = app_handle.state::<tokio::runtime::Runtime>();
let initial_keybind = rt
.block_on(crate::db::settings::get_keybind(app_handle.clone()))
.expect("Failed to get initial keybind");
if let Err(e) = register_shortcut(&state, &initial_keybind) {
eprintln!("Error registering initial shortcut: {:?}", e);
}
let state_clone = Arc::clone(&state);
app_handle.listen("update-shortcut", move |event| {
let payload_str = event.payload().replace("\\\"", "\"");
let trimmed_str = payload_str.trim_matches('"');
unregister_current_hotkey(&state_clone);
let payload: Vec<String> = serde_json::from_str(trimmed_str).unwrap_or_default();
if let Err(e) = register_shortcut(&state_clone, &payload) {
eprintln!("Error re-registering shortcut: {:?}", e);
}
});
let state_clone = Arc::clone(&state);
app_handle.listen("save_keybind", move |event| {
let payload_str = event.payload().to_string();
unregister_current_hotkey(&state_clone);
let payload: Vec<String> = serde_json::from_str(&payload_str).unwrap_or_default();
if let Err(e) = register_shortcut(&state_clone, &payload) {
eprintln!("Error registering saved shortcut: {:?}", e);
}
});
setup_hotkey_receiver(app_handle);
}
fn setup_hotkey_receiver(app_handle: AppHandle) {
std::thread::spawn(move || {
loop {
match GlobalHotKeyEvent::receiver().recv() {
Ok(event) => {
if event.state == HotKeyState::Released {
continue;
}
handle_hotkey_event(&app_handle);
}
Err(e) => eprintln!("Error receiving hotkey event: {:?}", e),
}
}
});
}
fn unregister_current_hotkey(state: &Arc<Mutex<HotkeyState>>) {
let mut hotkey_state = state.lock();
if let Some(old_hotkey) = hotkey_state.registered_hotkey.take() {
if let Some(manager) = &hotkey_state.manager {
let _ = manager.unregister(old_hotkey);
}
}
}
fn register_shortcut(state: &Arc<Mutex<HotkeyState>>, shortcut: &[String]) -> Result<(), Box<dyn std::error::Error>> {
let hotkey = parse_hotkey(shortcut)?;
let mut hotkey_state = state.lock();
if let Some(manager) = &hotkey_state.manager {
manager.register(hotkey.clone())?;
hotkey_state.registered_hotkey = Some(hotkey);
Ok(())
} else {
Err("Hotkey manager not initialized".into())
}
}
fn parse_hotkey(shortcut: &[String]) -> Result<HotKey, Box<dyn std::error::Error>> {
let mut modifiers = Modifiers::empty();
let mut code = None;
for part in shortcut {
match part.as_str() {
"ControlLeft" => modifiers |= Modifiers::CONTROL,
"AltLeft" => modifiers |= Modifiers::ALT,
"ShiftLeft" => modifiers |= Modifiers::SHIFT,
"MetaLeft" => modifiers |= Modifiers::META,
key => code = Some(Code::from(KeyCode::from_str(key)?)),
}
}
let key_code = code.ok_or_else(|| "No valid key code found".to_string())?;
Ok(HotKey::new(Some(modifiers), key_code))
}
fn handle_hotkey_event(app_handle: &AppHandle) {
let window = app_handle.get_webview_window("main").unwrap();
if window.is_visible().unwrap() {
window.hide().unwrap();
} else {
window.set_always_on_top(true).unwrap();
window.show().unwrap();
window.set_focus().unwrap();
let window_clone = window.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(100));
window_clone.set_always_on_top(false).unwrap();
});
center_window_on_current_monitor(&window);
}
let _ = app_handle.track_event(
"hotkey_triggered",
Some(
serde_json::json!({
"action": if window.is_visible().unwrap() { "hide" } else { "show" }
})
)
);
use crate::utils::commands::center_window_on_current_monitor;
use crate::utils::keys::KeyCode;
use global_hotkey::{
hotkey::{ Code, HotKey, Modifiers },
GlobalHotKeyEvent,
GlobalHotKeyManager,
HotKeyState,
};
use parking_lot::Mutex;
use std::str::FromStr;
use std::sync::Arc;
use tauri::{ AppHandle, Manager, Listener };
use tauri_plugin_aptabase::EventTracker;
#[derive(Default)]
struct HotkeyState {
manager: Option<GlobalHotKeyManager>,
registered_hotkey: Option<HotKey>,
}
unsafe impl Send for HotkeyState {}
pub fn setup(app_handle: tauri::AppHandle) {
let state = Arc::new(Mutex::new(HotkeyState::default()));
let manager = match GlobalHotKeyManager::new() {
Ok(manager) => manager,
Err(err) => {
eprintln!("Failed to initialize hotkey manager: {:?}", err);
return;
}
};
{
let mut hotkey_state = state.lock();
hotkey_state.manager = Some(manager);
}
let rt = app_handle.state::<tokio::runtime::Runtime>();
let initial_keybind = rt
.block_on(crate::db::settings::get_keybind(app_handle.clone()))
.expect("Failed to get initial keybind");
if let Err(e) = register_shortcut(&state, &initial_keybind) {
eprintln!("Error registering initial shortcut: {:?}", e);
}
let state_clone = Arc::clone(&state);
app_handle.listen("update-shortcut", move |event| {
let payload_str = event.payload().replace("\\\"", "\"");
let trimmed_str = payload_str.trim_matches('"');
unregister_current_hotkey(&state_clone);
let payload: Vec<String> = serde_json::from_str(trimmed_str).unwrap_or_default();
if let Err(e) = register_shortcut(&state_clone, &payload) {
eprintln!("Error re-registering shortcut: {:?}", e);
}
});
let state_clone = Arc::clone(&state);
app_handle.listen("save_keybind", move |event| {
let payload_str = event.payload().to_string();
unregister_current_hotkey(&state_clone);
let payload: Vec<String> = serde_json::from_str(&payload_str).unwrap_or_default();
if let Err(e) = register_shortcut(&state_clone, &payload) {
eprintln!("Error registering saved shortcut: {:?}", e);
}
});
setup_hotkey_receiver(app_handle);
}
fn setup_hotkey_receiver(app_handle: AppHandle) {
std::thread::spawn(move || {
loop {
match GlobalHotKeyEvent::receiver().recv() {
Ok(event) => {
if event.state == HotKeyState::Released {
continue;
}
handle_hotkey_event(&app_handle);
}
Err(e) => eprintln!("Error receiving hotkey event: {:?}", e),
}
}
});
}
fn unregister_current_hotkey(state: &Arc<Mutex<HotkeyState>>) {
let mut hotkey_state = state.lock();
if let Some(old_hotkey) = hotkey_state.registered_hotkey.take() {
if let Some(manager) = &hotkey_state.manager {
let _ = manager.unregister(old_hotkey);
}
}
}
fn register_shortcut(state: &Arc<Mutex<HotkeyState>>, shortcut: &[String]) -> Result<(), Box<dyn std::error::Error>> {
let hotkey = parse_hotkey(shortcut)?;
let mut hotkey_state = state.lock();
if let Some(manager) = &hotkey_state.manager {
manager.register(hotkey.clone())?;
hotkey_state.registered_hotkey = Some(hotkey);
Ok(())
} else {
Err("Hotkey manager not initialized".into())
}
}
fn parse_hotkey(shortcut: &[String]) -> Result<HotKey, Box<dyn std::error::Error>> {
let mut modifiers = Modifiers::empty();
let mut code = None;
for part in shortcut {
match part.as_str() {
"ControlLeft" => modifiers |= Modifiers::CONTROL,
"AltLeft" => modifiers |= Modifiers::ALT,
"ShiftLeft" => modifiers |= Modifiers::SHIFT,
"MetaLeft" => modifiers |= Modifiers::META,
key => code = Some(Code::from(KeyCode::from_str(key)?)),
}
}
let key_code = code.ok_or_else(|| "No valid key code found".to_string())?;
Ok(HotKey::new(Some(modifiers), key_code))
}
fn handle_hotkey_event(app_handle: &AppHandle) {
let window = app_handle.get_webview_window("main").unwrap();
if window.is_visible().unwrap() {
window.hide().unwrap();
} else {
window.set_always_on_top(true).unwrap();
window.show().unwrap();
window.set_focus().unwrap();
let window_clone = window.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(100));
window_clone.set_always_on_top(false).unwrap();
});
center_window_on_current_monitor(&window);
}
let _ = app_handle.track_event(
"hotkey_triggered",
Some(
serde_json::json!({
"action": if window.is_visible().unwrap() { "hide" } else { "show" }
})
)
);
}

View file

@ -1,4 +1,4 @@
pub mod clipboard;
pub mod hotkeys;
pub mod tray;
pub mod updater;
pub mod clipboard;
pub mod hotkeys;
pub mod tray;
pub mod updater;

View file

@ -1,61 +1,61 @@
use tauri::{ menu::{ MenuBuilder, MenuItemBuilder }, tray::TrayIconBuilder, Emitter, Manager };
use tauri_plugin_aptabase::EventTracker;
pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
let window = app.get_webview_window("main").unwrap();
let is_visible = window.is_visible().unwrap();
let _ = app.track_event(
"tray_toggle",
Some(serde_json::json!({
"action": if is_visible { "hide" } else { "show" }
}))
);
let icon_bytes = include_bytes!("../../icons/Square71x71Logo.png");
let icon = tauri::image::Image::from_bytes(icon_bytes).unwrap();
let _tray = TrayIconBuilder::new()
.menu(
&MenuBuilder::new(app)
.items(&[&MenuItemBuilder::with_id("app_name", "Qopy").enabled(false).build(app)?])
.items(&[&MenuItemBuilder::with_id("show", "Show/Hide").build(app)?])
.items(&[&MenuItemBuilder::with_id("settings", "Settings").build(app)?])
.items(&[&MenuItemBuilder::with_id("quit", "Quit").build(app)?])
.build()?
)
.on_menu_event(move |_app, event| {
match event.id().as_ref() {
"quit" => {
let _ = _app.track_event("app_quit", None);
std::process::exit(0);
}
"show" => {
let _ = _app.track_event(
"tray_toggle",
Some(
serde_json::json!({
"action": if is_visible { "hide" } else { "show" }
})
)
);
let is_visible = window.is_visible().unwrap();
if is_visible {
window.hide().unwrap();
} else {
window.show().unwrap();
window.set_focus().unwrap();
}
window.emit("main_route", ()).unwrap();
}
"settings" => {
let _ = _app.track_event("tray_settings", None);
window.emit("settings", ()).unwrap();
}
_ => (),
}
})
.icon(icon)
.build(app)?;
Ok(())
}
use tauri::{ menu::{ MenuBuilder, MenuItemBuilder }, tray::TrayIconBuilder, Emitter, Manager };
use tauri_plugin_aptabase::EventTracker;
pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
let window = app.get_webview_window("main").unwrap();
let is_visible = window.is_visible().unwrap();
let _ = app.track_event(
"tray_toggle",
Some(serde_json::json!({
"action": if is_visible { "hide" } else { "show" }
}))
);
let icon_bytes = include_bytes!("../../icons/Square71x71Logo.png");
let icon = tauri::image::Image::from_bytes(icon_bytes).unwrap();
let _tray = TrayIconBuilder::new()
.menu(
&MenuBuilder::new(app)
.items(&[&MenuItemBuilder::with_id("app_name", "Qopy").enabled(false).build(app)?])
.items(&[&MenuItemBuilder::with_id("show", "Show/Hide").build(app)?])
.items(&[&MenuItemBuilder::with_id("settings", "Settings").build(app)?])
.items(&[&MenuItemBuilder::with_id("quit", "Quit").build(app)?])
.build()?
)
.on_menu_event(move |_app, event| {
match event.id().as_ref() {
"quit" => {
let _ = _app.track_event("app_quit", None);
std::process::exit(0);
}
"show" => {
let _ = _app.track_event(
"tray_toggle",
Some(
serde_json::json!({
"action": if is_visible { "hide" } else { "show" }
})
)
);
let is_visible = window.is_visible().unwrap();
if is_visible {
window.hide().unwrap();
} else {
window.show().unwrap();
window.set_focus().unwrap();
}
window.emit("main_route", ()).unwrap();
}
"settings" => {
let _ = _app.track_event("tray_settings", None);
window.emit("settings", ()).unwrap();
}
_ => (),
}
})
.icon(icon)
.build(app)?;
Ok(())
}

View file

@ -1,94 +1,94 @@
use tauri::{ async_runtime, AppHandle, Manager };
use tauri_plugin_dialog::{ DialogExt, MessageDialogButtons, MessageDialogKind };
use tauri_plugin_updater::UpdaterExt;
pub async fn check_for_updates(app: AppHandle, prompted: bool) {
println!("Checking for updates...");
let updater = app.updater().unwrap();
let response = updater.check().await;
match response {
Ok(Some(update)) => {
let cur_ver = &update.current_version;
let new_ver = &update.version;
let mut msg = String::new();
msg.extend([
&format!("{cur_ver} -> {new_ver}\n\n"),
"Would you like to install it now?",
]);
let window = app.get_webview_window("main").unwrap();
window.show().unwrap();
window.set_focus().unwrap();
app.dialog()
.message(msg)
.title("Qopy Update Available")
.buttons(
MessageDialogButtons::OkCancelCustom(
String::from("Install"),
String::from("Cancel")
)
)
.show(move |response| {
if !response {
return;
}
async_runtime::spawn(async move {
match
update.download_and_install(
|_, _| {},
|| {}
).await
{
Ok(_) => {
app.dialog()
.message(
"Update installed successfully. The application needs to restart to apply the changes."
)
.title("Qopy Update Installed")
.buttons(
MessageDialogButtons::OkCancelCustom(
String::from("Restart"),
String::from("Cancel")
)
)
.show(move |response| {
if response {
app.restart();
}
});
}
Err(e) => {
println!("Error installing new update: {:?}", e);
app.dialog()
.message(
"Failed to install new update. The new update can be downloaded from Github"
)
.kind(MessageDialogKind::Error)
.show(|_| {});
}
}
});
});
}
Ok(None) => {
println!("No updates available.");
}
Err(e) => {
if prompted {
let window = app.get_webview_window("main").unwrap();
window.show().unwrap();
window.set_focus().unwrap();
app.dialog()
.message("No updates available.")
.title("Qopy Update Check")
.show(|_| {});
}
println!("No updates available. {}", e.to_string());
}
}
}
use tauri::{ async_runtime, AppHandle, Manager };
use tauri_plugin_dialog::{ DialogExt, MessageDialogButtons, MessageDialogKind };
use tauri_plugin_updater::UpdaterExt;
pub async fn check_for_updates(app: AppHandle, prompted: bool) {
println!("Checking for updates...");
let updater = app.updater().unwrap();
let response = updater.check().await;
match response {
Ok(Some(update)) => {
let cur_ver = &update.current_version;
let new_ver = &update.version;
let mut msg = String::new();
msg.extend([
&format!("{cur_ver} -> {new_ver}\n\n"),
"Would you like to install it now?",
]);
let window = app.get_webview_window("main").unwrap();
window.show().unwrap();
window.set_focus().unwrap();
app.dialog()
.message(msg)
.title("Qopy Update Available")
.buttons(
MessageDialogButtons::OkCancelCustom(
String::from("Install"),
String::from("Cancel")
)
)
.show(move |response| {
if !response {
return;
}
async_runtime::spawn(async move {
match
update.download_and_install(
|_, _| {},
|| {}
).await
{
Ok(_) => {
app.dialog()
.message(
"Update installed successfully. The application needs to restart to apply the changes."
)
.title("Qopy Update Installed")
.buttons(
MessageDialogButtons::OkCancelCustom(
String::from("Restart"),
String::from("Cancel")
)
)
.show(move |response| {
if response {
app.restart();
}
});
}
Err(e) => {
println!("Error installing new update: {:?}", e);
app.dialog()
.message(
"Failed to install new update. The new update can be downloaded from Github"
)
.kind(MessageDialogKind::Error)
.show(|_| {});
}
}
});
});
}
Ok(None) => {
println!("No updates available.");
}
Err(e) => {
if prompted {
let window = app.get_webview_window("main").unwrap();
window.show().unwrap();
window.set_focus().unwrap();
app.dialog()
.message("No updates available.")
.title("Qopy Update Check")
.show(|_| {});
}
println!("No updates available. {}", e.to_string());
}
}
}

View file

@ -1,107 +1,107 @@
use include_dir::{ include_dir, Dir };
use sqlx::sqlite::{ SqlitePool, SqlitePoolOptions };
use std::fs;
use tauri::Manager;
use tokio::runtime::Runtime as TokioRuntime;
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/db/migrations");
pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
let rt = TokioRuntime::new().expect("Failed to create Tokio runtime");
app.manage(rt);
let rt = app.state::<TokioRuntime>();
let app_data_dir = app.path().app_data_dir().unwrap();
fs::create_dir_all(&app_data_dir).expect("Failed to create app data directory");
let db_path = app_data_dir.join("data.db");
let is_new_db = !db_path.exists();
if is_new_db {
fs::File::create(&db_path).expect("Failed to create database file");
}
let db_url = format!("sqlite:{}", db_path.to_str().unwrap());
let pool = rt.block_on(async {
SqlitePoolOptions::new()
.max_connections(5)
.connect(&db_url).await
.expect("Failed to create pool")
});
app.manage(pool.clone());
rt.block_on(async {
apply_migrations(&pool).await?;
if is_new_db {
if let Err(e) = super::history::initialize_history(&pool).await {
eprintln!("Failed to initialize history: {}", e);
}
if let Err(e) = super::settings::initialize_settings(&pool).await {
eprintln!("Failed to initialize settings: {}", e);
}
}
Ok::<(), Box<dyn std::error::Error>>(())
})?;
Ok(())
}
async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
sqlx
::query(
"CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER PRIMARY KEY,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
);"
)
.execute(pool).await?;
let current_version: Option<i64> = sqlx
::query_scalar("SELECT MAX(version) FROM schema_version")
.fetch_one(pool).await?;
let current_version = current_version.unwrap_or(0);
let mut migration_files: Vec<(i64, &str)> = MIGRATIONS_DIR.files()
.filter_map(|file| {
let file_name = file.path().file_name()?.to_str()?;
if file_name.ends_with(".sql") && file_name.starts_with("v") {
let version: i64 = file_name
.trim_start_matches("v")
.trim_end_matches(".sql")
.parse()
.ok()?;
Some((version, file.contents_utf8()?))
} else {
None
}
})
.collect();
migration_files.sort_by_key(|(version, _)| *version);
for (version, content) in migration_files {
if version > current_version {
let statements: Vec<&str> = content
.split(';')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
for statement in statements {
sqlx
::query(statement)
.execute(pool).await
.map_err(|e| format!("Failed to execute migration {}: {}", version, e))?;
}
sqlx
::query("INSERT INTO schema_version (version) VALUES (?)")
.bind(version)
.execute(pool).await?;
}
}
Ok(())
}
use include_dir::{ include_dir, Dir };
use sqlx::sqlite::{ SqlitePool, SqlitePoolOptions };
use std::fs;
use tauri::Manager;
use tokio::runtime::Runtime as TokioRuntime;
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/db/migrations");
pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
let rt = TokioRuntime::new().expect("Failed to create Tokio runtime");
app.manage(rt);
let rt = app.state::<TokioRuntime>();
let app_data_dir = app.path().app_data_dir().unwrap();
fs::create_dir_all(&app_data_dir).expect("Failed to create app data directory");
let db_path = app_data_dir.join("data.db");
let is_new_db = !db_path.exists();
if is_new_db {
fs::File::create(&db_path).expect("Failed to create database file");
}
let db_url = format!("sqlite:{}", db_path.to_str().unwrap());
let pool = rt.block_on(async {
SqlitePoolOptions::new()
.max_connections(5)
.connect(&db_url).await
.expect("Failed to create pool")
});
app.manage(pool.clone());
rt.block_on(async {
apply_migrations(&pool).await?;
if is_new_db {
if let Err(e) = super::history::initialize_history(&pool).await {
eprintln!("Failed to initialize history: {}", e);
}
if let Err(e) = super::settings::initialize_settings(&pool).await {
eprintln!("Failed to initialize settings: {}", e);
}
}
Ok::<(), Box<dyn std::error::Error>>(())
})?;
Ok(())
}
async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
sqlx
::query(
"CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER PRIMARY KEY,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
);"
)
.execute(pool).await?;
let current_version: Option<i64> = sqlx
::query_scalar("SELECT MAX(version) FROM schema_version")
.fetch_one(pool).await?;
let current_version = current_version.unwrap_or(0);
let mut migration_files: Vec<(i64, &str)> = MIGRATIONS_DIR.files()
.filter_map(|file| {
let file_name = file.path().file_name()?.to_str()?;
if file_name.ends_with(".sql") && file_name.starts_with("v") {
let version: i64 = file_name
.trim_start_matches("v")
.trim_end_matches(".sql")
.parse()
.ok()?;
Some((version, file.contents_utf8()?))
} else {
None
}
})
.collect();
migration_files.sort_by_key(|(version, _)| *version);
for (version, content) in migration_files {
if version > current_version {
let statements: Vec<&str> = content
.split(';')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
for statement in statements {
sqlx
::query(statement)
.execute(pool).await
.map_err(|e| format!("Failed to execute migration {}: {}", version, e))?;
}
sqlx
::query("INSERT INTO schema_version (version) VALUES (?)")
.bind(version)
.execute(pool).await?;
}
}
Ok(())
}

View file

@ -1,226 +1,226 @@
use crate::utils::types::{ ContentType, HistoryItem };
use base64::{ engine::general_purpose::STANDARD, Engine };
use rand::{ rng, Rng };
use rand::distr::Alphanumeric;
use sqlx::{ Row, SqlitePool };
use std::fs;
use tauri_plugin_aptabase::EventTracker;
use tauri::Emitter;
pub async fn initialize_history(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
let id: String = rng()
.sample_iter(&Alphanumeric)
.take(16)
.map(char::from)
.collect();
sqlx::query(
"INSERT INTO history (id, source, content_type, content, timestamp) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)"
)
.bind(id)
.bind("System")
.bind("text")
.bind("Welcome to your clipboard history!")
.execute(pool).await?;
Ok(())
}
#[tauri::command]
pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result<Vec<HistoryItem>, String> {
let rows = sqlx
::query(
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC"
)
.fetch_all(&*pool).await
.map_err(|e| e.to_string())?;
let items = rows
.iter()
.map(|row| HistoryItem {
id: row.get("id"),
source: row.get("source"),
source_icon: row.get("source_icon"),
content_type: ContentType::from(row.get::<String, _>("content_type")),
content: row.get("content"),
favicon: row.get("favicon"),
timestamp: row.get("timestamp"),
language: row.get("language"),
})
.collect();
Ok(items)
}
#[tauri::command]
pub async fn add_history_item(
app_handle: tauri::AppHandle,
pool: tauri::State<'_, SqlitePool>,
item: HistoryItem
) -> Result<(), String> {
let (id, source, source_icon, content_type, content, favicon, timestamp, language) =
item.to_row();
let existing = sqlx
::query("SELECT id FROM history WHERE content = ? AND content_type = ?")
.bind(&content)
.bind(&content_type)
.fetch_optional(&*pool).await
.map_err(|e| e.to_string())?;
match existing {
Some(_) => {
sqlx
::query(
"UPDATE history SET source = ?, source_icon = ?, timestamp = strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now'), favicon = ?, language = ? WHERE content = ? AND content_type = ?"
)
.bind(&source)
.bind(&source_icon)
.bind(&favicon)
.bind(&language)
.bind(&content)
.bind(&content_type)
.execute(&*pool).await
.map_err(|e| e.to_string())?;
}
None => {
sqlx
::query(
"INSERT INTO history (id, source, source_icon, content_type, content, favicon, timestamp, language) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
)
.bind(id)
.bind(source)
.bind(source_icon)
.bind(content_type)
.bind(content)
.bind(favicon)
.bind(timestamp)
.bind(language)
.execute(&*pool).await
.map_err(|e| e.to_string())?;
}
}
let _ = app_handle.track_event(
"history_item_added",
Some(serde_json::json!({
"content_type": item.content_type.to_string()
}))
);
let _ = app_handle.emit("clipboard-content-updated", ());
Ok(())
}
#[tauri::command]
pub async fn search_history(
pool: tauri::State<'_, SqlitePool>,
query: String
) -> Result<Vec<HistoryItem>, String> {
if query.trim().is_empty() {
return Ok(Vec::new());
}
let query = format!("%{}%", query);
let rows = sqlx
::query(
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language
FROM history
WHERE content LIKE ?
ORDER BY timestamp DESC
LIMIT 100"
)
.bind(query)
.fetch_all(&*pool).await
.map_err(|e| e.to_string())?;
let mut items = Vec::with_capacity(rows.len());
for row in rows.iter() {
items.push(HistoryItem {
id: row.get("id"),
source: row.get("source"),
source_icon: row.get("source_icon"),
content_type: ContentType::from(row.get::<String, _>("content_type")),
content: row.get("content"),
favicon: row.get("favicon"),
timestamp: row.get("timestamp"),
language: row.get("language"),
});
}
Ok(items)
}
#[tauri::command]
pub async fn load_history_chunk(
pool: tauri::State<'_, SqlitePool>,
offset: i64,
limit: i64
) -> Result<Vec<HistoryItem>, String> {
let rows = sqlx
::query(
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC LIMIT ? OFFSET ?"
)
.bind(limit)
.bind(offset)
.fetch_all(&*pool).await
.map_err(|e| e.to_string())?;
let items = rows
.iter()
.map(|row| HistoryItem {
id: row.get("id"),
source: row.get("source"),
source_icon: row.get("source_icon"),
content_type: ContentType::from(row.get::<String, _>("content_type")),
content: row.get("content"),
favicon: row.get("favicon"),
timestamp: row.get("timestamp"),
language: row.get("language"),
})
.collect();
Ok(items)
}
#[tauri::command]
pub async fn delete_history_item(
app_handle: tauri::AppHandle,
pool: tauri::State<'_, SqlitePool>,
id: String
) -> Result<(), String> {
sqlx
::query("DELETE FROM history WHERE id = ?")
.bind(id)
.execute(&*pool).await
.map_err(|e| e.to_string())?;
let _ = app_handle.track_event("history_item_deleted", None);
let _ = app_handle.emit("clipboard-content-updated", ());
Ok(())
}
#[tauri::command]
pub async fn clear_history(
app_handle: tauri::AppHandle,
pool: tauri::State<'_, SqlitePool>
) -> Result<(), String> {
sqlx
::query("DELETE FROM history")
.execute(&*pool).await
.map_err(|e| e.to_string())?;
let _ = app_handle.track_event("history_cleared", None);
let _ = app_handle.emit("clipboard-content-updated", ());
Ok(())
}
#[tauri::command]
pub async fn read_image(filename: String) -> Result<String, String> {
let bytes = fs::read(filename).map_err(|e| e.to_string())?;
Ok(STANDARD.encode(bytes))
}
use crate::utils::types::{ ContentType, HistoryItem };
use base64::{ engine::general_purpose::STANDARD, Engine };
use rand::{ rng, Rng };
use rand::distr::Alphanumeric;
use sqlx::{ Row, SqlitePool };
use std::fs;
use tauri_plugin_aptabase::EventTracker;
use tauri::Emitter;
pub async fn initialize_history(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
let id: String = rng()
.sample_iter(&Alphanumeric)
.take(16)
.map(char::from)
.collect();
sqlx::query(
"INSERT INTO history (id, source, content_type, content, timestamp) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)"
)
.bind(id)
.bind("System")
.bind("text")
.bind("Welcome to your clipboard history!")
.execute(pool).await?;
Ok(())
}
#[tauri::command]
pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result<Vec<HistoryItem>, String> {
let rows = sqlx
::query(
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC"
)
.fetch_all(&*pool).await
.map_err(|e| e.to_string())?;
let items = rows
.iter()
.map(|row| HistoryItem {
id: row.get("id"),
source: row.get("source"),
source_icon: row.get("source_icon"),
content_type: ContentType::from(row.get::<String, _>("content_type")),
content: row.get("content"),
favicon: row.get("favicon"),
timestamp: row.get("timestamp"),
language: row.get("language"),
})
.collect();
Ok(items)
}
#[tauri::command]
pub async fn add_history_item(
app_handle: tauri::AppHandle,
pool: tauri::State<'_, SqlitePool>,
item: HistoryItem
) -> Result<(), String> {
let (id, source, source_icon, content_type, content, favicon, timestamp, language) =
item.to_row();
let existing = sqlx
::query("SELECT id FROM history WHERE content = ? AND content_type = ?")
.bind(&content)
.bind(&content_type)
.fetch_optional(&*pool).await
.map_err(|e| e.to_string())?;
match existing {
Some(_) => {
sqlx
::query(
"UPDATE history SET source = ?, source_icon = ?, timestamp = strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now'), favicon = ?, language = ? WHERE content = ? AND content_type = ?"
)
.bind(&source)
.bind(&source_icon)
.bind(&favicon)
.bind(&language)
.bind(&content)
.bind(&content_type)
.execute(&*pool).await
.map_err(|e| e.to_string())?;
}
None => {
sqlx
::query(
"INSERT INTO history (id, source, source_icon, content_type, content, favicon, timestamp, language) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
)
.bind(id)
.bind(source)
.bind(source_icon)
.bind(content_type)
.bind(content)
.bind(favicon)
.bind(timestamp)
.bind(language)
.execute(&*pool).await
.map_err(|e| e.to_string())?;
}
}
let _ = app_handle.track_event(
"history_item_added",
Some(serde_json::json!({
"content_type": item.content_type.to_string()
}))
);
let _ = app_handle.emit("clipboard-content-updated", ());
Ok(())
}
#[tauri::command]
pub async fn search_history(
pool: tauri::State<'_, SqlitePool>,
query: String
) -> Result<Vec<HistoryItem>, String> {
if query.trim().is_empty() {
return Ok(Vec::new());
}
let query = format!("%{}%", query);
let rows = sqlx
::query(
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language
FROM history
WHERE content LIKE ?
ORDER BY timestamp DESC
LIMIT 100"
)
.bind(query)
.fetch_all(&*pool).await
.map_err(|e| e.to_string())?;
let mut items = Vec::with_capacity(rows.len());
for row in rows.iter() {
items.push(HistoryItem {
id: row.get("id"),
source: row.get("source"),
source_icon: row.get("source_icon"),
content_type: ContentType::from(row.get::<String, _>("content_type")),
content: row.get("content"),
favicon: row.get("favicon"),
timestamp: row.get("timestamp"),
language: row.get("language"),
});
}
Ok(items)
}
#[tauri::command]
pub async fn load_history_chunk(
pool: tauri::State<'_, SqlitePool>,
offset: i64,
limit: i64
) -> Result<Vec<HistoryItem>, String> {
let rows = sqlx
::query(
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC LIMIT ? OFFSET ?"
)
.bind(limit)
.bind(offset)
.fetch_all(&*pool).await
.map_err(|e| e.to_string())?;
let items = rows
.iter()
.map(|row| HistoryItem {
id: row.get("id"),
source: row.get("source"),
source_icon: row.get("source_icon"),
content_type: ContentType::from(row.get::<String, _>("content_type")),
content: row.get("content"),
favicon: row.get("favicon"),
timestamp: row.get("timestamp"),
language: row.get("language"),
})
.collect();
Ok(items)
}
#[tauri::command]
pub async fn delete_history_item(
app_handle: tauri::AppHandle,
pool: tauri::State<'_, SqlitePool>,
id: String
) -> Result<(), String> {
sqlx
::query("DELETE FROM history WHERE id = ?")
.bind(id)
.execute(&*pool).await
.map_err(|e| e.to_string())?;
let _ = app_handle.track_event("history_item_deleted", None);
let _ = app_handle.emit("clipboard-content-updated", ());
Ok(())
}
#[tauri::command]
pub async fn clear_history(
app_handle: tauri::AppHandle,
pool: tauri::State<'_, SqlitePool>
) -> Result<(), String> {
sqlx
::query("DELETE FROM history")
.execute(&*pool).await
.map_err(|e| e.to_string())?;
let _ = app_handle.track_event("history_cleared", None);
let _ = app_handle.emit("clipboard-content-updated", ());
Ok(())
}
#[tauri::command]
pub async fn read_image(filename: String) -> Result<String, String> {
let bytes = fs::read(filename).map_err(|e| e.to_string())?;
Ok(STANDARD.encode(bytes))
}

View file

@ -1,12 +1,12 @@
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS history (
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
content_type TEXT NOT NULL,
content TEXT NOT NULL,
favicon TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS history (
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
content_type TEXT NOT NULL,
content TEXT NOT NULL,
favicon TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);

View file

@ -1,3 +1,3 @@
ALTER TABLE history ADD COLUMN source TEXT DEFAULT 'System' NOT NULL;
ALTER TABLE history ADD COLUMN source_icon TEXT;
ALTER TABLE history ADD COLUMN language TEXT;
ALTER TABLE history ADD COLUMN source TEXT DEFAULT 'System' NOT NULL;
ALTER TABLE history ADD COLUMN source_icon TEXT;
ALTER TABLE history ADD COLUMN language TEXT;

View file

@ -1 +1 @@
INSERT INTO settings (key, value) VALUES ('autostart', 'true');
INSERT INTO settings (key, value) VALUES ('autostart', 'true');

View file

@ -1,3 +1,3 @@
pub mod database;
pub mod history;
pub mod settings;
pub mod database;
pub mod history;
pub mod settings;

View file

@ -1,87 +1,87 @@
use serde::{ Deserialize, Serialize };
use serde_json;
use sqlx::Row;
use sqlx::SqlitePool;
use tauri::{ Emitter, Manager };
use tauri_plugin_aptabase::EventTracker;
#[derive(Deserialize, Serialize)]
struct KeybindSetting {
keybind: Vec<String>,
}
pub async fn initialize_settings(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
let default_keybind = KeybindSetting {
keybind: vec!["Meta".to_string(), "V".to_string()],
};
let json = serde_json::to_string(&default_keybind)?;
sqlx
::query("INSERT INTO settings (key, value) VALUES ('keybind', ?)")
.bind(json)
.execute(pool).await?;
Ok(())
}
#[tauri::command]
pub async fn get_setting(
pool: tauri::State<'_, SqlitePool>,
key: String
) -> Result<String, String> {
let row = sqlx
::query("SELECT value FROM settings WHERE key = ?")
.bind(key)
.fetch_optional(&*pool).await
.map_err(|e| e.to_string())?;
Ok(row.map(|r| r.get("value")).unwrap_or_default())
}
#[tauri::command]
pub async fn save_setting(
app_handle: tauri::AppHandle,
pool: tauri::State<'_, SqlitePool>,
key: String,
value: String
) -> Result<(), String> {
sqlx
::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
.bind(key.clone())
.bind(value.clone())
.execute(&*pool).await
.map_err(|e| e.to_string())?;
let _ = app_handle.track_event(
"setting_saved",
Some(serde_json::json!({
"key": key
}))
);
if key == "keybind" {
let _ = app_handle.emit("update-shortcut", &value).map_err(|e| e.to_string())?;
}
Ok(())
}
#[tauri::command]
pub async fn get_keybind(app_handle: tauri::AppHandle) -> Result<Vec<String>, String> {
let pool = app_handle.state::<SqlitePool>();
let row = sqlx
::query("SELECT value FROM settings WHERE key = 'keybind'")
.fetch_optional(&*pool).await
.map_err(|e| e.to_string())?;
let json = row
.map(|r| r.get::<String, _>("value"))
.unwrap_or_else(|| {
serde_json
::to_string(&vec!["MetaLeft".to_string(), "KeyV".to_string()])
.expect("Failed to serialize default keybind")
});
serde_json::from_str::<Vec<String>>(&json).map_err(|e| e.to_string())
}
use serde::{ Deserialize, Serialize };
use serde_json;
use sqlx::Row;
use sqlx::SqlitePool;
use tauri::{ Emitter, Manager };
use tauri_plugin_aptabase::EventTracker;
#[derive(Deserialize, Serialize)]
struct KeybindSetting {
keybind: Vec<String>,
}
pub async fn initialize_settings(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
let default_keybind = KeybindSetting {
keybind: vec!["Meta".to_string(), "V".to_string()],
};
let json = serde_json::to_string(&default_keybind)?;
sqlx
::query("INSERT INTO settings (key, value) VALUES ('keybind', ?)")
.bind(json)
.execute(pool).await?;
Ok(())
}
#[tauri::command]
pub async fn get_setting(
pool: tauri::State<'_, SqlitePool>,
key: String
) -> Result<String, String> {
let row = sqlx
::query("SELECT value FROM settings WHERE key = ?")
.bind(key)
.fetch_optional(&*pool).await
.map_err(|e| e.to_string())?;
Ok(row.map(|r| r.get("value")).unwrap_or_default())
}
#[tauri::command]
pub async fn save_setting(
app_handle: tauri::AppHandle,
pool: tauri::State<'_, SqlitePool>,
key: String,
value: String
) -> Result<(), String> {
sqlx
::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
.bind(key.clone())
.bind(value.clone())
.execute(&*pool).await
.map_err(|e| e.to_string())?;
let _ = app_handle.track_event(
"setting_saved",
Some(serde_json::json!({
"key": key
}))
);
if key == "keybind" {
let _ = app_handle.emit("update-shortcut", &value).map_err(|e| e.to_string())?;
}
Ok(())
}
#[tauri::command]
pub async fn get_keybind(app_handle: tauri::AppHandle) -> Result<Vec<String>, String> {
let pool = app_handle.state::<SqlitePool>();
let row = sqlx
::query("SELECT value FROM settings WHERE key = 'keybind'")
.fetch_optional(&*pool).await
.map_err(|e| e.to_string())?;
let json = row
.map(|r| r.get::<String, _>("value"))
.unwrap_or_else(|| {
serde_json
::to_string(&vec!["MetaLeft".to_string(), "KeyV".to_string()])
.expect("Failed to serialize default keybind")
});
serde_json::from_str::<Vec<String>>(&json).map_err(|e| e.to_string())
}

View file

@ -1,136 +1,136 @@
#![cfg_attr(all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows")]
mod api;
mod db;
mod utils;
use sqlx::sqlite::SqlitePoolOptions;
use std::fs;
use tauri::Manager;
use tauri_plugin_aptabase::{ EventTracker, InitOptions };
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_prevent_default::Flags;
fn main() {
let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
let _guard = runtime.enter();
tauri::Builder
::default()
.plugin(tauri_plugin_clipboard::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_sql::Builder::default().build())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_updater::Builder::default().build())
.plugin(
tauri_plugin_aptabase::Builder
::new("A-SH-8937252746")
.with_options(InitOptions {
host: Some("https://aptabase.pandadev.net".to_string()),
flush_interval: None,
})
.with_panic_hook(
Box::new(|client, info, msg| {
let location = info
.location()
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
.unwrap_or_else(|| "".to_string());
let _ = client.track_event(
"panic",
Some(
serde_json::json!({
"info": format!("{} ({})", msg, location),
})
)
);
})
)
.build()
)
.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec![])))
.plugin(
tauri_plugin_prevent_default::Builder
::new()
.with_flags(Flags::all().difference(Flags::CONTEXT_MENU))
.build()
)
.setup(|app| {
#[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
let app_data_dir = app.path().app_data_dir().unwrap();
utils::logger::init_logger(&app_data_dir).expect("Failed to initialize logger");
fs::create_dir_all(&app_data_dir).expect("Failed to create app data directory");
let db_path = app_data_dir.join("data.db");
let is_new_db = !db_path.exists();
if is_new_db {
fs::File::create(&db_path).expect("Failed to create database file");
}
let db_url = format!("sqlite:{}", db_path.to_str().unwrap());
let app_handle = app.handle().clone();
let app_handle_clone = app_handle.clone();
tauri::async_runtime::spawn(async move {
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect(&db_url).await
.expect("Failed to create pool");
app_handle_clone.manage(pool);
});
let main_window = app.get_webview_window("main");
let _ = db::database::setup(app);
api::hotkeys::setup(app_handle.clone());
api::tray::setup(app)?;
api::clipboard::setup(app.handle());
let _ = api::clipboard::start_monitor(app_handle.clone());
utils::commands::center_window_on_current_monitor(main_window.as_ref().unwrap());
main_window
.as_ref()
.map(|w| w.hide())
.unwrap_or(Ok(()))?;
let _ = app.track_event("app_started", None);
tauri::async_runtime::spawn(async move {
api::updater::check_for_updates(app_handle, false).await;
});
Ok(())
})
.on_window_event(|_app, _event| {
#[cfg(not(dev))]
if let tauri::WindowEvent::Focused(false) = _event {
if let Some(window) = _app.get_webview_window("main") {
let _ = window.hide();
}
}
})
.invoke_handler(
tauri::generate_handler![
api::clipboard::write_and_paste,
db::history::get_history,
db::history::add_history_item,
db::history::search_history,
db::history::load_history_chunk,
db::history::delete_history_item,
db::history::clear_history,
db::history::read_image,
db::settings::get_setting,
db::settings::save_setting,
utils::commands::fetch_page_meta,
utils::commands::get_app_info
]
)
.run(tauri::generate_context!())
.expect("error while running tauri application");
#![cfg_attr(all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows")]
mod api;
mod db;
mod utils;
use sqlx::sqlite::SqlitePoolOptions;
use std::fs;
use tauri::Manager;
use tauri_plugin_aptabase::{ EventTracker, InitOptions };
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_prevent_default::Flags;
fn main() {
let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
let _guard = runtime.enter();
tauri::Builder
::default()
.plugin(tauri_plugin_clipboard::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_sql::Builder::default().build())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_updater::Builder::default().build())
.plugin(
tauri_plugin_aptabase::Builder
::new("A-SH-8937252746")
.with_options(InitOptions {
host: Some("https://aptabase.pandadev.net".to_string()),
flush_interval: None,
})
.with_panic_hook(
Box::new(|client, info, msg| {
let location = info
.location()
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
.unwrap_or_else(|| "".to_string());
let _ = client.track_event(
"panic",
Some(
serde_json::json!({
"info": format!("{} ({})", msg, location),
})
)
);
})
)
.build()
)
.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec![])))
.plugin(
tauri_plugin_prevent_default::Builder
::new()
.with_flags(Flags::all().difference(Flags::CONTEXT_MENU))
.build()
)
.setup(|app| {
#[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
let app_data_dir = app.path().app_data_dir().unwrap();
utils::logger::init_logger(&app_data_dir).expect("Failed to initialize logger");
fs::create_dir_all(&app_data_dir).expect("Failed to create app data directory");
let db_path = app_data_dir.join("data.db");
let is_new_db = !db_path.exists();
if is_new_db {
fs::File::create(&db_path).expect("Failed to create database file");
}
let db_url = format!("sqlite:{}", db_path.to_str().unwrap());
let app_handle = app.handle().clone();
let app_handle_clone = app_handle.clone();
tauri::async_runtime::spawn(async move {
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect(&db_url).await
.expect("Failed to create pool");
app_handle_clone.manage(pool);
});
let main_window = app.get_webview_window("main");
let _ = db::database::setup(app);
api::hotkeys::setup(app_handle.clone());
api::tray::setup(app)?;
api::clipboard::setup(app.handle());
let _ = api::clipboard::start_monitor(app_handle.clone());
utils::commands::center_window_on_current_monitor(main_window.as_ref().unwrap());
main_window
.as_ref()
.map(|w| w.hide())
.unwrap_or(Ok(()))?;
let _ = app.track_event("app_started", None);
tauri::async_runtime::spawn(async move {
api::updater::check_for_updates(app_handle, false).await;
});
Ok(())
})
.on_window_event(|_app, _event| {
#[cfg(not(dev))]
if let tauri::WindowEvent::Focused(false) = _event {
if let Some(window) = _app.get_webview_window("main") {
let _ = window.hide();
}
}
})
.invoke_handler(
tauri::generate_handler![
api::clipboard::write_and_paste,
db::history::get_history,
db::history::add_history_item,
db::history::search_history,
db::history::load_history_chunk,
db::history::delete_history_item,
db::history::clear_history,
db::history::read_image,
db::settings::get_setting,
db::settings::save_setting,
utils::commands::fetch_page_meta,
utils::commands::get_app_info
]
)
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View file

@ -1,155 +1,155 @@
use applications::{AppInfoContext, AppInfo, AppTrait, utils::image::RustImage};
use base64::{ engine::general_purpose::STANDARD, Engine };
use image::codecs::png::PngEncoder;
use tauri::PhysicalPosition;
use meta_fetcher;
pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
if
let Some(monitor) = window
.available_monitors()
.unwrap()
.iter()
.find(|m| {
let primary_monitor = window
.primary_monitor()
.unwrap()
.expect("Failed to get primary monitor");
let mouse_position = primary_monitor.position();
let monitor_position = m.position();
let monitor_size = m.size();
mouse_position.x >= monitor_position.x &&
mouse_position.x < monitor_position.x + (monitor_size.width as i32) &&
mouse_position.y >= monitor_position.y &&
mouse_position.y < monitor_position.y + (monitor_size.height as i32)
})
{
let monitor_size = monitor.size();
let window_size = window.outer_size().unwrap();
let x = ((monitor_size.width as i32) - (window_size.width as i32)) / 2;
let y = ((monitor_size.height as i32) - (window_size.height as i32)) / 2;
window
.set_position(PhysicalPosition::new(monitor.position().x + x, monitor.position().y + y))
.unwrap();
}
}
#[tauri::command]
pub fn get_app_info() -> (String, Option<String>) {
println!("Getting app info");
let mut ctx = AppInfoContext::new(vec![]);
println!("Created AppInfoContext");
if let Err(e) = ctx.refresh_apps() {
println!("Failed to refresh apps: {:?}", e);
return ("System".to_string(), None);
}
println!("Refreshed apps");
let result = std::panic::catch_unwind(|| {
match ctx.get_frontmost_application() {
Ok(window) => {
println!("Found frontmost application: {}", window.name);
let name = window.name.clone();
let icon = window
.load_icon()
.ok()
.and_then(|i| {
println!("Loading icon for {}", name);
i.to_png().ok().map(|png| {
let encoded = STANDARD.encode(png.get_bytes());
println!("Icon encoded successfully");
encoded
})
});
println!("Returning app info: {} with icon: {}", name, icon.is_some());
(name, icon)
}
Err(e) => {
println!("Failed to get frontmost application: {:?}", e);
("System".to_string(), None)
}
}
});
match result {
Ok(info) => info,
Err(_) => {
println!("Panic occurred while getting app info");
("System".to_string(), None)
}
}
}
fn _process_icon_to_base64(path: &str) -> Result<String, Box<dyn std::error::Error>> {
let img = image::open(path)?;
let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
let mut png_buffer = Vec::new();
resized.write_with_encoder(PngEncoder::new(&mut png_buffer))?;
Ok(STANDARD.encode(png_buffer))
}
pub fn detect_color(color: &str) -> bool {
let color = color.trim().to_lowercase();
// hex
if color.starts_with('#') && color.len() == color.trim_end_matches(char::is_whitespace).len() {
let hex = &color[1..];
return match hex.len() {
3 | 6 | 8 => hex.chars().all(|c| c.is_ascii_hexdigit()),
_ => false,
};
}
// rgb/rgba
if
(color.starts_with("rgb(") || color.starts_with("rgba(")) &&
color.ends_with(")") &&
!color[..color.len() - 1].contains(")")
{
let values = color
.trim_start_matches("rgba(")
.trim_start_matches("rgb(")
.trim_end_matches(')')
.split(',')
.collect::<Vec<&str>>();
return match values.len() {
3 | 4 => values.iter().all(|v| v.trim().parse::<f32>().is_ok()),
_ => false,
};
}
// hsl/hsla
if
(color.starts_with("hsl(") || color.starts_with("hsla(")) &&
color.ends_with(")") &&
!color[..color.len() - 1].contains(")")
{
let values = color
.trim_start_matches("hsla(")
.trim_start_matches("hsl(")
.trim_end_matches(')')
.split(',')
.collect::<Vec<&str>>();
return match values.len() {
3 | 4 => values.iter().all(|v| v.trim().parse::<f32>().is_ok()),
_ => false,
};
}
false
}
#[tauri::command]
pub async fn fetch_page_meta(url: String) -> Result<(String, Option<String>), String> {
let metadata = meta_fetcher
::fetch_metadata(&url)
.map_err(|e| format!("Failed to fetch metadata: {}", e))?;
Ok((metadata.title.unwrap_or_else(|| "No title found".to_string()), metadata.image))
}
use applications::{AppInfoContext, AppInfo, AppTrait, utils::image::RustImage};
use base64::{ engine::general_purpose::STANDARD, Engine };
use image::codecs::png::PngEncoder;
use tauri::PhysicalPosition;
use meta_fetcher;
pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
if
let Some(monitor) = window
.available_monitors()
.unwrap()
.iter()
.find(|m| {
let primary_monitor = window
.primary_monitor()
.unwrap()
.expect("Failed to get primary monitor");
let mouse_position = primary_monitor.position();
let monitor_position = m.position();
let monitor_size = m.size();
mouse_position.x >= monitor_position.x &&
mouse_position.x < monitor_position.x + (monitor_size.width as i32) &&
mouse_position.y >= monitor_position.y &&
mouse_position.y < monitor_position.y + (monitor_size.height as i32)
})
{
let monitor_size = monitor.size();
let window_size = window.outer_size().unwrap();
let x = ((monitor_size.width as i32) - (window_size.width as i32)) / 2;
let y = ((monitor_size.height as i32) - (window_size.height as i32)) / 2;
window
.set_position(PhysicalPosition::new(monitor.position().x + x, monitor.position().y + y))
.unwrap();
}
}
#[tauri::command]
pub fn get_app_info() -> (String, Option<String>) {
println!("Getting app info");
let mut ctx = AppInfoContext::new(vec![]);
println!("Created AppInfoContext");
if let Err(e) = ctx.refresh_apps() {
println!("Failed to refresh apps: {:?}", e);
return ("System".to_string(), None);
}
println!("Refreshed apps");
let result = std::panic::catch_unwind(|| {
match ctx.get_frontmost_application() {
Ok(window) => {
println!("Found frontmost application: {}", window.name);
let name = window.name.clone();
let icon = window
.load_icon()
.ok()
.and_then(|i| {
println!("Loading icon for {}", name);
i.to_png().ok().map(|png| {
let encoded = STANDARD.encode(png.get_bytes());
println!("Icon encoded successfully");
encoded
})
});
println!("Returning app info: {} with icon: {}", name, icon.is_some());
(name, icon)
}
Err(e) => {
println!("Failed to get frontmost application: {:?}", e);
("System".to_string(), None)
}
}
});
match result {
Ok(info) => info,
Err(_) => {
println!("Panic occurred while getting app info");
("System".to_string(), None)
}
}
}
fn _process_icon_to_base64(path: &str) -> Result<String, Box<dyn std::error::Error>> {
let img = image::open(path)?;
let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
let mut png_buffer = Vec::new();
resized.write_with_encoder(PngEncoder::new(&mut png_buffer))?;
Ok(STANDARD.encode(png_buffer))
}
pub fn detect_color(color: &str) -> bool {
let color = color.trim().to_lowercase();
// hex
if color.starts_with('#') && color.len() == color.trim_end_matches(char::is_whitespace).len() {
let hex = &color[1..];
return match hex.len() {
3 | 6 | 8 => hex.chars().all(|c| c.is_ascii_hexdigit()),
_ => false,
};
}
// rgb/rgba
if
(color.starts_with("rgb(") || color.starts_with("rgba(")) &&
color.ends_with(")") &&
!color[..color.len() - 1].contains(")")
{
let values = color
.trim_start_matches("rgba(")
.trim_start_matches("rgb(")
.trim_end_matches(')')
.split(',')
.collect::<Vec<&str>>();
return match values.len() {
3 | 4 => values.iter().all(|v| v.trim().parse::<f32>().is_ok()),
_ => false,
};
}
// hsl/hsla
if
(color.starts_with("hsl(") || color.starts_with("hsla(")) &&
color.ends_with(")") &&
!color[..color.len() - 1].contains(")")
{
let values = color
.trim_start_matches("hsla(")
.trim_start_matches("hsl(")
.trim_end_matches(')')
.split(',')
.collect::<Vec<&str>>();
return match values.len() {
3 | 4 => values.iter().all(|v| v.trim().parse::<f32>().is_ok()),
_ => false,
};
}
false
}
#[tauri::command]
pub async fn fetch_page_meta(url: String) -> Result<(String, Option<String>), String> {
let metadata = meta_fetcher
::fetch_metadata(&url)
.map_err(|e| format!("Failed to fetch metadata: {}", e))?;
Ok((metadata.title.unwrap_or_else(|| "No title found".to_string()), metadata.image))
}

View file

@ -1,23 +1,23 @@
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use image::ImageFormat;
use reqwest;
use url::Url;
pub async fn fetch_favicon_as_base64(
url: Url
) -> Result<Option<String>, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let favicon_url = format!("https://favicone.com/{}", url.host_str().unwrap());
let response = client.get(&favicon_url).send().await?;
if response.status().is_success() {
let bytes = response.bytes().await?;
let img = image::load_from_memory(&bytes)?;
let mut png_bytes: Vec<u8> = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut png_bytes), ImageFormat::Png)?;
Ok(Some(STANDARD.encode(&png_bytes)))
} else {
Ok(None)
}
}
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use image::ImageFormat;
use reqwest;
use url::Url;
pub async fn fetch_favicon_as_base64(
url: Url
) -> Result<Option<String>, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let favicon_url = format!("https://favicone.com/{}", url.host_str().unwrap());
let response = client.get(&favicon_url).send().await?;
if response.status().is_success() {
let bytes = response.bytes().await?;
let img = image::load_from_memory(&bytes)?;
let mut png_bytes: Vec<u8> = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut png_bytes), ImageFormat::Png)?;
Ok(Some(STANDARD.encode(&png_bytes)))
} else {
Ok(None)
}
}

View file

@ -1,120 +1,120 @@
use global_hotkey::hotkey::Code;
use std::str::FromStr;
pub struct KeyCode(Code);
impl FromStr for KeyCode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let code = match s {
"Backquote" => Code::Backquote,
"Backslash" => Code::Backslash,
"BracketLeft" => Code::BracketLeft,
"BracketRight" => Code::BracketRight,
"Comma" => Code::Comma,
"Digit0" => Code::Digit0,
"Digit1" => Code::Digit1,
"Digit2" => Code::Digit2,
"Digit3" => Code::Digit3,
"Digit4" => Code::Digit4,
"Digit5" => Code::Digit5,
"Digit6" => Code::Digit6,
"Digit7" => Code::Digit7,
"Digit8" => Code::Digit8,
"Digit9" => Code::Digit9,
"Equal" => Code::Equal,
"KeyA" => Code::KeyA,
"KeyB" => Code::KeyB,
"KeyC" => Code::KeyC,
"KeyD" => Code::KeyD,
"KeyE" => Code::KeyE,
"KeyF" => Code::KeyF,
"KeyG" => Code::KeyG,
"KeyH" => Code::KeyH,
"KeyI" => Code::KeyI,
"KeyJ" => Code::KeyJ,
"KeyK" => Code::KeyK,
"KeyL" => Code::KeyL,
"KeyM" => Code::KeyM,
"KeyN" => Code::KeyN,
"KeyO" => Code::KeyO,
"KeyP" => Code::KeyP,
"KeyQ" => Code::KeyQ,
"KeyR" => Code::KeyR,
"KeyS" => Code::KeyS,
"KeyT" => Code::KeyT,
"KeyU" => Code::KeyU,
"KeyV" => Code::KeyV,
"KeyW" => Code::KeyW,
"KeyX" => Code::KeyX,
"KeyY" => Code::KeyY,
"KeyZ" => Code::KeyZ,
"Minus" => Code::Minus,
"Period" => Code::Period,
"Quote" => Code::Quote,
"Semicolon" => Code::Semicolon,
"Slash" => Code::Slash,
"Backspace" => Code::Backspace,
"CapsLock" => Code::CapsLock,
"Delete" => Code::Delete,
"Enter" => Code::Enter,
"Space" => Code::Space,
"Tab" => Code::Tab,
"End" => Code::End,
"Home" => Code::Home,
"Insert" => Code::Insert,
"PageDown" => Code::PageDown,
"PageUp" => Code::PageUp,
"ArrowDown" => Code::ArrowDown,
"ArrowLeft" => Code::ArrowLeft,
"ArrowRight" => Code::ArrowRight,
"ArrowUp" => Code::ArrowUp,
"NumLock" => Code::NumLock,
"Numpad0" => Code::Numpad0,
"Numpad1" => Code::Numpad1,
"Numpad2" => Code::Numpad2,
"Numpad3" => Code::Numpad3,
"Numpad4" => Code::Numpad4,
"Numpad5" => Code::Numpad5,
"Numpad6" => Code::Numpad6,
"Numpad7" => Code::Numpad7,
"Numpad8" => Code::Numpad8,
"Numpad9" => Code::Numpad9,
"NumpadAdd" => Code::NumpadAdd,
"NumpadDecimal" => Code::NumpadDecimal,
"NumpadDivide" => Code::NumpadDivide,
"NumpadMultiply" => Code::NumpadMultiply,
"NumpadSubtract" => Code::NumpadSubtract,
"Escape" => Code::Escape,
"PrintScreen" => Code::PrintScreen,
"ScrollLock" => Code::ScrollLock,
"Pause" => Code::Pause,
"AudioVolumeDown" => Code::AudioVolumeDown,
"AudioVolumeMute" => Code::AudioVolumeMute,
"AudioVolumeUp" => Code::AudioVolumeUp,
"F1" => Code::F1,
"F2" => Code::F2,
"F3" => Code::F3,
"F4" => Code::F4,
"F5" => Code::F5,
"F6" => Code::F6,
"F7" => Code::F7,
"F8" => Code::F8,
"F9" => Code::F9,
"F10" => Code::F10,
"F11" => Code::F11,
"F12" => Code::F12,
_ => {
return Err(format!("Unknown key code: {}", s));
}
};
Ok(KeyCode(code))
}
}
impl From<KeyCode> for Code {
fn from(key_code: KeyCode) -> Self {
key_code.0
}
}
use global_hotkey::hotkey::Code;
use std::str::FromStr;
pub struct KeyCode(Code);
impl FromStr for KeyCode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let code = match s {
"Backquote" => Code::Backquote,
"Backslash" => Code::Backslash,
"BracketLeft" => Code::BracketLeft,
"BracketRight" => Code::BracketRight,
"Comma" => Code::Comma,
"Digit0" => Code::Digit0,
"Digit1" => Code::Digit1,
"Digit2" => Code::Digit2,
"Digit3" => Code::Digit3,
"Digit4" => Code::Digit4,
"Digit5" => Code::Digit5,
"Digit6" => Code::Digit6,
"Digit7" => Code::Digit7,
"Digit8" => Code::Digit8,
"Digit9" => Code::Digit9,
"Equal" => Code::Equal,
"KeyA" => Code::KeyA,
"KeyB" => Code::KeyB,
"KeyC" => Code::KeyC,
"KeyD" => Code::KeyD,
"KeyE" => Code::KeyE,
"KeyF" => Code::KeyF,
"KeyG" => Code::KeyG,
"KeyH" => Code::KeyH,
"KeyI" => Code::KeyI,
"KeyJ" => Code::KeyJ,
"KeyK" => Code::KeyK,
"KeyL" => Code::KeyL,
"KeyM" => Code::KeyM,
"KeyN" => Code::KeyN,
"KeyO" => Code::KeyO,
"KeyP" => Code::KeyP,
"KeyQ" => Code::KeyQ,
"KeyR" => Code::KeyR,
"KeyS" => Code::KeyS,
"KeyT" => Code::KeyT,
"KeyU" => Code::KeyU,
"KeyV" => Code::KeyV,
"KeyW" => Code::KeyW,
"KeyX" => Code::KeyX,
"KeyY" => Code::KeyY,
"KeyZ" => Code::KeyZ,
"Minus" => Code::Minus,
"Period" => Code::Period,
"Quote" => Code::Quote,
"Semicolon" => Code::Semicolon,
"Slash" => Code::Slash,
"Backspace" => Code::Backspace,
"CapsLock" => Code::CapsLock,
"Delete" => Code::Delete,
"Enter" => Code::Enter,
"Space" => Code::Space,
"Tab" => Code::Tab,
"End" => Code::End,
"Home" => Code::Home,
"Insert" => Code::Insert,
"PageDown" => Code::PageDown,
"PageUp" => Code::PageUp,
"ArrowDown" => Code::ArrowDown,
"ArrowLeft" => Code::ArrowLeft,
"ArrowRight" => Code::ArrowRight,
"ArrowUp" => Code::ArrowUp,
"NumLock" => Code::NumLock,
"Numpad0" => Code::Numpad0,
"Numpad1" => Code::Numpad1,
"Numpad2" => Code::Numpad2,
"Numpad3" => Code::Numpad3,
"Numpad4" => Code::Numpad4,
"Numpad5" => Code::Numpad5,
"Numpad6" => Code::Numpad6,
"Numpad7" => Code::Numpad7,
"Numpad8" => Code::Numpad8,
"Numpad9" => Code::Numpad9,
"NumpadAdd" => Code::NumpadAdd,
"NumpadDecimal" => Code::NumpadDecimal,
"NumpadDivide" => Code::NumpadDivide,
"NumpadMultiply" => Code::NumpadMultiply,
"NumpadSubtract" => Code::NumpadSubtract,
"Escape" => Code::Escape,
"PrintScreen" => Code::PrintScreen,
"ScrollLock" => Code::ScrollLock,
"Pause" => Code::Pause,
"AudioVolumeDown" => Code::AudioVolumeDown,
"AudioVolumeMute" => Code::AudioVolumeMute,
"AudioVolumeUp" => Code::AudioVolumeUp,
"F1" => Code::F1,
"F2" => Code::F2,
"F3" => Code::F3,
"F4" => Code::F4,
"F5" => Code::F5,
"F6" => Code::F6,
"F7" => Code::F7,
"F8" => Code::F8,
"F9" => Code::F9,
"F10" => Code::F10,
"F11" => Code::F11,
"F12" => Code::F12,
_ => {
return Err(format!("Unknown key code: {}", s));
}
};
Ok(KeyCode(code))
}
}
impl From<KeyCode> for Code {
fn from(key_code: KeyCode) -> Self {
key_code.0
}
}

View file

@ -1,84 +1,84 @@
use chrono;
use log::{ LevelFilter, SetLoggerError };
use std::fs::{ File, OpenOptions };
use std::io::Write;
use std::panic;
pub struct FileLogger {
file: File,
}
impl log::Log for FileLogger {
fn enabled(&self, _metadata: &log::Metadata) -> bool {
true
}
fn log(&self, record: &log::Record) {
if self.enabled(record.metadata()) {
let mut file = self.file.try_clone().expect("Failed to clone file handle");
writeln!(
file,
"{} [{:<5}] {}: {} ({}:{})",
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
record.level(),
record.target(),
record.args(),
record.file().unwrap_or("unknown"),
record.line().unwrap_or(0)
).expect("Failed to write to log file");
}
}
fn flush(&self) {
self.file.sync_all().expect("Failed to flush log file");
}
}
pub fn init_logger(app_data_dir: &std::path::Path) -> Result<(), SetLoggerError> {
let logs_dir = app_data_dir.join("logs");
std::fs::create_dir_all(&logs_dir).expect("Failed to create logs directory");
let log_path = logs_dir.join("app.log");
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.expect("Failed to open log file");
let panic_file = file.try_clone().expect("Failed to clone file handle");
panic::set_hook(
Box::new(move |panic_info| {
let mut file = panic_file.try_clone().expect("Failed to clone file handle");
let location = panic_info
.location()
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
.unwrap_or_else(|| "unknown location".to_string());
let message = match panic_info.payload().downcast_ref::<&str>() {
Some(s) => *s,
None =>
match panic_info.payload().downcast_ref::<String>() {
Some(s) => s.as_str(),
None => "Unknown panic message",
}
};
let _ = writeln!(
file,
"{} [PANIC] rust_panic: {} ({})",
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
message,
location
);
})
);
let logger = Box::new(FileLogger { file });
unsafe {
log::set_logger_racy(Box::leak(logger))?;
}
log::set_max_level(LevelFilter::Debug);
Ok(())
}
use chrono;
use log::{ LevelFilter, SetLoggerError };
use std::fs::{ File, OpenOptions };
use std::io::Write;
use std::panic;
pub struct FileLogger {
file: File,
}
impl log::Log for FileLogger {
fn enabled(&self, _metadata: &log::Metadata) -> bool {
true
}
fn log(&self, record: &log::Record) {
if self.enabled(record.metadata()) {
let mut file = self.file.try_clone().expect("Failed to clone file handle");
writeln!(
file,
"{} [{:<5}] {}: {} ({}:{})",
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
record.level(),
record.target(),
record.args(),
record.file().unwrap_or("unknown"),
record.line().unwrap_or(0)
).expect("Failed to write to log file");
}
}
fn flush(&self) {
self.file.sync_all().expect("Failed to flush log file");
}
}
pub fn init_logger(app_data_dir: &std::path::Path) -> Result<(), SetLoggerError> {
let logs_dir = app_data_dir.join("logs");
std::fs::create_dir_all(&logs_dir).expect("Failed to create logs directory");
let log_path = logs_dir.join("app.log");
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.expect("Failed to open log file");
let panic_file = file.try_clone().expect("Failed to clone file handle");
panic::set_hook(
Box::new(move |panic_info| {
let mut file = panic_file.try_clone().expect("Failed to clone file handle");
let location = panic_info
.location()
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
.unwrap_or_else(|| "unknown location".to_string());
let message = match panic_info.payload().downcast_ref::<&str>() {
Some(s) => *s,
None =>
match panic_info.payload().downcast_ref::<String>() {
Some(s) => s.as_str(),
None => "Unknown panic message",
}
};
let _ = writeln!(
file,
"{} [PANIC] rust_panic: {} ({})",
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
message,
location
);
})
);
let logger = Box::new(FileLogger { file });
unsafe {
log::set_logger_racy(Box::leak(logger))?;
}
log::set_max_level(LevelFilter::Debug);
Ok(())
}

View file

@ -1,5 +1,5 @@
pub mod commands;
pub mod favicon;
pub mod types;
pub mod logger;
pub mod keys;
pub mod commands;
pub mod favicon;
pub mod types;
pub mod logger;
pub mod keys;

View file

@ -1,155 +1,155 @@
use chrono::{ DateTime, Utc };
use serde::{ Deserialize, Serialize };
use std::fmt;
use uuid::Uuid;
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct HistoryItem {
pub id: String,
pub source: String,
pub source_icon: Option<String>,
pub content_type: ContentType,
pub content: String,
pub favicon: Option<String>,
pub timestamp: DateTime<Utc>,
pub language: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ContentType {
Text,
Image,
File,
Link,
Color,
Code,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct InfoText {
pub source: String,
pub content_type: ContentType,
pub characters: i32,
pub words: i32,
pub copied: DateTime<Utc>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct InfoImage {
pub source: String,
pub content_type: ContentType,
pub dimensions: String,
pub size: i64,
pub copied: DateTime<Utc>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct InfoFile {
pub source: String,
pub content_type: ContentType,
pub path: String,
pub filesize: i64,
pub copied: DateTime<Utc>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct InfoLink {
pub source: String,
pub content_type: ContentType,
pub title: Option<String>,
pub url: String,
pub characters: i32,
pub copied: DateTime<Utc>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct InfoColor {
pub source: String,
pub content_type: ContentType,
pub hex: String,
pub rgb: String,
pub copied: DateTime<Utc>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct InfoCode {
pub source: String,
pub content_type: ContentType,
pub language: String,
pub lines: i32,
pub copied: DateTime<Utc>,
}
impl fmt::Display for ContentType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ContentType::Text => write!(f, "text"),
ContentType::Image => write!(f, "image"),
ContentType::File => write!(f, "file"),
ContentType::Link => write!(f, "link"),
ContentType::Color => write!(f, "color"),
ContentType::Code => write!(f, "code"),
}
}
}
impl From<String> for ContentType {
fn from(s: String) -> Self {
match s.to_lowercase().as_str() {
"text" => ContentType::Text,
"image" => ContentType::Image,
"file" => ContentType::File,
"link" => ContentType::Link,
"color" => ContentType::Color,
"code" => ContentType::Code,
_ => ContentType::Text,
}
}
}
impl HistoryItem {
pub fn new(
source: String,
content_type: ContentType,
content: String,
favicon: Option<String>,
source_icon: Option<String>,
language: Option<String>
) -> Self {
Self {
id: Uuid::new_v4().to_string(),
source,
source_icon,
content_type,
content,
favicon,
timestamp: Utc::now(),
language,
}
}
pub fn to_row(
&self
) -> (
String,
String,
Option<String>,
String,
String,
Option<String>,
DateTime<Utc>,
Option<String>,
) {
(
self.id.clone(),
self.source.clone(),
self.source_icon.clone(),
self.content_type.to_string(),
self.content.clone(),
self.favicon.clone(),
self.timestamp,
self.language.clone(),
)
}
}
use chrono::{ DateTime, Utc };
use serde::{ Deserialize, Serialize };
use std::fmt;
use uuid::Uuid;
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct HistoryItem {
pub id: String,
pub source: String,
pub source_icon: Option<String>,
pub content_type: ContentType,
pub content: String,
pub favicon: Option<String>,
pub timestamp: DateTime<Utc>,
pub language: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ContentType {
Text,
Image,
File,
Link,
Color,
Code,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct InfoText {
pub source: String,
pub content_type: ContentType,
pub characters: i32,
pub words: i32,
pub copied: DateTime<Utc>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct InfoImage {
pub source: String,
pub content_type: ContentType,
pub dimensions: String,
pub size: i64,
pub copied: DateTime<Utc>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct InfoFile {
pub source: String,
pub content_type: ContentType,
pub path: String,
pub filesize: i64,
pub copied: DateTime<Utc>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct InfoLink {
pub source: String,
pub content_type: ContentType,
pub title: Option<String>,
pub url: String,
pub characters: i32,
pub copied: DateTime<Utc>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct InfoColor {
pub source: String,
pub content_type: ContentType,
pub hex: String,
pub rgb: String,
pub copied: DateTime<Utc>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct InfoCode {
pub source: String,
pub content_type: ContentType,
pub language: String,
pub lines: i32,
pub copied: DateTime<Utc>,
}
impl fmt::Display for ContentType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ContentType::Text => write!(f, "text"),
ContentType::Image => write!(f, "image"),
ContentType::File => write!(f, "file"),
ContentType::Link => write!(f, "link"),
ContentType::Color => write!(f, "color"),
ContentType::Code => write!(f, "code"),
}
}
}
impl From<String> for ContentType {
fn from(s: String) -> Self {
match s.to_lowercase().as_str() {
"text" => ContentType::Text,
"image" => ContentType::Image,
"file" => ContentType::File,
"link" => ContentType::Link,
"color" => ContentType::Color,
"code" => ContentType::Code,
_ => ContentType::Text,
}
}
}
impl HistoryItem {
pub fn new(
source: String,
content_type: ContentType,
content: String,
favicon: Option<String>,
source_icon: Option<String>,
language: Option<String>
) -> Self {
Self {
id: Uuid::new_v4().to_string(),
source,
source_icon,
content_type,
content,
favicon,
timestamp: Utc::now(),
language,
}
}
pub fn to_row(
&self
) -> (
String,
String,
Option<String>,
String,
String,
Option<String>,
DateTime<Utc>,
Option<String>,
) {
(
self.id.clone(),
self.source.clone(),
self.source_icon.clone(),
self.content_type.to_string(),
self.content.clone(),
self.favicon.clone(),
self.timestamp,
self.language.clone(),
)
}
}

View file

@ -1,58 +1,58 @@
{
"productName": "Qopy",
"version": "0.4.0",
"identifier": "net.pandadev.qopy",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:3000",
"beforeDevCommand": "pnpm nuxt dev",
"beforeBuildCommand": "pnpm nuxt generate"
},
"app": {
"windows": [
{
"title": "Qopy",
"titleBarStyle": "Overlay",
"fullscreen": false,
"resizable": false,
"height": 474,
"width": 750,
"minHeight": 474,
"maxHeight": 474,
"minWidth": 750,
"maxWidth": 750,
"decorations": false,
"center": true,
"shadow": false,
"transparent": true,
"skipTaskbar": true,
"alwaysOnTop": true
}
],
"security": {
"csp": null
},
"withGlobalTauri": true,
"macOSPrivateApi": true
},
"bundle": {
"createUpdaterArtifacts": true,
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"category": "DeveloperTool"
},
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNDIzNjA1QjE0NjU1OTkKUldTWlZVYXhCVFpDRWNvNmt0UE5lQmZkblEyZGZiZ2tHelJvT2YvNVpLU1RIM1RKZFQrb2tzWWwK",
"endpoints": ["https://qopy.pandadev.net/"]
}
},
"$schema": "../node_modules/@tauri-apps/cli/schema.json"
}
{
"productName": "Qopy",
"version": "0.4.0",
"identifier": "net.pandadev.qopy",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:3000",
"beforeDevCommand": "bun nuxt dev",
"beforeBuildCommand": "bun nuxt generate"
},
"app": {
"windows": [
{
"title": "Qopy",
"titleBarStyle": "Overlay",
"fullscreen": false,
"resizable": false,
"height": 474,
"width": 750,
"minHeight": 474,
"maxHeight": 474,
"minWidth": 750,
"maxWidth": 750,
"decorations": false,
"center": true,
"shadow": false,
"transparent": true,
"skipTaskbar": true,
"alwaysOnTop": true
}
],
"security": {
"csp": null
},
"withGlobalTauri": true,
"macOSPrivateApi": true
},
"bundle": {
"createUpdaterArtifacts": true,
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"category": "DeveloperTool"
},
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNDIzNjA1QjE0NjU1OTkKUldTWlZVYXhCVFpDRWNvNmt0UE5lQmZkblEyZGZiZ2tHelJvT2YvNVpLU1RIM1RKZFQrb2tzWWwK",
"endpoints": ["https://qopy.pandadev.net/"]
}
},
"$schema": "../node_modules/@tauri-apps/cli/schema.json"
}

View file

@ -1,185 +1,185 @@
$primary: #2e2d2b;
$accent: #feb453;
$divider: #ffffff0d;
$text: #e5dfd5;
$text2: #ada9a1;
$mutedtext: #78756f;
$search-height: 56px;
$sidebar-width: 286px;
$bottom-bar-height: 39px;
$info-panel-height: 160px;
$content-view-height: calc(
100% - $search-height - $info-panel-height - $bottom-bar-height
);
main {
width: 100vw;
height: 100vh;
background-color: $primary;
border: 1px solid $divider;
display: flex;
flex-direction: column;
border-radius: 12px;
justify-content: space-between;
}
.container {
height: 376px;
width: 100%;
display: flex;
}
.results {
display: flex;
flex-direction: column;
padding: 14px 8px;
gap: 8px;
min-width: 286px;
border-right: 1px solid var(--border);
.time-separator {
font-size: 12px;
color: $text2;
font-family: SFRoundedSemiBold;
padding-left: 8px;
}
.group {
& + .group {
margin-top: 16px;
}
.time-separator {
margin-bottom: 8px;
}
.results-group {
display: flex;
flex-direction: column;
}
}
.favicon,
.image,
.icon {
width: 18px;
height: 18px;
}
}
.right {
display: flex;
flex-direction: column;
width: 100%;
}
.content {
height: 100%;
font-family: CommitMono !important;
font-size: 12px;
letter-spacing: 1;
border-radius: 10px;
width: 462px;
display: flex;
flex-direction: column;
align-items: flex-start;
overflow: hidden;
z-index: 2;
color: $text;
&:not(:has(.image)) {
padding: 8px;
}
span.content-text {
font-family: CommitMono !important;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-word;
max-width: 100%;
}
.image {
width: 100%;
height: 100%;
object-fit: contain;
object-position: center;
}
}
.information {
min-height: 160px;
width: 462px;
border-top: 1px solid $divider;
padding: 14px;
z-index: 1;
display: flex;
flex-direction: column;
gap: 14px;
.title {
font-family: SFRoundedSemiBold;
font-size: 12px;
letter-spacing: 0.6px;
color: $text;
}
.info-content {
display: flex;
gap: 0;
flex-direction: column;
.info-row {
display: flex;
width: 100%;
font-size: 12px;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid $divider;
&:last-child {
border-bottom: none;
padding-bottom: 0;
}
&:first-child {
padding-top: 14px;
}
p {
font-family: SFRoundedMedium;
color: $text2;
font-weight: 500;
flex-shrink: 0;
}
span {
font-family: CommitMono;
color: $text;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
margin-left: 32px;
display: flex;
align-items: center;
gap: 4px;
img {
width: 13px;
height: 13px;
object-fit: contain;
}
}
}
}
}
.search-loading {
display: flex;
justify-content: center;
padding: 20px;
font-size: 16px;
color: var(--text-secondary);
$primary: #2e2d2b;
$accent: #feb453;
$divider: #ffffff0d;
$text: #e5dfd5;
$text2: #ada9a1;
$mutedtext: #78756f;
$search-height: 56px;
$sidebar-width: 286px;
$bottom-bar-height: 39px;
$info-panel-height: 160px;
$content-view-height: calc(
100% - $search-height - $info-panel-height - $bottom-bar-height
);
main {
width: 100vw;
height: 100vh;
background-color: $primary;
border: 1px solid $divider;
display: flex;
flex-direction: column;
border-radius: 12px;
justify-content: space-between;
}
.container {
height: 376px;
width: 100%;
display: flex;
}
.results {
display: flex;
flex-direction: column;
padding: 14px 8px;
gap: 8px;
min-width: 286px;
border-right: 1px solid var(--border);
.time-separator {
font-size: 12px;
color: $text2;
font-family: SFRoundedSemiBold;
padding-left: 8px;
}
.group {
& + .group {
margin-top: 16px;
}
.time-separator {
margin-bottom: 8px;
}
.results-group {
display: flex;
flex-direction: column;
}
}
.favicon,
.image,
.icon {
width: 18px;
height: 18px;
}
}
.right {
display: flex;
flex-direction: column;
width: 100%;
}
.content {
height: 100%;
font-family: CommitMono !important;
font-size: 12px;
letter-spacing: 1;
border-radius: 10px;
width: 462px;
display: flex;
flex-direction: column;
align-items: flex-start;
overflow: hidden;
z-index: 2;
color: $text;
&:not(:has(.image)) {
padding: 8px;
}
span.content-text {
font-family: CommitMono !important;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-word;
max-width: 100%;
}
.image {
width: 100%;
height: 100%;
object-fit: contain;
object-position: center;
}
}
.information {
min-height: 160px;
width: 462px;
border-top: 1px solid $divider;
padding: 14px;
z-index: 1;
display: flex;
flex-direction: column;
gap: 14px;
.title {
font-family: SFRoundedSemiBold;
font-size: 12px;
letter-spacing: 0.6px;
color: $text;
}
.info-content {
display: flex;
gap: 0;
flex-direction: column;
.info-row {
display: flex;
width: 100%;
font-size: 12px;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid $divider;
&:last-child {
border-bottom: none;
padding-bottom: 0;
}
&:first-child {
padding-top: 14px;
}
p {
font-family: SFRoundedMedium;
color: $text2;
font-weight: 500;
flex-shrink: 0;
}
span {
font-family: CommitMono;
color: $text;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
margin-left: 32px;
display: flex;
align-items: center;
gap: 4px;
img {
width: 13px;
height: 13px;
object-fit: contain;
}
}
}
}
}
.search-loading {
display: flex;
justify-content: center;
padding: 20px;
font-size: 16px;
color: var(--text-secondary);
}

View file

@ -1,235 +1,235 @@
$primary: #2e2d2b;
$accent: #feb453;
$divider: #ffffff0d;
$text: #e5dfd5;
$text2: #ada9a1;
$mutedtext: #78756f;
main {
width: 100vw;
height: 100vh;
background-color: $primary;
border: 1px solid $divider;
display: flex;
flex-direction: column;
border-radius: 12px;
justify-content: space-between;
}
.back {
position: absolute;
top: 16px;
left: 16px;
display: flex;
gap: 8px;
align-items: center;
img {
background-color: $divider;
border-radius: 6px;
padding: 8px 6px;
}
p {
color: $text2;
}
}
p {
font-family: SFRoundedMedium;
}
.settings-container {
width: 100%;
height: 100%;
margin-top: 26px;
position: relative;
font-size: 12px;
font-family: SFRoundedMedium;
.settings {
position: absolute;
left: 50%;
transform: translateX(-50%);
margin-left: -26px;
display: flex;
gap: 24px;
.names {
display: flex;
flex-direction: column;
gap: 16px;
p {
font-family: SFRoundedSemiBold;
color: $text2;
display: flex;
justify-content: right;
}
}
.actions {
display: flex;
flex-direction: column;
gap: 16px;
color: $mutedtext;
}
}
}
.launch {
display: flex;
align-items: center;
gap: 6px;
input[type="checkbox"] {
appearance: none;
width: 14px;
height: 14px;
background-color: transparent;
border-radius: 5px;
border: 1px solid $mutedtext;
position: relative;
cursor: pointer;
transition: background-color 0.2s;
&:checked {
~ .checkmark {
opacity: 1;
}
}
}
.checkmark {
height: 14px;
width: 14px;
position: absolute;
opacity: 0;
transition: opacity 0.2s;
}
p {
color: $text2;
}
}
.keybind-input {
width: min-content;
white-space: nowrap;
padding: 6px;
border: 1px solid $divider;
color: $text2;
display: flex;
border-radius: 10px;
outline: none;
gap: 4px;
.key {
color: $text2;
font-family: SFRoundedMedium;
background-color: $divider;
padding: 2px 6px;
border-radius: 6px;
font-size: 14px;
}
}
.keybind-input:focus {
border: 1px solid rgba(255, 255, 255, 0.2);
}
.empty-keybind {
border-color: rgba(255, 82, 82, 0.298);
}
.top-bar {
width: 100%;
min-height: 56px;
border-bottom: 1px solid $divider;
}
.bottom-bar {
height: 40px;
width: calc(100vw - 2px);
backdrop-filter: blur(18px);
background-color: hsla(40, 3%, 16%, 0.8);
position: fixed;
bottom: 1px;
left: 1px;
z-index: 100;
border-radius: 0 0 12px 12px;
display: flex;
flex-direction: row;
justify-content: space-between;
padding-inline: 12px;
padding-right: 6px;
padding-top: 1px;
align-items: center;
font-size: 14px;
border-top: 1px solid $divider;
p {
color: $text2;
}
.left {
display: flex;
align-items: center;
gap: 8px;
.logo {
width: 18px;
height: 18px;
}
}
.right {
display: flex;
align-items: center;
.actions div {
display: flex;
align-items: center;
gap: 2px;
}
.divider {
width: 2px;
height: 12px;
background-color: $divider;
margin-left: 8px;
margin-right: 4px;
transition: all 0.2s;
}
.actions {
padding: 4px;
padding-left: 8px;
display: flex;
align-items: center;
gap: 8px;
border-radius: 7px;
background-color: transparent;
transition: all 0.2s;
cursor: pointer;
p {
color: $text;
}
&.disabled {
pointer-events: none;
opacity: 0.5;
}
}
.actions:hover {
background-color: $divider;
}
&:hover .actions:hover ~ .divider {
opacity: 0;
}
}
}
$primary: #2e2d2b;
$accent: #feb453;
$divider: #ffffff0d;
$text: #e5dfd5;
$text2: #ada9a1;
$mutedtext: #78756f;
main {
width: 100vw;
height: 100vh;
background-color: $primary;
border: 1px solid $divider;
display: flex;
flex-direction: column;
border-radius: 12px;
justify-content: space-between;
}
.back {
position: absolute;
top: 16px;
left: 16px;
display: flex;
gap: 8px;
align-items: center;
img {
background-color: $divider;
border-radius: 6px;
padding: 8px 6px;
}
p {
color: $text2;
}
}
p {
font-family: SFRoundedMedium;
}
.settings-container {
width: 100%;
height: 100%;
margin-top: 26px;
position: relative;
font-size: 12px;
font-family: SFRoundedMedium;
.settings {
position: absolute;
left: 50%;
transform: translateX(-50%);
margin-left: -26px;
display: flex;
gap: 24px;
.names {
display: flex;
flex-direction: column;
gap: 16px;
p {
font-family: SFRoundedSemiBold;
color: $text2;
display: flex;
justify-content: right;
}
}
.actions {
display: flex;
flex-direction: column;
gap: 16px;
color: $mutedtext;
}
}
}
.launch {
display: flex;
align-items: center;
gap: 6px;
input[type="checkbox"] {
appearance: none;
width: 14px;
height: 14px;
background-color: transparent;
border-radius: 5px;
border: 1px solid $mutedtext;
position: relative;
cursor: pointer;
transition: background-color 0.2s;
&:checked {
~ .checkmark {
opacity: 1;
}
}
}
.checkmark {
height: 14px;
width: 14px;
position: absolute;
opacity: 0;
transition: opacity 0.2s;
}
p {
color: $text2;
}
}
.keybind-input {
width: min-content;
white-space: nowrap;
padding: 6px;
border: 1px solid $divider;
color: $text2;
display: flex;
border-radius: 10px;
outline: none;
gap: 4px;
.key {
color: $text2;
font-family: SFRoundedMedium;
background-color: $divider;
padding: 2px 6px;
border-radius: 6px;
font-size: 14px;
}
}
.keybind-input:focus {
border: 1px solid rgba(255, 255, 255, 0.2);
}
.empty-keybind {
border-color: rgba(255, 82, 82, 0.298);
}
.top-bar {
width: 100%;
min-height: 56px;
border-bottom: 1px solid $divider;
}
.bottom-bar {
height: 40px;
width: calc(100vw - 2px);
backdrop-filter: blur(18px);
background-color: hsla(40, 3%, 16%, 0.8);
position: fixed;
bottom: 1px;
left: 1px;
z-index: 100;
border-radius: 0 0 12px 12px;
display: flex;
flex-direction: row;
justify-content: space-between;
padding-inline: 12px;
padding-right: 6px;
padding-top: 1px;
align-items: center;
font-size: 14px;
border-top: 1px solid $divider;
p {
color: $text2;
}
.left {
display: flex;
align-items: center;
gap: 8px;
.logo {
width: 18px;
height: 18px;
}
}
.right {
display: flex;
align-items: center;
.actions div {
display: flex;
align-items: center;
gap: 2px;
}
.divider {
width: 2px;
height: 12px;
background-color: $divider;
margin-left: 8px;
margin-right: 4px;
transition: all 0.2s;
}
.actions {
padding: 4px;
padding-left: 8px;
display: flex;
align-items: center;
gap: 8px;
border-radius: 7px;
background-color: transparent;
transition: all 0.2s;
cursor: pointer;
p {
color: $text;
}
&.disabled {
pointer-events: none;
opacity: 0.5;
}
}
.actions:hover {
background-color: $divider;
}
&:hover .actions:hover ~ .divider {
opacity: 0;
}
}
}

View file

@ -1,4 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

52
types/keyboard.d.ts vendored
View file

@ -1,27 +1,27 @@
import type { Key as WaraduKey, useKeyboard } from '@waradu/keyboard';
declare module '#app' {
interface NuxtApp {
$keyboard: {
listen: ReturnType<typeof useKeyboard>['listen'];
init: ReturnType<typeof useKeyboard>['init'];
Key: typeof WaraduKey;
currentOS: string;
clearAll: () => void;
};
}
}
declare module 'vue' {
interface ComponentCustomProperties {
$keyboard: {
listen: ReturnType<typeof useKeyboard>['listen'];
init: ReturnType<typeof useKeyboard>['init'];
Key: typeof WaraduKey;
currentOS: string;
clearAll: () => void;
};
}
}
import type { Key as WaraduKey, useKeyboard } from '@waradu/keyboard';
declare module '#app' {
interface NuxtApp {
$keyboard: {
listen: ReturnType<typeof useKeyboard>['listen'];
init: ReturnType<typeof useKeyboard>['init'];
Key: typeof WaraduKey;
currentOS: string;
clearAll: () => void;
};
}
}
declare module 'vue' {
interface ComponentCustomProperties {
$keyboard: {
listen: ReturnType<typeof useKeyboard>['listen'];
init: ReturnType<typeof useKeyboard>['init'];
Key: typeof WaraduKey;
currentOS: string;
clearAll: () => void;
};
}
}
export {};

View file

@ -1,217 +1,217 @@
export enum KeyValues {
Backquote = 'Backquote',
Backslash = 'Backslash',
BracketLeft = 'BracketLeft',
BracketRight = 'BracketRight',
Comma = 'Comma',
Digit0 = 'Digit0',
Digit1 = 'Digit1',
Digit2 = 'Digit2',
Digit3 = 'Digit3',
Digit4 = 'Digit4',
Digit5 = 'Digit5',
Digit6 = 'Digit6',
Digit7 = 'Digit7',
Digit8 = 'Digit8',
Digit9 = 'Digit9',
Equal = 'Equal',
KeyA = 'KeyA',
KeyB = 'KeyB',
KeyC = 'KeyC',
KeyD = 'KeyD',
KeyE = 'KeyE',
KeyF = 'KeyF',
KeyG = 'KeyG',
KeyH = 'KeyH',
KeyI = 'KeyI',
KeyJ = 'KeyJ',
KeyK = 'KeyK',
KeyL = 'KeyL',
KeyM = 'KeyM',
KeyN = 'KeyN',
KeyO = 'KeyO',
KeyP = 'KeyP',
KeyQ = 'KeyQ',
KeyR = 'KeyR',
KeyS = 'KeyS',
KeyT = 'KeyT',
KeyU = 'KeyU',
KeyV = 'KeyV',
KeyW = 'KeyW',
KeyX = 'KeyX',
KeyY = 'KeyY',
KeyZ = 'KeyZ',
Minus = 'Minus',
Period = 'Period',
Quote = 'Quote',
Semicolon = 'Semicolon',
Slash = 'Slash',
AltLeft = 'AltLeft',
AltRight = 'AltRight',
Backspace = 'Backspace',
CapsLock = 'CapsLock',
ContextMenu = 'ContextMenu',
ControlLeft = 'ControlLeft',
ControlRight = 'ControlRight',
Enter = 'Enter',
MetaLeft = 'MetaLeft',
MetaRight = 'MetaRight',
ShiftLeft = 'ShiftLeft',
ShiftRight = 'ShiftRight',
Space = 'Space',
Tab = 'Tab',
Delete = 'Delete',
End = 'End',
Home = 'Home',
Insert = 'Insert',
PageDown = 'PageDown',
PageUp = 'PageUp',
ArrowDown = 'ArrowDown',
ArrowLeft = 'ArrowLeft',
ArrowRight = 'ArrowRight',
ArrowUp = 'ArrowUp',
NumLock = 'NumLock',
Numpad0 = 'Numpad0',
Numpad1 = 'Numpad1',
Numpad2 = 'Numpad2',
Numpad3 = 'Numpad3',
Numpad4 = 'Numpad4',
Numpad5 = 'Numpad5',
Numpad6 = 'Numpad6',
Numpad7 = 'Numpad7',
Numpad8 = 'Numpad8',
Numpad9 = 'Numpad9',
NumpadAdd = 'NumpadAdd',
NumpadDecimal = 'NumpadDecimal',
NumpadDivide = 'NumpadDivide',
NumpadMultiply = 'NumpadMultiply',
NumpadSubtract = 'NumpadSubtract',
Escape = 'Escape',
PrintScreen = 'PrintScreen',
ScrollLock = 'ScrollLock',
Pause = 'Pause',
AudioVolumeDown = 'AudioVolumeDown',
AudioVolumeMute = 'AudioVolumeMute',
AudioVolumeUp = 'AudioVolumeUp',
F1 = 'F1',
F2 = 'F2',
F3 = 'F3',
F4 = 'F4',
F5 = 'F5',
F6 = 'F6',
F7 = 'F7',
F8 = 'F8',
F9 = 'F9',
F10 = 'F10',
F11 = 'F11',
F12 = 'F12',
}
export enum KeyLabels {
Backquote = '`',
Backslash = '\\',
BracketLeft = '[',
BracketRight = ']',
Comma = ',',
Digit0 = '0',
Digit1 = '1',
Digit2 = '2',
Digit3 = '3',
Digit4 = '4',
Digit5 = '5',
Digit6 = '6',
Digit7 = '7',
Digit8 = '8',
Digit9 = '9',
Equal = '=',
KeyA = 'A',
KeyB = 'B',
KeyC = 'C',
KeyD = 'D',
KeyE = 'E',
KeyF = 'F',
KeyG = 'G',
KeyH = 'H',
KeyI = 'I',
KeyJ = 'J',
KeyK = 'K',
KeyL = 'L',
KeyM = 'M',
KeyN = 'N',
KeyO = 'O',
KeyP = 'P',
KeyQ = 'Q',
KeyR = 'R',
KeyS = 'S',
KeyT = 'T',
KeyU = 'U',
KeyV = 'V',
KeyW = 'W',
KeyX = 'X',
KeyY = 'Y',
KeyZ = 'Z',
Minus = '-',
Period = '.',
Quote = "'",
Semicolon = ';',
Slash = '/',
AltLeft = 'Alt',
AltRight = 'Alt (Right)',
Backspace = 'Backspace',
CapsLock = 'Caps Lock',
ContextMenu = 'Context Menu',
ControlLeft = 'Ctrl',
ControlRight = 'Ctrl (Right)',
Enter = 'Enter',
MetaLeft = 'Meta',
MetaRight = 'Meta (Right)',
ShiftLeft = 'Shift',
ShiftRight = 'Shift (Right)',
Space = 'Space',
Tab = 'Tab',
Delete = 'Delete',
End = 'End',
Home = 'Home',
Insert = 'Insert',
PageDown = 'Page Down',
PageUp = 'Page Up',
ArrowDown = '↓',
ArrowLeft = '←',
ArrowRight = '→',
ArrowUp = '↑',
NumLock = 'Num Lock',
Numpad0 = 'Numpad 0',
Numpad1 = 'Numpad 1',
Numpad2 = 'Numpad 2',
Numpad3 = 'Numpad 3',
Numpad4 = 'Numpad 4',
Numpad5 = 'Numpad 5',
Numpad6 = 'Numpad 6',
Numpad7 = 'Numpad 7',
Numpad8 = 'Numpad 8',
Numpad9 = 'Numpad 9',
NumpadAdd = 'Numpad +',
NumpadDecimal = 'Numpad .',
NumpadDivide = 'Numpad /',
NumpadMultiply = 'Numpad *',
NumpadSubtract = 'Numpad -',
Escape = 'Esc',
PrintScreen = 'Print Screen',
ScrollLock = 'Scroll Lock',
Pause = 'Pause',
AudioVolumeDown = 'Volume Down',
AudioVolumeMute = 'Volume Mute',
AudioVolumeUp = 'Volume Up',
F1 = 'F1',
F2 = 'F2',
F3 = 'F3',
F4 = 'F4',
F5 = 'F5',
F6 = 'F6',
F7 = 'F7',
F8 = 'F8',
F9 = 'F9',
F10 = 'F10',
F11 = 'F11',
F12 = 'F12',
export enum KeyValues {
Backquote = 'Backquote',
Backslash = 'Backslash',
BracketLeft = 'BracketLeft',
BracketRight = 'BracketRight',
Comma = 'Comma',
Digit0 = 'Digit0',
Digit1 = 'Digit1',
Digit2 = 'Digit2',
Digit3 = 'Digit3',
Digit4 = 'Digit4',
Digit5 = 'Digit5',
Digit6 = 'Digit6',
Digit7 = 'Digit7',
Digit8 = 'Digit8',
Digit9 = 'Digit9',
Equal = 'Equal',
KeyA = 'KeyA',
KeyB = 'KeyB',
KeyC = 'KeyC',
KeyD = 'KeyD',
KeyE = 'KeyE',
KeyF = 'KeyF',
KeyG = 'KeyG',
KeyH = 'KeyH',
KeyI = 'KeyI',
KeyJ = 'KeyJ',
KeyK = 'KeyK',
KeyL = 'KeyL',
KeyM = 'KeyM',
KeyN = 'KeyN',
KeyO = 'KeyO',
KeyP = 'KeyP',
KeyQ = 'KeyQ',
KeyR = 'KeyR',
KeyS = 'KeyS',
KeyT = 'KeyT',
KeyU = 'KeyU',
KeyV = 'KeyV',
KeyW = 'KeyW',
KeyX = 'KeyX',
KeyY = 'KeyY',
KeyZ = 'KeyZ',
Minus = 'Minus',
Period = 'Period',
Quote = 'Quote',
Semicolon = 'Semicolon',
Slash = 'Slash',
AltLeft = 'AltLeft',
AltRight = 'AltRight',
Backspace = 'Backspace',
CapsLock = 'CapsLock',
ContextMenu = 'ContextMenu',
ControlLeft = 'ControlLeft',
ControlRight = 'ControlRight',
Enter = 'Enter',
MetaLeft = 'MetaLeft',
MetaRight = 'MetaRight',
ShiftLeft = 'ShiftLeft',
ShiftRight = 'ShiftRight',
Space = 'Space',
Tab = 'Tab',
Delete = 'Delete',
End = 'End',
Home = 'Home',
Insert = 'Insert',
PageDown = 'PageDown',
PageUp = 'PageUp',
ArrowDown = 'ArrowDown',
ArrowLeft = 'ArrowLeft',
ArrowRight = 'ArrowRight',
ArrowUp = 'ArrowUp',
NumLock = 'NumLock',
Numpad0 = 'Numpad0',
Numpad1 = 'Numpad1',
Numpad2 = 'Numpad2',
Numpad3 = 'Numpad3',
Numpad4 = 'Numpad4',
Numpad5 = 'Numpad5',
Numpad6 = 'Numpad6',
Numpad7 = 'Numpad7',
Numpad8 = 'Numpad8',
Numpad9 = 'Numpad9',
NumpadAdd = 'NumpadAdd',
NumpadDecimal = 'NumpadDecimal',
NumpadDivide = 'NumpadDivide',
NumpadMultiply = 'NumpadMultiply',
NumpadSubtract = 'NumpadSubtract',
Escape = 'Escape',
PrintScreen = 'PrintScreen',
ScrollLock = 'ScrollLock',
Pause = 'Pause',
AudioVolumeDown = 'AudioVolumeDown',
AudioVolumeMute = 'AudioVolumeMute',
AudioVolumeUp = 'AudioVolumeUp',
F1 = 'F1',
F2 = 'F2',
F3 = 'F3',
F4 = 'F4',
F5 = 'F5',
F6 = 'F6',
F7 = 'F7',
F8 = 'F8',
F9 = 'F9',
F10 = 'F10',
F11 = 'F11',
F12 = 'F12',
}
export enum KeyLabels {
Backquote = '`',
Backslash = '\\',
BracketLeft = '[',
BracketRight = ']',
Comma = ',',
Digit0 = '0',
Digit1 = '1',
Digit2 = '2',
Digit3 = '3',
Digit4 = '4',
Digit5 = '5',
Digit6 = '6',
Digit7 = '7',
Digit8 = '8',
Digit9 = '9',
Equal = '=',
KeyA = 'A',
KeyB = 'B',
KeyC = 'C',
KeyD = 'D',
KeyE = 'E',
KeyF = 'F',
KeyG = 'G',
KeyH = 'H',
KeyI = 'I',
KeyJ = 'J',
KeyK = 'K',
KeyL = 'L',
KeyM = 'M',
KeyN = 'N',
KeyO = 'O',
KeyP = 'P',
KeyQ = 'Q',
KeyR = 'R',
KeyS = 'S',
KeyT = 'T',
KeyU = 'U',
KeyV = 'V',
KeyW = 'W',
KeyX = 'X',
KeyY = 'Y',
KeyZ = 'Z',
Minus = '-',
Period = '.',
Quote = "'",
Semicolon = ';',
Slash = '/',
AltLeft = 'Alt',
AltRight = 'Alt (Right)',
Backspace = 'Backspace',
CapsLock = 'Caps Lock',
ContextMenu = 'Context Menu',
ControlLeft = 'Ctrl',
ControlRight = 'Ctrl (Right)',
Enter = 'Enter',
MetaLeft = 'Meta',
MetaRight = 'Meta (Right)',
ShiftLeft = 'Shift',
ShiftRight = 'Shift (Right)',
Space = 'Space',
Tab = 'Tab',
Delete = 'Delete',
End = 'End',
Home = 'Home',
Insert = 'Insert',
PageDown = 'Page Down',
PageUp = 'Page Up',
ArrowDown = '↓',
ArrowLeft = '←',
ArrowRight = '→',
ArrowUp = '↑',
NumLock = 'Num Lock',
Numpad0 = 'Numpad 0',
Numpad1 = 'Numpad 1',
Numpad2 = 'Numpad 2',
Numpad3 = 'Numpad 3',
Numpad4 = 'Numpad 4',
Numpad5 = 'Numpad 5',
Numpad6 = 'Numpad 6',
Numpad7 = 'Numpad 7',
Numpad8 = 'Numpad 8',
Numpad9 = 'Numpad 9',
NumpadAdd = 'Numpad +',
NumpadDecimal = 'Numpad .',
NumpadDivide = 'Numpad /',
NumpadMultiply = 'Numpad *',
NumpadSubtract = 'Numpad -',
Escape = 'Esc',
PrintScreen = 'Print Screen',
ScrollLock = 'Scroll Lock',
Pause = 'Pause',
AudioVolumeDown = 'Volume Down',
AudioVolumeMute = 'Volume Mute',
AudioVolumeUp = 'Volume Up',
F1 = 'F1',
F2 = 'F2',
F3 = 'F3',
F4 = 'F4',
F5 = 'F5',
F6 = 'F6',
F7 = 'F7',
F8 = 'F8',
F9 = 'F9',
F10 = 'F10',
F11 = 'F11',
F12 = 'F12',
}

View file

@ -1,116 +1,116 @@
import { v4 as uuidv4 } from "uuid";
export enum ContentType {
Text = "text",
Image = "image",
File = "file",
Link = "link",
Color = "color",
Code = "code",
}
export class HistoryItem {
id: string;
source: string;
source_icon?: string;
content_type: ContentType;
content: string;
favicon?: string;
timestamp: Date;
language?: string;
constructor(
source: string,
content_type: ContentType,
content: string,
favicon?: string,
source_icon?: string,
language?: string
) {
this.id = uuidv4();
this.source = source;
this.source_icon = source_icon;
this.content_type = content_type;
this.content = content;
this.favicon = favicon;
this.timestamp = new Date();
this.language = language;
}
toRow(): [
string,
string,
string | undefined,
string,
string,
string | undefined,
Date,
string | undefined
] {
return [
this.id,
this.source,
this.source_icon,
this.content_type,
this.content,
this.favicon,
this.timestamp,
this.language,
];
}
}
export interface Settings {
key: string;
value: string;
}
export interface InfoText {
source: string;
content_type: ContentType.Text;
characters: number;
words: number;
copied: Date;
}
export interface InfoImage {
source: string;
content_type: ContentType.Image;
dimensions: string;
size: number;
copied: Date;
}
export interface InfoFile {
source: string;
content_type: ContentType.File;
path: string;
filesize: number;
copied: Date;
}
export interface InfoLink {
source: string;
content_type: ContentType.Link;
title?: string;
url: string;
characters: number;
copied: Date;
}
export interface InfoColor {
source: string;
content_type: ContentType.Color;
hex: string;
rgb: string;
hsl: string;
copied: Date;
}
export interface InfoCode {
source: string;
content_type: ContentType.Code;
language: string;
lines: number;
copied: Date;
}
import { v4 as uuidv4 } from "uuid";
export enum ContentType {
Text = "text",
Image = "image",
File = "file",
Link = "link",
Color = "color",
Code = "code",
}
export class HistoryItem {
id: string;
source: string;
source_icon?: string;
content_type: ContentType;
content: string;
favicon?: string;
timestamp: Date;
language?: string;
constructor(
source: string,
content_type: ContentType,
content: string,
favicon?: string,
source_icon?: string,
language?: string
) {
this.id = uuidv4();
this.source = source;
this.source_icon = source_icon;
this.content_type = content_type;
this.content = content;
this.favicon = favicon;
this.timestamp = new Date();
this.language = language;
}
toRow(): [
string,
string,
string | undefined,
string,
string,
string | undefined,
Date,
string | undefined
] {
return [
this.id,
this.source,
this.source_icon,
this.content_type,
this.content,
this.favicon,
this.timestamp,
this.language,
];
}
}
export interface Settings {
key: string;
value: string;
}
export interface InfoText {
source: string;
content_type: ContentType.Text;
characters: number;
words: number;
copied: Date;
}
export interface InfoImage {
source: string;
content_type: ContentType.Image;
dimensions: string;
size: number;
copied: Date;
}
export interface InfoFile {
source: string;
content_type: ContentType.File;
path: string;
filesize: number;
copied: Date;
}
export interface InfoLink {
source: string;
content_type: ContentType.Link;
title?: string;
url: string;
characters: number;
copied: Date;
}
export interface InfoColor {
source: string;
content_type: ContentType.Color;
hex: string;
rgb: string;
hsl: string;
copied: Date;
}
export interface InfoCode {
source: string;
content_type: ContentType.Code;
language: string;
lines: number;
copied: Date;
}