chore: system

This commit is contained in:
0PandaDEV 2025-06-08 20:22:50 +02:00
parent aa928f7094
commit 97c023df91
No known key found for this signature in database
GPG key ID: 85A398412EEB8FC9
78 changed files with 15225 additions and 15225 deletions

2
.github/FUNDING.yml vendored
View file

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

View file

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

View file

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

View file

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

View file

@ -1,249 +1,249 @@
name: "Nightly Builds" name: "Nightly Builds"
on: on:
push: push:
branches: branches:
- main - main
pull_request: pull_request:
branches: branches:
- main - main
workflow_dispatch: workflow_dispatch:
jobs: jobs:
prepare: prepare:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
version: ${{ steps.get_version.outputs.VERSION }} version: ${{ steps.get_version.outputs.VERSION }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Get version - name: Get version
id: get_version id: get_version
run: echo "VERSION=$(node -p "require('./src-tauri/tauri.conf.json').version")" >> $GITHUB_OUTPUT run: echo "VERSION=$(node -p "require('./src-tauri/tauri.conf.json').version")" >> $GITHUB_OUTPUT
build-macos: build-macos:
needs: prepare needs: prepare
runs-on: macos-latest runs-on: macos-latest
timeout-minutes: 30 timeout-minutes: 30
strategy: strategy:
matrix: matrix:
include: include:
- args: "--target aarch64-apple-darwin" - args: "--target aarch64-apple-darwin"
arch: "arm64" arch: "arm64"
- args: "--target x86_64-apple-darwin" - args: "--target x86_64-apple-darwin"
arch: "x64" arch: "x64"
env: env:
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Redact Sensitive Information - name: Redact Sensitive Information
run: | run: |
function redact_output { 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" 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 exec > >(redact_output) 2>&1
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
targets: aarch64-apple-darwin,x86_64-apple-darwin targets: aarch64-apple-darwin,x86_64-apple-darwin
- uses: swatinem/rust-cache@v2 - uses: swatinem/rust-cache@v2
with: with:
workspaces: "src-tauri -> target" workspaces: "src-tauri -> target"
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/" cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
shared-key: "macos-rust-cache" shared-key: "macos-rust-cache"
save-if: "true" save-if: "true"
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:
path: ~/.pnpm-store path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pnpm- ${{ runner.os }}-pnpm-
- run: npm install -g pnpm && pnpm install - run: npm install -g pnpm && pnpm install
- name: Import Apple Developer Certificate - name: Import Apple Developer Certificate
env: env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: | run: |
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12 echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -lut 7200 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 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 set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
security find-identity -v -p codesigning build.keychain security find-identity -v -p codesigning build.keychain
- name: Verify Certificate - name: Verify Certificate
run: | run: |
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Apple Development") CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Apple Development")
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}') CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
echo "Certificate imported." echo "Certificate imported."
- uses: tauri-apps/tauri-action@v0 - uses: tauri-apps/tauri-action@v0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }} APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
with: with:
args: ${{ matrix.args }} args: ${{ matrix.args }}
- name: Debug Signing Process - name: Debug Signing Process
if: failure() if: failure()
run: | run: |
echo "Attempting manual signing:" 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 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:" 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/' 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 - name: Set architecture label
run: | run: |
if [[ "${{ matrix.args }}" == "--target aarch64-apple-darwin" ]]; then if [[ "${{ matrix.args }}" == "--target aarch64-apple-darwin" ]]; then
echo "ARCH_LABEL=aarch64-apple-darwin" >> $GITHUB_ENV echo "ARCH_LABEL=aarch64-apple-darwin" >> $GITHUB_ENV
else else
echo "ARCH_LABEL=x86_64-apple-darwin" >> $GITHUB_ENV echo "ARCH_LABEL=x86_64-apple-darwin" >> $GITHUB_ENV
fi fi
- name: Rename and Publish macOS Artifacts - name: Rename and Publish macOS Artifacts
run: | 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/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 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 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 - uses: actions/upload-artifact@v4
with: with:
name: macos-dmg-${{ matrix.arch }} name: macos-dmg-${{ matrix.arch }}
path: "src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/dmg/*.dmg" path: "src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/dmg/*.dmg"
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: updater-macos-${{ matrix.arch }} name: updater-macos-${{ matrix.arch }}
path: | 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
src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/*.app.tar.gz.sig src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/*.app.tar.gz.sig
build-windows: build-windows:
needs: prepare needs: prepare
strategy: strategy:
matrix: matrix:
include: include:
- args: "--target x86_64-pc-windows-msvc" - args: "--target x86_64-pc-windows-msvc"
arch: "x64" arch: "x64"
target: "x86_64-pc-windows-msvc" target: "x86_64-pc-windows-msvc"
- args: "--target aarch64-pc-windows-msvc" - args: "--target aarch64-pc-windows-msvc"
arch: "arm64" arch: "arm64"
target: "aarch64-pc-windows-msvc" target: "aarch64-pc-windows-msvc"
runs-on: windows-latest runs-on: windows-latest
env: env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
targets: x86_64-pc-windows-msvc,aarch64-pc-windows-msvc targets: x86_64-pc-windows-msvc,aarch64-pc-windows-msvc
- uses: swatinem/rust-cache@v2 - uses: swatinem/rust-cache@v2
with: with:
workspaces: "src-tauri -> target" workspaces: "src-tauri -> target"
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/" cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
shared-key: "windows-rust-cache" shared-key: "windows-rust-cache"
save-if: "true" save-if: "true"
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:
path: ~/.pnpm-store path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pnpm- ${{ runner.os }}-pnpm-
- run: npm install -g pnpm && pnpm install - run: npm install -g pnpm && pnpm install
- uses: tauri-apps/tauri-action@v0 - uses: tauri-apps/tauri-action@v0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
args: ${{ matrix.args }} args: ${{ matrix.args }}
- name: List Bundle Directory - name: List Bundle Directory
shell: pwsh shell: pwsh
run: | run: |
Write-Output "Checking build directories..." Write-Output "Checking build directories..."
Get-ChildItem -Path "src-tauri/target" -Recurse -Directory | Where-Object { $_.Name -eq "msi" } | ForEach-Object { Get-ChildItem -Path "src-tauri/target" -Recurse -Directory | Where-Object { $_.Name -eq "msi" } | ForEach-Object {
Write-Output "Found MSI directory: $($_.FullName)" Write-Output "Found MSI directory: $($_.FullName)"
Get-ChildItem -Path $_.FullName -Filter "*.msi" | ForEach-Object { Get-ChildItem -Path $_.FullName -Filter "*.msi" | ForEach-Object {
Write-Output "Found MSI file: $($_.FullName)" Write-Output "Found MSI file: $($_.FullName)"
} }
} }
- name: Rename and Publish Windows Artifacts - name: Rename and Publish Windows Artifacts
run: | 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 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 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 - uses: actions/upload-artifact@v4
with: with:
name: windows-${{ matrix.arch }} name: windows-${{ matrix.arch }}
path: src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi path: src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: updater-windows-${{ matrix.arch }} name: updater-windows-${{ matrix.arch }}
path: | path: |
src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi
src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi.sig src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi.sig
build-ubuntu: build-ubuntu:
needs: prepare needs: prepare
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
targets: x86_64-unknown-linux-gnu targets: x86_64-unknown-linux-gnu
- uses: swatinem/rust-cache@v2 - uses: swatinem/rust-cache@v2
with: with:
workspaces: "src-tauri -> target" workspaces: "src-tauri -> target"
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/" cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
shared-key: "ubuntu-rust-cache" shared-key: "ubuntu-rust-cache"
save-if: "true" save-if: "true"
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:
path: ~/.pnpm-store path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pnpm- ${{ runner.os }}-pnpm-
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt update 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 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 echo "PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig" >> $GITHUB_ENV
- run: npm install -g pnpm && pnpm install - run: npm install -g pnpm && pnpm install
- uses: tauri-apps/tauri-action@v0 - uses: tauri-apps/tauri-action@v0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
args: --target x86_64-unknown-linux-gnu args: --target x86_64-unknown-linux-gnu
- name: Rename Linux Artifacts - name: Rename Linux Artifacts
run: | 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/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 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/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 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 - uses: actions/upload-artifact@v4
with: with:
name: ubuntu-deb name: ubuntu-deb
path: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb path: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: ubuntu-appimage name: ubuntu-appimage
path: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage path: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: ubuntu-rpm name: ubuntu-rpm
path: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm path: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: updater-ubuntu name: updater-ubuntu
path: | 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
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig

View file

@ -1,325 +1,325 @@
name: "Release" name: "Release"
on: on:
push: push:
tags: tags:
- "v*" - "v*"
workflow_dispatch: workflow_dispatch:
jobs: jobs:
prepare: prepare:
permissions: write-all permissions: write-all
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
version: ${{ steps.get_version.outputs.VERSION }} version: ${{ steps.get_version.outputs.VERSION }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Get version - name: Get version
id: get_version id: get_version
run: | run: |
VERSION=$(node -p 'require("./src-tauri/tauri.conf.json").version') VERSION=$(node -p 'require("./src-tauri/tauri.conf.json").version')
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
build-macos: build-macos:
permissions: write-all permissions: write-all
needs: prepare needs: prepare
strategy: strategy:
matrix: matrix:
include: include:
- args: "--target aarch64-apple-darwin" - args: "--target aarch64-apple-darwin"
arch: "silicon" arch: "silicon"
- args: "--target x86_64-apple-darwin" - args: "--target x86_64-apple-darwin"
arch: "intel" arch: "intel"
runs-on: macos-latest runs-on: macos-latest
env: env:
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Redact Sensitive Information - name: Redact Sensitive Information
run: | run: |
function redact_output { function redact_output {
sed -e "s/${{ secrets.REDACT_PATTERN }}/REDACTED/g" sed -e "s/${{ secrets.REDACT_PATTERN }}/REDACTED/g"
} }
exec > >(redact_output) 2>&1 exec > >(redact_output) 2>&1
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
targets: aarch64-apple-darwin,x86_64-apple-darwin targets: aarch64-apple-darwin,x86_64-apple-darwin
- uses: swatinem/rust-cache@v2 - uses: swatinem/rust-cache@v2
with: with:
workspaces: "src-tauri -> target" workspaces: "src-tauri -> target"
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/" cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
shared-key: "macos-rust-cache" shared-key: "macos-rust-cache"
save-if: "true" save-if: "true"
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:
path: ~/.pnpm-store path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pnpm- ${{ runner.os }}-pnpm-
- run: npm install -g pnpm && pnpm install - run: npm install -g pnpm && pnpm install
- name: Import Apple Developer Certificate - name: Import Apple Developer Certificate
env: env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: | run: |
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12 echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" 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 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 set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
- uses: tauri-apps/tauri-action@v0 - uses: tauri-apps/tauri-action@v0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with: with:
args: ${{ matrix.args }} args: ${{ matrix.args }}
- name: Rename macOS Artifacts - name: Rename macOS Artifacts
run: | run: |
mv src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/dmg/*.dmg src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/dmg/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.dmg mv src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/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 - name: Upload artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: macos-${{ matrix.arch }}-binaries name: macos-${{ matrix.arch }}-binaries
path: | path: |
src-tauri/target/**/release/bundle/dmg/*.dmg src-tauri/target/**/release/bundle/dmg/*.dmg
build-windows: build-windows:
permissions: write-all permissions: write-all
needs: prepare needs: prepare
strategy: strategy:
matrix: matrix:
include: include:
- args: "--target x86_64-pc-windows-msvc" - args: "--target x86_64-pc-windows-msvc"
arch: "x64" arch: "x64"
target: "x86_64-pc-windows-msvc" target: "x86_64-pc-windows-msvc"
- args: "--target aarch64-pc-windows-msvc" - args: "--target aarch64-pc-windows-msvc"
arch: "arm64" arch: "arm64"
target: "aarch64-pc-windows-msvc" target: "aarch64-pc-windows-msvc"
runs-on: windows-latest runs-on: windows-latest
env: env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
targets: x86_64-pc-windows-msvc,aarch64-pc-windows-msvc targets: x86_64-pc-windows-msvc,aarch64-pc-windows-msvc
- uses: swatinem/rust-cache@v2 - uses: swatinem/rust-cache@v2
with: with:
workspaces: "src-tauri -> target" workspaces: "src-tauri -> target"
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/" cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
shared-key: "windows-rust-cache" shared-key: "windows-rust-cache"
save-if: "true" save-if: "true"
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:
path: ~/.pnpm-store path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pnpm- ${{ runner.os }}-pnpm-
- run: npm install -g pnpm && pnpm install - run: npm install -g pnpm && pnpm install
- uses: tauri-apps/tauri-action@v0 - uses: tauri-apps/tauri-action@v0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
args: ${{ matrix.args }} args: ${{ matrix.args }}
- name: List Bundle Directory - name: List Bundle Directory
shell: pwsh shell: pwsh
run: | run: |
$bundlePath = "src-tauri/target/${{ matrix.target }}/release/bundle/msi" $bundlePath = "src-tauri/target/${{ matrix.target }}/release/bundle/msi"
if (Test-Path $bundlePath) { if (Test-Path $bundlePath) {
Write-Output "Contents of ${bundlePath}:" Write-Output "Contents of ${bundlePath}:"
Get-ChildItem -Path $bundlePath Get-ChildItem -Path $bundlePath
} else { } else {
Write-Output "Path ${bundlePath} does not exist." Write-Output "Path ${bundlePath} does not exist."
} }
- name: Rename Windows Artifacts - name: Rename Windows Artifacts
shell: pwsh shell: pwsh
run: | run: |
$bundlePath = "src-tauri/target/${{ matrix.target }}/release/bundle/msi" $bundlePath = "src-tauri/target/${{ matrix.target }}/release/bundle/msi"
$version = "${{ needs.prepare.outputs.version }}" $version = "${{ needs.prepare.outputs.version }}"
$arch = "${{ matrix.arch }}" $arch = "${{ matrix.arch }}"
if (Test-Path $bundlePath) { if (Test-Path $bundlePath) {
$msiFiles = Get-ChildItem -Path "$bundlePath/*.msi" $msiFiles = Get-ChildItem -Path "$bundlePath/*.msi"
foreach ($file in $msiFiles) { foreach ($file in $msiFiles) {
$newName = "Qopy-$version`_$arch.msi" $newName = "Qopy-$version`_$arch.msi"
Rename-Item -Path $file.FullName -NewName $newName Rename-Item -Path $file.FullName -NewName $newName
} }
} else { } else {
Write-Error "Path ${bundlePath} does not exist." Write-Error "Path ${bundlePath} does not exist."
exit 1 exit 1
} }
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: windows-${{ matrix.arch }}-binaries name: windows-${{ matrix.arch }}-binaries
path: src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi path: src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi
build-linux: build-linux:
permissions: write-all permissions: write-all
needs: prepare needs: prepare
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- uses: swatinem/rust-cache@v2 - uses: swatinem/rust-cache@v2
with: with:
workspaces: "src-tauri -> target" workspaces: "src-tauri -> target"
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/" cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
shared-key: "linux-rust-cache" shared-key: "linux-rust-cache"
save-if: "true" save-if: "true"
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:
path: ~/.pnpm-store path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pnpm- ${{ runner.os }}-pnpm-
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt update 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 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 echo "PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig" >> $GITHUB_ENV
- run: npm install -g pnpm && pnpm install - run: npm install -g pnpm && pnpm install
- name: Generate Changelog - name: Generate Changelog
id: changelog id: changelog
run: | run: |
CHANGELOG=$(git log $(git describe --tags --abbrev=0)..HEAD --pretty=format:"- %s") CHANGELOG=$(git log $(git describe --tags --abbrev=0)..HEAD --pretty=format:"- %s")
echo "CHANGELOG<<EOF" >> $GITHUB_ENV echo "CHANGELOG<<EOF" >> $GITHUB_ENV
echo "$CHANGELOG" >> $GITHUB_ENV echo "$CHANGELOG" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV
- uses: tauri-apps/tauri-action@v0 - uses: tauri-apps/tauri-action@v0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
args: --target x86_64-unknown-linux-gnu args: --target x86_64-unknown-linux-gnu
- name: Rename Linux Artifacts - name: Rename Linux Artifacts
run: | 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/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 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 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 - name: Upload artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: linux-binaries name: linux-binaries
path: | path: |
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb 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/appimage/*.AppImage
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
create-release: create-release:
permissions: write-all permissions: write-all
needs: [prepare, build-macos, build-windows, build-linux] needs: [prepare, build-macos, build-windows, build-linux]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.PAT }} token: ${{ secrets.PAT }}
- name: Check if release already exists - name: Check if release already exists
id: check_release id: check_release
run: | run: |
VERSION="${{ needs.prepare.outputs.version }}" VERSION="${{ needs.prepare.outputs.version }}"
RELEASE_EXISTS=$(gh release view v$VERSION --json id --jq '.id' 2>/dev/null || echo "") RELEASE_EXISTS=$(gh release view v$VERSION --json id --jq '.id' 2>/dev/null || echo "")
if [ -n "$RELEASE_EXISTS" ]; then if [ -n "$RELEASE_EXISTS" ]; then
echo "SKIP_RELEASE=true" >> $GITHUB_ENV echo "SKIP_RELEASE=true" >> $GITHUB_ENV
else else
echo "SKIP_RELEASE=false" >> $GITHUB_ENV echo "SKIP_RELEASE=false" >> $GITHUB_ENV
fi fi
env: env:
GITHUB_TOKEN: ${{ secrets.PAT }} GITHUB_TOKEN: ${{ secrets.PAT }}
- name: Download all artifacts - name: Download all artifacts
if: env.SKIP_RELEASE == 'false' if: env.SKIP_RELEASE == 'false'
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
path: artifacts path: artifacts
- name: Update CHANGELOG - name: Update CHANGELOG
if: env.SKIP_RELEASE == 'false' if: env.SKIP_RELEASE == 'false'
id: changelog id: changelog
uses: requarks/changelog-action@v1 uses: requarks/changelog-action@v1
with: with:
token: ${{ github.token }} token: ${{ github.token }}
tag: ${{ github.ref_name }} tag: ${{ github.ref_name }}
- name: Generate Release Body - name: Generate Release Body
if: env.SKIP_RELEASE == 'false' if: env.SKIP_RELEASE == 'false'
id: release_body id: release_body
run: | run: |
VERSION="${{ needs.prepare.outputs.version }}" VERSION="${{ needs.prepare.outputs.version }}"
# Calculate hashes with corrected paths # Calculate hashes with corrected paths
WINDOWS_ARM_HASH=$(sha256sum "artifacts/windows-arm64-binaries/Qopy-${VERSION}_arm64.msi" | awk '{ print $1 }') 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 }') 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_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 }') 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 }') 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 }') 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 }') REDHAT_HASH=$(sha256sum "artifacts/linux-binaries/rpm/Qopy-${VERSION}.rpm" | awk '{ print $1 }')
# Debug output # Debug output
echo "Calculated hashes:" echo "Calculated hashes:"
echo "Windows ARM: $WINDOWS_ARM_HASH" echo "Windows ARM: $WINDOWS_ARM_HASH"
echo "Windows x64: $WINDOWS_64_HASH" echo "Windows x64: $WINDOWS_64_HASH"
echo "Mac Silicon: $MAC_SILICON_HASH" echo "Mac Silicon: $MAC_SILICON_HASH"
echo "Mac Intel: $MAC_INTEL_HASH" echo "Mac Intel: $MAC_INTEL_HASH"
echo "Debian: $DEBIAN_HASH" echo "Debian: $DEBIAN_HASH"
echo "AppImage: $APPIMAGE_HASH" echo "AppImage: $APPIMAGE_HASH"
echo "Red Hat: $REDHAT_HASH" echo "Red Hat: $REDHAT_HASH"
RELEASE_BODY=$(cat <<-EOF RELEASE_BODY=$(cat <<-EOF
${{ needs.create-release.outputs.changelog }} ${{ needs.create-release.outputs.changelog }}
## ⬇️ Downloads ## ⬇️ Downloads
- [Windows (x64)](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}_x64.msi) - ${WINDOWS_64_HASH} - [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} - [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 (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} - [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} - [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} - [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} - [Red Hat](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}.rpm) - ${REDHAT_HASH}
EOF EOF
) )
echo "RELEASE_BODY<<EOF" >> $GITHUB_ENV echo "RELEASE_BODY<<EOF" >> $GITHUB_ENV
echo "$RELEASE_BODY" >> $GITHUB_ENV echo "$RELEASE_BODY" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV
- name: Create Release - name: Create Release
if: env.SKIP_RELEASE == 'false' if: env.SKIP_RELEASE == 'false'
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
env: env:
GITHUB_TOKEN: ${{ secrets.PAT }} GITHUB_TOKEN: ${{ secrets.PAT }}
with: with:
draft: true draft: true
tag_name: v${{ needs.prepare.outputs.version }} tag_name: v${{ needs.prepare.outputs.version }}
name: v${{ needs.prepare.outputs.version }} name: v${{ needs.prepare.outputs.version }}
files: | files: |
artifacts/**/*.dmg artifacts/**/*.dmg
artifacts/**/*.msi artifacts/**/*.msi
artifacts/**/*.deb artifacts/**/*.deb
artifacts/**/*.AppImage artifacts/**/*.AppImage
artifacts/**/*.rpm artifacts/**/*.rpm
body: ${{ env.RELEASE_BODY }} body: ${{ env.RELEASE_BODY }}

54
.gitignore vendored
View file

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

View file

@ -1,23 +1,23 @@
# Get Started # 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). 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. All the data of Qopy is stored inside of a SQLite database.
| Operating System | Path | | Operating System | Path |
|------------------|-----------------------------------------------------------------| |------------------|-----------------------------------------------------------------|
| Windows | `C:\Users\USERNAME\AppData\Roaming\net.pandadev.qopy` | | Windows | `C:\Users\USERNAME\AppData\Roaming\net.pandadev.qopy` |
| macOS | `/Users/USERNAME/Library/Application Support/net.pandadev.qopy` | | macOS | `/Users/USERNAME/Library/Application Support/net.pandadev.qopy` |
| Linux | `/home/USERNAME/.local/share/net.pandadev.qopy` | | Linux | `/home/USERNAME/.local/share/net.pandadev.qopy` |
## Disable Windows+V for default clipboard manager ## 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> <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 To disable the default clipboard manager popup from windows open Command prompt and run this command
```cmd ```cmd
reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\System" /v AllowClipboardHistory /t REG_DWORD /d 0 /f 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. 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"> <div align="center">
<img align="center" width="128px" src="src-tauri/icons/icon.png" /> <img align="center" width="128px" src="src-tauri/icons/icon.png" />
<h1 align="center"><b>Qopy</b></h1> <h1 align="center"><b>Qopy</b></h1>
The fixed and simple clipboard manager for both Windows and Linux. 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"> <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) <img src="./public/windows.png"> Windows (x64)
</a> </a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_arm64.msi"> <a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_arm64.msi">
Windows (arm64) Windows (arm64)
</a> </a>
<br> <br>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3.deb"> <a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3.deb">
<img src="./public/linux.png"> Linux (deb) <img src="./public/linux.png"> Linux (deb)
</a> </a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3.rpm"> <a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3.rpm">
Linux (rpm) Linux (rpm)
</a> </a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3.AppImage"> <a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3.AppImage">
Linux (AppImage) Linux (AppImage)
</a> </a>
<br> <br>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_silicon.dmg"> <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) <img src="./public/apple.png"> macOS (Silicon)
</a> </a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_intel.dmg"> <a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_intel.dmg">
macOS (Intel) macOS (Intel)
</a> </a>
<br> <br>
<br> <br>
<sup>Nightly releases can be found <a href="https://github.com/0PandaDEV/qopy/actions/workflows/build.yml">here</a> </sup> <sup>Nightly releases can be found <a href="https://github.com/0PandaDEV/qopy/actions/workflows/build.yml">here</a> </sup>
</div> </div>
[discord »](https://discord.gg/invite/Y7SbYphVw9) [discord »](https://discord.gg/invite/Y7SbYphVw9)
> \[!IMPORTANT] > \[!IMPORTANT]
> >
> **Star this project**, You will receive all release notifications from GitHub without any delay \~ ⭐️ > **Star this project**, You will receive all release notifications from GitHub without any delay \~ ⭐️
<details> <details>
<summary><kbd>Star History</kbd></summary> <summary><kbd>Star History</kbd></summary>
<a href="https://starchart.cc/0PandaDEV/Qopy"> <a href="https://starchart.cc/0PandaDEV/Qopy">
<picture> <picture>
<img width="100%" src="https://starchart.cc/0PandaDEV/Qopy.svg?variant=adaptive"> <img width="100%" src="https://starchart.cc/0PandaDEV/Qopy.svg?variant=adaptive">
</picture> </picture>
</a> </a>
</details> </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) [![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 ## 📋 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. Qopy is a fixed clipboard manager designed as a simple alternative to the standard clipboard on Windows. It aims to provide a faster, more reliable experience while providing an extensive set of features compared to its Windows counterpart.
## 🚧 Roadmap ## 🚧 Roadmap
- [x] [Setup guide](https://github.com/0PandaDEV/Qopy/blob/main/GET_STARTED.md) - [x] [Setup guide](https://github.com/0PandaDEV/Qopy/blob/main/GET_STARTED.md)
- [ ] Sync Clipboard across devices https://github.com/0PandaDEV/Qopy/issues/8 - [ ] Sync Clipboard across devices https://github.com/0PandaDEV/Qopy/issues/8
- [x] Settings https://github.com/0PandaDEV/Qopy/issues/2 - [x] Settings https://github.com/0PandaDEV/Qopy/issues/2
- [x] Metadata for copied items https://github.com/0PandaDEV/Qopy/issues/5 - [x] Metadata for copied items https://github.com/0PandaDEV/Qopy/issues/5
- [ ] Code highlighting https://github.com/0PandaDEV/Qopy/issues/7 - [ ] Code highlighting https://github.com/0PandaDEV/Qopy/issues/7
- [ ] Streamshare integration https://github.com/0PandaDEV/Qopy/issues/4 - [ ] Streamshare integration https://github.com/0PandaDEV/Qopy/issues/4
- [ ] Content type filter https://github.com/0PandaDEV/Qopy/issues/16 - [ ] Content type filter https://github.com/0PandaDEV/Qopy/issues/16
- [ ] Preview for copied files https://github.com/0PandaDEV/Qopy/issues/15 - [ ] Preview for copied files https://github.com/0PandaDEV/Qopy/issues/15
- [ ] Convert files to other formats https://github.com/0PandaDEV/Qopy/issues/17 - [ ] 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] Option for custom keybind https://github.com/0PandaDEV/Qopy/issues/3
- [x] macOS Support https://github.com/0PandaDEV/Qopy/issues/13 - [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> <sup>If you have ideas for features to include, please write a feature request [here](https://github.com/0pandadev/Qopy/issues).</sup>
## 📦 Concepts ## 📦 Concepts
Here you can see a few concepts these might not be implemented: Here you can see a few concepts these might not be implemented:
![Clipboard](https://github.com/user-attachments/assets/45a44a13-6ebd-4f2d-84d2-55178e303a54) ![Clipboard](https://github.com/user-attachments/assets/45a44a13-6ebd-4f2d-84d2-55178e303a54)
![Settings](https://github.com/user-attachments/assets/bff5456a-f413-4e62-a43d-22c8e453aa87) ![Settings](https://github.com/user-attachments/assets/bff5456a-f413-4e62-a43d-22c8e453aa87)
## ❤️ Donations & Support ## ❤️ 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. 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> <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 ## ⌨️ Local development
You can use GitHub Codespaces for online development: You can use GitHub Codespaces for online development:
[![][codespaces-shield]][codespaces-link] [![][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: Or to get Qopy set up on your machine, you'll need to have Rust and bun installed. Then, follow these steps:
```zsh ```zsh
git clone https://github.com/0pandadev/Qopy.git git clone https://github.com/0pandadev/Qopy.git
cd Qopy cd Qopy
bun i bun i
bun dev bun dev
``` ```
> \[!TIP] > \[!TIP]
> >
> If you are interested in contributing code, feel free to check out the [Issues](https://github.com/0pandadev/Qopy/issues) section. > If you are interested in contributing code, feel free to check out the [Issues](https://github.com/0pandadev/Qopy/issues) section.
## 🔨 Building for production ## 🔨 Building for production
To build for production simply execute: To build for production simply execute:
```zsh ```zsh
bun build bun build
``` ```
> \[!NOTE] > \[!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. > 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`. > You can find them in `src-tauri/target/release/bundle`.
## 📝 License ## 📝 License
Qopy is licensed under AGPL-3. See the [LICENSE file](./LICENCE) for more information. Qopy is licensed under AGPL-3. See the [LICENSE file](./LICENCE) for more information.
[codespaces-link]: https://codespaces.new/0pandadev/Qopy [codespaces-link]: https://codespaces.new/0pandadev/Qopy
[codespaces-shield]: https://github.com/codespaces/badge.svg [codespaces-shield]: https://github.com/codespaces/badge.svg

View file

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

218
app.vue
View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,7 +1,7 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#F84E4E" fill-rule="evenodd" <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" 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" /> clip-rule="evenodd" />
</svg> </svg>
</template> </template>

View file

@ -1,14 +1,14 @@
<template> <template>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g> <g>
<path <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" 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" fill="none" stroke-width="1.5" stroke="#E5DFD5" stroke-linecap="round" stroke-linejoin="round"
transform="translate(5 0.778)" /> transform="translate(5 0.778)" />
<path <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" 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" fill="none" stroke-width="1.5" stroke="#E5DFD5" stroke-linecap="round" stroke-linejoin="round"
transform="translate(2.333 2.161)" /> transform="translate(2.333 2.161)" />
</g> </g>
</svg> </svg>
</template> </template>

View file

@ -1,7 +1,7 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd" <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" 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" /> clip-rule="evenodd" />
</svg> </svg>
</template> </template>

View file

@ -1,16 +1,16 @@
<template> <template>
<svg <svg
width="18" width="18"
height="18" height="18"
viewBox="0 0 18 18" viewBox="0 0 18 18"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<g> <g>
<path <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" 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="#E5DFD5"
fill-rule="evenodd" fill-rule="evenodd"
transform="translate(1.5 0)" /> transform="translate(1.5 0)" />
</g> </g>
</svg> </svg>
</template> </template>

View file

@ -1,10 +1,10 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<g fill="none"> <g fill="none">
<path fill="#E5DFD5" fill-rule="evenodd" <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" 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" /> clip-rule="evenodd" />
<path stroke="#E5DFD5" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.5 8h11" /> <path stroke="#E5DFD5" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.5 8h11" />
</g> </g>
</svg> </svg>
</template> </template>

View file

@ -1,16 +1,16 @@
<template> <template>
<svg <svg
width="18" width="18"
height="18" height="18"
viewBox="0 0 18 18" viewBox="0 0 18 18"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<g> <g>
<path <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" 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="#E5DFD5"
fill-rule="evenodd" fill-rule="evenodd"
transform="translate(1.5 0)" /> transform="translate(1.5 0)" />
</g> </g>
</svg> </svg>
</template> </template>

View file

@ -1,7 +1,7 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd" <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" 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" /> clip-rule="evenodd" />
</svg> </svg>
</template> </template>

View file

@ -1,7 +1,7 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd" <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" 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" /> clip-rule="evenodd" />
</svg> </svg>
</template> </template>

View file

@ -1,15 +1,15 @@
<template> <template>
<svg <svg
width="18" width="18"
height="18" height="18"
viewBox="0 0 18 18" viewBox="0 0 18 18"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<g> <g>
<path <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" 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="#E5DFD5"
fill-rule="evenodd" /> fill-rule="evenodd" />
</g> </g>
</svg> </svg>
</template> </template>

View file

@ -1,15 +1,15 @@
<template> <template>
<svg <svg
width="18" width="18"
height="18" height="18"
viewBox="0 0 18 18" viewBox="0 0 18 18"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<g> <g>
<path <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" 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="#E5DFD5"
fill-rule="evenodd" /> fill-rule="evenodd" />
</g> </g>
</svg> </svg>
</template> </template>

View file

@ -1,7 +1,7 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd" <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" 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" /> clip-rule="evenodd" />
</svg> </svg>
</template> </template>

View file

@ -1,7 +1,7 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd" <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" 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" /> clip-rule="evenodd" />
</svg> </svg>
</template> </template>

View file

@ -1,7 +1,7 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd" <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" 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" /> clip-rule="evenodd" />
</svg> </svg>
</template> </template>

View file

@ -1,7 +1,7 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd" <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" 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" /> clip-rule="evenodd" />
</svg> </svg>
</template> </template>

View file

@ -1,16 +1,16 @@
<template> <template>
<svg <svg
width="18" width="18"
height="18" height="18"
viewBox="0 0 18 18" viewBox="0 0 18 18"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<g> <g>
<path <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" 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="#E5DFD5"
fill-rule="evenodd" fill-rule="evenodd"
transform="translate(1.5 0)" /> transform="translate(1.5 0)" />
</g> </g>
</svg> </svg>
</template> </template>

View file

@ -1,7 +1,7 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
<path fill="#E5DFD5" fill-rule="evenodd" <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" 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" /> clip-rule="evenodd" />
</svg> </svg>
</template> </template>

View file

@ -1,39 +1,39 @@
<template> <template>
<svg <svg
width="24px" width="24px"
height="20px" height="20px"
viewBox="0 0 24 20" viewBox="0 0 24 20"
version="1.1" version="1.1"
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<defs> <defs>
<path d="M0 0L24 0L24 20L0 20L0 0Z" id="path_1" /> <path d="M0 0L24 0L24 20L0 20L0 0Z" id="path_1" />
<clipPath id="clip_1"> <clipPath id="clip_1">
<use <use
xlink:href="#path_1" xlink:href="#path_1"
clip-rule="evenodd" clip-rule="evenodd"
fill-rule="evenodd" fill-rule="evenodd"
transform="translate(0, -2.133523)" /> transform="translate(0, -2.133523)" />
</clipPath> </clipPath>
</defs> </defs>
<g id="cmd"> <g id="cmd">
<path <path
d="M-751 -2016L-751 -2016L-751 -1996L-775 -1996L-775 -2016L-751 -2016Z" d="M-751 -2016L-751 -2016L-751 -1996L-775 -1996L-775 -2016L-751 -2016Z"
id="cmd" id="cmd"
fill="none" fill="none"
stroke="none" /> stroke="none" />
<path <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" 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" id="Rectangle"
fill="#FFFFFF" fill="#FFFFFF"
fill-opacity="0.050980393" fill-opacity="0.050980393"
stroke="none" /> stroke="none" />
<g id="⌘" clip-path="url(#clip_1)" transform="translate(0 2.133523)"> <g id="⌘" clip-path="url(#clip_1)" transform="translate(0 2.133523)">
<g transform="translate(5.5692472, 0)" id="⌘" fill="#E5DFD5"> <g transform="translate(5.5692472, 0)" id="⌘" fill="#E5DFD5">
<path <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" /> 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> </g>
</g> </g>
</svg> </svg>
</template> </template>

View file

@ -1,41 +1,41 @@
<template> <template>
<svg <svg
width="24px" width="24px"
height="20px" height="20px"
viewBox="0 0 24 20" viewBox="0 0 24 20"
version="1.1" version="1.1"
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<g id="Enter" fill-opacity="1"> <g id="Enter" fill-opacity="1">
<path <path
d="M-659 -2016L-659 -2016L-659 -1996L-683 -1996L-683 -2016L-659 -2016Z" d="M-659 -2016L-659 -2016L-659 -1996L-683 -1996L-683 -2016L-659 -2016Z"
id="Enter" id="Enter"
fill="none" fill="none"
stroke="none" /> stroke="none" />
<path <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" 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" id="Rectangle"
fill="#FFFFFF" fill="#FFFFFF"
fill-opacity="0.050980393" fill-opacity="0.050980393"
stroke="none" /> stroke="none" />
<path <path
d="M16.0597 5.48914L16.0597 10.5L7.5 10.5" d="M16.0597 5.48914L16.0597 10.5L7.5 10.5"
id="Vector" id="Vector"
fill="none" fill="none"
fill-rule="evenodd" fill-rule="evenodd"
stroke="#E5DFD5" stroke="#E5DFD5"
stroke-width="1.5" stroke-width="1.5"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" /> stroke-linejoin="round" />
<path <path
d="M9.5 8.5L9.5 12.5035L7 10.5L9.5 8.5Z" d="M9.5 8.5L9.5 12.5035L7 10.5L9.5 8.5Z"
id="Vector" id="Vector"
fill="#E5DFD5" fill="#E5DFD5"
fill-rule="evenodd" fill-rule="evenodd"
stroke="#E5DFD5" stroke="#E5DFD5"
stroke-width="1.5" stroke-width="1.5"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" /> stroke-linejoin="round" />
</g> </g>
</svg> </svg>
</template> </template>

View file

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

View file

@ -1,12 +1,12 @@
<template> <template>
<svg width="24" height="20" viewBox="0 0 24 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="24" height="20" viewBox="0 0 24 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g> <g>
<path <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" 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" /> fill="#FFFFFF" fill-opacity="0.051" />
<path <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" 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)" /> fill="none" stroke-width="1.3" stroke="#E5DFD5" transform="translate(7 5)" />
</g> </g>
</svg> </svg>
</template> </template>

View file

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

View file

@ -1,122 +1,122 @@
<template> <template>
<div <div
:class="['result', { selected }]" :class="['result', { selected }]"
@click="$emit('select')" @click="$emit('select')"
:ref="el => { if (selected && el) $emit('setRef', el as HTMLElement) }"> :ref="el => { if (selected && el) $emit('setRef', el as HTMLElement) }">
<template v-if="item.content_type === 'image'"> <template v-if="item.content_type === 'image'">
<img <img
v-if="imageUrl" v-if="imageUrl"
:src="imageUrl" :src="imageUrl"
alt="Image" alt="Image"
class="image" class="image"
@error="$emit('imageError')" /> @error="$emit('imageError')" />
<IconsImage v-else class="icon" /> <IconsImage v-else class="icon" />
</template> </template>
<template v-else-if="hasFavicon(item.favicon ?? '')"> <template v-else-if="hasFavicon(item.favicon ?? '')">
<img <img
v-if="item.favicon" v-if="item.favicon"
:src="getFaviconFromDb(item.favicon)" :src="getFaviconFromDb(item.favicon)"
alt="Favicon" alt="Favicon"
class="favicon" class="favicon"
@error=" @error="
($event.target as HTMLImageElement).src = '/public/icons/Link.svg' ($event.target as HTMLImageElement).src = '/public/icons/Link.svg'
" /> " />
<IconsLink v-else class="icon" /> <IconsLink v-else class="icon" />
</template> </template>
<IconsFile <IconsFile
class="icon" class="icon"
v-else-if="item.content_type === ContentType.File" /> v-else-if="item.content_type === ContentType.File" />
<IconsText <IconsText
class="icon" class="icon"
v-else-if="item.content_type === ContentType.Text" /> v-else-if="item.content_type === ContentType.Text" />
<svg <svg
v-else-if="item.content_type === ContentType.Color" v-else-if="item.content_type === ContentType.Color"
width="18" width="18"
height="18" height="18"
viewBox="0 0 18 18" viewBox="0 0 18 18"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<g> <g>
<rect width="18" height="18" /> <rect width="18" height="18" />
<path <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" 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" /> fill="#E5DFD5" />
<path <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" 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" /> :fill="item.content" />
</g> </g>
</svg> </svg>
<IconsCode <IconsCode
class="icon" class="icon"
v-else-if="item.content_type === ContentType.Code" /> v-else-if="item.content_type === ContentType.Code" />
<span v-if="item.content_type === ContentType.Image"> <span v-if="item.content_type === ContentType.Image">
Image ({{ dimensions || "Loading..." }}) Image ({{ dimensions || "Loading..." }})
</span> </span>
<span v-else>{{ truncateContent(item.content) }}</span> <span v-else>{{ truncateContent(item.content) }}</span>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ContentType } from "~/types/types"; import { ContentType } from "~/types/types";
import type { HistoryItem } from "~/types/types"; import type { HistoryItem } from "~/types/types";
defineProps<{ defineProps<{
item: HistoryItem; item: HistoryItem;
selected: boolean; selected: boolean;
imageUrl?: string; imageUrl?: string;
dimensions?: string; dimensions?: string;
}>(); }>();
defineEmits<{ defineEmits<{
(e: "select"): void; (e: "select"): void;
(e: "imageError"): void; (e: "imageError"): void;
(e: "setRef", el: HTMLElement): void; (e: "setRef", el: HTMLElement): void;
}>(); }>();
const hasFavicon = (str: string): boolean => { const hasFavicon = (str: string): boolean => {
return str.trim() !== ""; return str.trim() !== "";
}; };
const getFaviconFromDb = (favicon: string): string => { const getFaviconFromDb = (favicon: string): string => {
return `data:image/png;base64,${favicon}`; return `data:image/png;base64,${favicon}`;
}; };
const truncateContent = (content: string): string => { const truncateContent = (content: string): string => {
const maxWidth = 284; const maxWidth = 284;
const charWidth = 9; const charWidth = 9;
const maxChars = Math.floor(maxWidth / charWidth); const maxChars = Math.floor(maxWidth / charWidth);
return content.length > maxChars return content.length > maxChars
? content.slice(0, maxChars - 3) + "..." ? content.slice(0, maxChars - 3) + "..."
: content; : content;
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.result { .result {
display: flex; display: flex;
gap: 12px; gap: 12px;
padding: 11px; padding: 11px;
border-radius: 10px; border-radius: 10px;
cursor: pointer; cursor: pointer;
align-items: center; align-items: center;
&.selected { &.selected {
background-color: var(--border); background-color: var(--border);
} }
.favicon, .favicon,
.image, .image,
.icon { .icon {
width: 18px; width: 18px;
height: 18px; height: 18px;
} }
span { span {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
color: var(--text); color: var(--text);
} }
} }
</style> </style>

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -1,27 +1,27 @@
import { platform } from "@tauri-apps/plugin-os"; import { platform } from "@tauri-apps/plugin-os";
import { useKeyboard, Key } from "@waradu/keyboard"; import { useKeyboard, Key } from "@waradu/keyboard";
export default defineNuxtPlugin(async (nuxtApp) => { export default defineNuxtPlugin(async (nuxtApp) => {
const keyboardInstance = useKeyboard(); const keyboardInstance = useKeyboard();
let currentOS = "windows"; let currentOS = "windows";
try { try {
const osName = await Promise.resolve(platform()); const osName = await Promise.resolve(platform());
currentOS = osName.toLowerCase().includes("mac") ? "macos" : "windows"; currentOS = osName.toLowerCase().includes("mac") ? "macos" : "windows";
} catch (error) { } catch (error) {
console.error("Error detecting platform:", error); console.error("Error detecting platform:", error);
} }
// Defer initialization until the app is mounted // Defer initialization until the app is mounted
nuxtApp.hook('app:mounted', () => { nuxtApp.hook('app:mounted', () => {
keyboardInstance.init(); keyboardInstance.init();
}); });
nuxtApp.provide('keyboard', { nuxtApp.provide('keyboard', {
listen: keyboardInstance.listen.bind(keyboardInstance), listen: keyboardInstance.listen.bind(keyboardInstance),
init: keyboardInstance.init.bind(keyboardInstance), init: keyboardInstance.init.bind(keyboardInstance),
Key, Key,
currentOS, currentOS,
// Provide a clear method if users need to manually clear all listeners from the instance // 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'); } 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 { ref, computed } from 'vue';
import type { HistoryItem } from '~/types/types'; import type { HistoryItem } from '~/types/types';
interface GroupedHistory { interface GroupedHistory {
label: string; label: string;
items: HistoryItem[]; items: HistoryItem[];
} }
const selectedGroupIndex = ref(0); const selectedGroupIndex = ref(0);
const selectedItemIndex = ref(0); const selectedItemIndex = ref(0);
const selectedElement = ref<HTMLElement | null>(null); const selectedElement = ref<HTMLElement | null>(null);
const useSelectedResult = (groupedHistory: Ref<GroupedHistory[]>) => { const useSelectedResult = (groupedHistory: Ref<GroupedHistory[]>) => {
const selectedItem = computed<HistoryItem | undefined>(() => { const selectedItem = computed<HistoryItem | undefined>(() => {
const group = groupedHistory.value[selectedGroupIndex.value]; const group = groupedHistory.value[selectedGroupIndex.value];
return group?.items[selectedItemIndex.value] ?? undefined; return group?.items[selectedItemIndex.value] ?? undefined;
}); });
const isSelected = (groupIndex: number, itemIndex: number): boolean => { const isSelected = (groupIndex: number, itemIndex: number): boolean => {
return selectedGroupIndex.value === groupIndex && selectedItemIndex.value === itemIndex; return selectedGroupIndex.value === groupIndex && selectedItemIndex.value === itemIndex;
}; };
const selectNext = (): void => { const selectNext = (): void => {
const currentGroup = groupedHistory.value[selectedGroupIndex.value]; const currentGroup = groupedHistory.value[selectedGroupIndex.value];
if (selectedItemIndex.value < currentGroup.items.length - 1) { if (selectedItemIndex.value < currentGroup.items.length - 1) {
selectedItemIndex.value++; selectedItemIndex.value++;
} else if (selectedGroupIndex.value < groupedHistory.value.length - 1) { } else if (selectedGroupIndex.value < groupedHistory.value.length - 1) {
selectedGroupIndex.value++; selectedGroupIndex.value++;
selectedItemIndex.value = 0; selectedItemIndex.value = 0;
} }
}; };
const selectPrevious = (): void => { const selectPrevious = (): void => {
if (selectedItemIndex.value > 0) { if (selectedItemIndex.value > 0) {
selectedItemIndex.value--; selectedItemIndex.value--;
} else if (selectedGroupIndex.value > 0) { } else if (selectedGroupIndex.value > 0) {
selectedGroupIndex.value--; selectedGroupIndex.value--;
selectedItemIndex.value = groupedHistory.value[selectedGroupIndex.value].items.length - 1; selectedItemIndex.value = groupedHistory.value[selectedGroupIndex.value].items.length - 1;
} }
}; };
const selectItem = (groupIndex: number, itemIndex: number): void => { const selectItem = (groupIndex: number, itemIndex: number): void => {
selectedGroupIndex.value = groupIndex; selectedGroupIndex.value = groupIndex;
selectedItemIndex.value = itemIndex; selectedItemIndex.value = itemIndex;
}; };
return { return {
selectedItem, selectedItem,
isSelected, isSelected,
selectNext, selectNext,
selectPrevious, selectPrevious,
selectItem, selectItem,
selectedElement selectedElement
}; };
}; };
export default defineNuxtPlugin(() => { export default defineNuxtPlugin(() => {
return { return {
provide: { provide: {
selectedResult: { selectedResult: {
selectedGroupIndex, selectedGroupIndex,
selectedItemIndex, selectedItemIndex,
selectedElement, selectedElement,
useSelectedResult useSelectedResult
} }
} }
}; };
}); });

View file

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

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?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"> <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)"> <g id="Arrow" transform="translate(0 0)">
<rect id="Rectangle" width="12" height="8" style="mix-blend-mode:normal;" 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)" /> <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> </g>
</svg> </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 # Generated by Cargo
# will have compiled files and executables # will have compiled files and executables
/target/ /target/
/gen/schemas /gen/schemas

15618
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -1,279 +1,279 @@
use tauri_plugin_aptabase::EventTracker; use tauri_plugin_aptabase::EventTracker;
use base64::{ engine::general_purpose::STANDARD, Engine }; use base64::{ engine::general_purpose::STANDARD, Engine };
// use hyperpolyglot; // use hyperpolyglot;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use rdev::{ simulate, EventType, Key }; use rdev::{ simulate, EventType, Key };
use regex::Regex; use regex::Regex;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use std::fs; use std::fs;
use std::sync::atomic::{ AtomicBool, Ordering }; use std::sync::atomic::{ AtomicBool, Ordering };
use std::{ thread, time::Duration }; use std::{ thread, time::Duration };
use tauri::{ AppHandle, Emitter, Listener, Manager }; use tauri::{ AppHandle, Emitter, Listener, Manager };
use tauri_plugin_clipboard::Clipboard; use tauri_plugin_clipboard::Clipboard;
use tokio::runtime::Runtime as TokioRuntime; use tokio::runtime::Runtime as TokioRuntime;
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
use crate::db; use crate::db;
use crate::utils::commands::get_app_info; use crate::utils::commands::get_app_info;
use crate::utils::favicon::fetch_favicon_as_base64; use crate::utils::favicon::fetch_favicon_as_base64;
use crate::utils::types::{ ContentType, HistoryItem }; use crate::utils::types::{ ContentType, HistoryItem };
lazy_static! { lazy_static! {
static ref IS_PROGRAMMATIC_PASTE: AtomicBool = AtomicBool::new(false); static ref IS_PROGRAMMATIC_PASTE: AtomicBool = AtomicBool::new(false);
} }
#[tauri::command] #[tauri::command]
pub async fn write_and_paste( pub async fn write_and_paste(
app_handle: AppHandle, app_handle: AppHandle,
content: String, content: String,
content_type: String content_type: String
) -> Result<(), String> { ) -> Result<(), String> {
let clipboard = app_handle.state::<Clipboard>(); let clipboard = app_handle.state::<Clipboard>();
match content_type.as_str() { match content_type.as_str() {
"text" => clipboard.write_text(content).map_err(|e| e.to_string())?, "text" => clipboard.write_text(content).map_err(|e| e.to_string())?,
"link" => 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())?, "color" => clipboard.write_text(content).map_err(|e| e.to_string())?,
"image" => { "image" => {
clipboard.write_image_base64(content).map_err(|e| e.to_string())?; clipboard.write_image_base64(content).map_err(|e| e.to_string())?;
} }
"files" => { "files" => {
clipboard clipboard
.write_files_uris( .write_files_uris(
content content
.split(", ") .split(", ")
.map(|file| file.to_string()) .map(|file| file.to_string())
.collect::<Vec<String>>() .collect::<Vec<String>>()
) )
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
} }
_ => { _ => {
return Err("Unsupported content type".to_string()); return Err("Unsupported content type".to_string());
} }
} }
IS_PROGRAMMATIC_PASTE.store(true, Ordering::SeqCst); IS_PROGRAMMATIC_PASTE.store(true, Ordering::SeqCst);
thread::spawn(|| { thread::spawn(|| {
thread::sleep(Duration::from_millis(100)); thread::sleep(Duration::from_millis(100));
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let modifier_key = Key::MetaLeft; let modifier_key = Key::MetaLeft;
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
let modifier_key = Key::ControlLeft; let modifier_key = Key::ControlLeft;
let events = vec![ let events = vec![
EventType::KeyPress(modifier_key), EventType::KeyPress(modifier_key),
EventType::KeyPress(Key::KeyV), EventType::KeyPress(Key::KeyV),
EventType::KeyRelease(Key::KeyV), EventType::KeyRelease(Key::KeyV),
EventType::KeyRelease(modifier_key) EventType::KeyRelease(modifier_key)
]; ];
for event in events { for event in events {
if let Err(e) = simulate(&event) { if let Err(e) = simulate(&event) {
println!("Simulation error: {:?}", e); println!("Simulation error: {:?}", e);
} }
thread::sleep(Duration::from_millis(20)); thread::sleep(Duration::from_millis(20));
} }
}); });
tokio::spawn(async { tokio::spawn(async {
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
IS_PROGRAMMATIC_PASTE.store(false, Ordering::SeqCst); IS_PROGRAMMATIC_PASTE.store(false, Ordering::SeqCst);
}); });
let _ = app_handle.track_event( let _ = app_handle.track_event(
"clipboard_paste", "clipboard_paste",
Some(serde_json::json!({ Some(serde_json::json!({
"content_type": content_type "content_type": content_type
})) }))
); );
Ok(()) Ok(())
} }
pub fn setup(app: &AppHandle) { pub fn setup(app: &AppHandle) {
let app_handle = app.clone(); let app_handle = app.clone();
let runtime = TokioRuntime::new().expect("Failed to create Tokio runtime"); let runtime = TokioRuntime::new().expect("Failed to create Tokio runtime");
app_handle.clone().listen("plugin:clipboard://clipboard-monitor/update", move |_event| { app_handle.clone().listen("plugin:clipboard://clipboard-monitor/update", move |_event| {
let app_handle = app_handle.clone(); let app_handle = app_handle.clone();
runtime.block_on(async move { runtime.block_on(async move {
if IS_PROGRAMMATIC_PASTE.load(Ordering::SeqCst) { if IS_PROGRAMMATIC_PASTE.load(Ordering::SeqCst) {
return; return;
} }
let clipboard = app_handle.state::<Clipboard>(); let clipboard = app_handle.state::<Clipboard>();
let available_types = clipboard.available_types().unwrap(); let available_types = clipboard.available_types().unwrap();
let (app_name, app_icon) = get_app_info(); let (app_name, app_icon) = get_app_info();
match get_pool(&app_handle).await { match get_pool(&app_handle).await {
Ok(pool) => { Ok(pool) => {
if available_types.image { if available_types.image {
println!("Handling image change"); println!("Handling image change");
if let Ok(image_data) = clipboard.read_image_base64() { if let Ok(image_data) = clipboard.read_image_base64() {
let file_path = save_image_to_file(&app_handle, &image_data).await let file_path = save_image_to_file(&app_handle, &image_data).await
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
.unwrap_or_else(|e| e); .unwrap_or_else(|e| e);
let _ = db::history::add_history_item( let _ = db::history::add_history_item(
app_handle.clone(), app_handle.clone(),
pool, pool,
HistoryItem::new( HistoryItem::new(
app_name, app_name,
ContentType::Image, ContentType::Image,
file_path, file_path,
None, None,
app_icon, app_icon,
None None
) )
).await; ).await;
} }
} else if available_types.files { } else if available_types.files {
println!("Handling files change"); println!("Handling files change");
if let Ok(files) = clipboard.read_files() { if let Ok(files) = clipboard.read_files() {
for file in files { for file in files {
let _ = db::history::add_history_item( let _ = db::history::add_history_item(
app_handle.clone(), app_handle.clone(),
pool.clone(), pool.clone(),
HistoryItem::new( HistoryItem::new(
app_name.clone(), app_name.clone(),
ContentType::File, ContentType::File,
file, file,
None, None,
app_icon.clone(), app_icon.clone(),
None None
) )
).await; ).await;
} }
} }
} else if available_types.text { } else if available_types.text {
println!("Handling text change"); println!("Handling text change");
if let Ok(text) = clipboard.read_text() { if let Ok(text) = clipboard.read_text() {
let text = text.to_string(); let text = text.to_string();
let url_regex = Regex::new( 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()@:%_\+.~#?&//=]*)$" r"^https?://(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*)$"
).unwrap(); ).unwrap();
if url_regex.is_match(&text) { if url_regex.is_match(&text) {
if let Ok(url) = Url::parse(&text) { if let Ok(url) = Url::parse(&text) {
let favicon = match fetch_favicon_as_base64(url).await { let favicon = match fetch_favicon_as_base64(url).await {
Ok(Some(f)) => Some(f), Ok(Some(f)) => Some(f),
_ => None, _ => None,
}; };
let _ = db::history::add_history_item( let _ = db::history::add_history_item(
app_handle.clone(), app_handle.clone(),
pool, pool,
HistoryItem::new( HistoryItem::new(
app_name, app_name,
ContentType::Link, ContentType::Link,
text, text,
favicon, favicon,
app_icon, app_icon,
None None
) )
).await; ).await;
} }
} else { } else {
if text.is_empty() { if text.is_empty() {
return; return;
} }
// Temporarily disabled code detection // Temporarily disabled code detection
/*if let Some(detection) = hyperpolyglot::detect_from_text(&text) { /*if let Some(detection) = hyperpolyglot::detect_from_text(&text) {
let language = match detection { let language = match detection {
hyperpolyglot::Detection::Heuristics(lang) => lang.to_string(), hyperpolyglot::Detection::Heuristics(lang) => lang.to_string(),
_ => detection.language().to_string(), _ => detection.language().to_string(),
}; };
let _ = db::history::add_history_item( let _ = db::history::add_history_item(
pool, pool,
HistoryItem::new(app_name, ContentType::Code, text, None, app_icon, Some(language)) HistoryItem::new(app_name, ContentType::Code, text, None, app_icon, Some(language))
).await; ).await;
} else*/ if crate::utils::commands::detect_color(&text) { } else*/ if crate::utils::commands::detect_color(&text) {
let _ = db::history::add_history_item( let _ = db::history::add_history_item(
app_handle.clone(), app_handle.clone(),
pool, pool,
HistoryItem::new( HistoryItem::new(
app_name, app_name,
ContentType::Color, ContentType::Color,
text, text,
None, None,
app_icon, app_icon,
None None
) )
).await; ).await;
} else { } else {
let _ = db::history::add_history_item( let _ = db::history::add_history_item(
app_handle.clone(), app_handle.clone(),
pool, pool,
HistoryItem::new( HistoryItem::new(
app_name, app_name,
ContentType::Text, ContentType::Text,
text.clone(), text.clone(),
None, None,
app_icon, app_icon,
None None
) )
).await; ).await;
} }
} }
} }
} else { } else {
println!("Unknown clipboard content type"); println!("Unknown clipboard content type");
} }
} }
Err(e) => { Err(e) => {
println!("Failed to get database pool: {}", e); println!("Failed to get database pool: {}", e);
} }
} }
let _ = app_handle.track_event( let _ = app_handle.track_event(
"clipboard_copied", "clipboard_copied",
Some( Some(
serde_json::json!({ serde_json::json!({
"content_type": if available_types.image { "image" } "content_type": if available_types.image { "image" }
else if available_types.files { "files" } else if available_types.files { "files" }
else if available_types.text { "text" } else if available_types.text { "text" }
else { "unknown" } else { "unknown" }
}) })
) )
); );
}); });
}); });
} }
async fn get_pool( async fn get_pool(
app_handle: &AppHandle app_handle: &AppHandle
) -> Result<tauri::State<'_, SqlitePool>, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<tauri::State<'_, SqlitePool>, Box<dyn std::error::Error + Send + Sync>> {
Ok(app_handle.state::<SqlitePool>()) Ok(app_handle.state::<SqlitePool>())
} }
#[tauri::command] #[tauri::command]
pub fn start_monitor(app_handle: AppHandle) -> Result<(), String> { pub fn start_monitor(app_handle: AppHandle) -> Result<(), String> {
let clipboard = app_handle.state::<Clipboard>(); let clipboard = app_handle.state::<Clipboard>();
clipboard.start_monitor(app_handle.clone()).map_err(|e| e.to_string())?; clipboard.start_monitor(app_handle.clone()).map_err(|e| e.to_string())?;
app_handle app_handle
.emit("plugin:clipboard://clipboard-monitor/status", true) .emit("plugin:clipboard://clipboard-monitor/status", true)
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
Ok(()) Ok(())
} }
async fn save_image_to_file( async fn save_image_to_file(
app_handle: &AppHandle, app_handle: &AppHandle,
base64_data: &str base64_data: &str
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
let app_data_dir = app_handle.path().app_data_dir().unwrap(); let app_data_dir = app_handle.path().app_data_dir().unwrap();
let images_dir = app_data_dir.join("images"); let images_dir = app_data_dir.join("images");
fs::create_dir_all(&images_dir)?; fs::create_dir_all(&images_dir)?;
let file_name = format!("{}.png", Uuid::new_v4()); let file_name = format!("{}.png", Uuid::new_v4());
let file_path = images_dir.join(&file_name); let file_path = images_dir.join(&file_name);
let bytes = STANDARD.decode(base64_data)?; let bytes = STANDARD.decode(base64_data)?;
fs::write(&file_path, bytes)?; fs::write(&file_path, bytes)?;
Ok(file_path.to_string_lossy().into_owned()) 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::commands::center_window_on_current_monitor;
use crate::utils::keys::KeyCode; use crate::utils::keys::KeyCode;
use global_hotkey::{ use global_hotkey::{
hotkey::{ Code, HotKey, Modifiers }, hotkey::{ Code, HotKey, Modifiers },
GlobalHotKeyEvent, GlobalHotKeyEvent,
GlobalHotKeyManager, GlobalHotKeyManager,
HotKeyState, HotKeyState,
}; };
use parking_lot::Mutex; use parking_lot::Mutex;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use tauri::{ AppHandle, Manager, Listener }; use tauri::{ AppHandle, Manager, Listener };
use tauri_plugin_aptabase::EventTracker; use tauri_plugin_aptabase::EventTracker;
#[derive(Default)] #[derive(Default)]
struct HotkeyState { struct HotkeyState {
manager: Option<GlobalHotKeyManager>, manager: Option<GlobalHotKeyManager>,
registered_hotkey: Option<HotKey>, registered_hotkey: Option<HotKey>,
} }
unsafe impl Send for HotkeyState {} unsafe impl Send for HotkeyState {}
pub fn setup(app_handle: tauri::AppHandle) { pub fn setup(app_handle: tauri::AppHandle) {
let state = Arc::new(Mutex::new(HotkeyState::default())); let state = Arc::new(Mutex::new(HotkeyState::default()));
let manager = match GlobalHotKeyManager::new() { let manager = match GlobalHotKeyManager::new() {
Ok(manager) => manager, Ok(manager) => manager,
Err(err) => { Err(err) => {
eprintln!("Failed to initialize hotkey manager: {:?}", err); eprintln!("Failed to initialize hotkey manager: {:?}", err);
return; return;
} }
}; };
{ {
let mut hotkey_state = state.lock(); let mut hotkey_state = state.lock();
hotkey_state.manager = Some(manager); hotkey_state.manager = Some(manager);
} }
let rt = app_handle.state::<tokio::runtime::Runtime>(); let rt = app_handle.state::<tokio::runtime::Runtime>();
let initial_keybind = rt let initial_keybind = rt
.block_on(crate::db::settings::get_keybind(app_handle.clone())) .block_on(crate::db::settings::get_keybind(app_handle.clone()))
.expect("Failed to get initial keybind"); .expect("Failed to get initial keybind");
if let Err(e) = register_shortcut(&state, &initial_keybind) { if let Err(e) = register_shortcut(&state, &initial_keybind) {
eprintln!("Error registering initial shortcut: {:?}", e); eprintln!("Error registering initial shortcut: {:?}", e);
} }
let state_clone = Arc::clone(&state); let state_clone = Arc::clone(&state);
app_handle.listen("update-shortcut", move |event| { app_handle.listen("update-shortcut", move |event| {
let payload_str = event.payload().replace("\\\"", "\""); let payload_str = event.payload().replace("\\\"", "\"");
let trimmed_str = payload_str.trim_matches('"'); let trimmed_str = payload_str.trim_matches('"');
unregister_current_hotkey(&state_clone); unregister_current_hotkey(&state_clone);
let payload: Vec<String> = serde_json::from_str(trimmed_str).unwrap_or_default(); let payload: Vec<String> = serde_json::from_str(trimmed_str).unwrap_or_default();
if let Err(e) = register_shortcut(&state_clone, &payload) { if let Err(e) = register_shortcut(&state_clone, &payload) {
eprintln!("Error re-registering shortcut: {:?}", e); eprintln!("Error re-registering shortcut: {:?}", e);
} }
}); });
let state_clone = Arc::clone(&state); let state_clone = Arc::clone(&state);
app_handle.listen("save_keybind", move |event| { app_handle.listen("save_keybind", move |event| {
let payload_str = event.payload().to_string(); let payload_str = event.payload().to_string();
unregister_current_hotkey(&state_clone); unregister_current_hotkey(&state_clone);
let payload: Vec<String> = serde_json::from_str(&payload_str).unwrap_or_default(); let payload: Vec<String> = serde_json::from_str(&payload_str).unwrap_or_default();
if let Err(e) = register_shortcut(&state_clone, &payload) { if let Err(e) = register_shortcut(&state_clone, &payload) {
eprintln!("Error registering saved shortcut: {:?}", e); eprintln!("Error registering saved shortcut: {:?}", e);
} }
}); });
setup_hotkey_receiver(app_handle); setup_hotkey_receiver(app_handle);
} }
fn setup_hotkey_receiver(app_handle: AppHandle) { fn setup_hotkey_receiver(app_handle: AppHandle) {
std::thread::spawn(move || { std::thread::spawn(move || {
loop { loop {
match GlobalHotKeyEvent::receiver().recv() { match GlobalHotKeyEvent::receiver().recv() {
Ok(event) => { Ok(event) => {
if event.state == HotKeyState::Released { if event.state == HotKeyState::Released {
continue; continue;
} }
handle_hotkey_event(&app_handle); handle_hotkey_event(&app_handle);
} }
Err(e) => eprintln!("Error receiving hotkey event: {:?}", e), Err(e) => eprintln!("Error receiving hotkey event: {:?}", e),
} }
} }
}); });
} }
fn unregister_current_hotkey(state: &Arc<Mutex<HotkeyState>>) { fn unregister_current_hotkey(state: &Arc<Mutex<HotkeyState>>) {
let mut hotkey_state = state.lock(); let mut hotkey_state = state.lock();
if let Some(old_hotkey) = hotkey_state.registered_hotkey.take() { if let Some(old_hotkey) = hotkey_state.registered_hotkey.take() {
if let Some(manager) = &hotkey_state.manager { if let Some(manager) = &hotkey_state.manager {
let _ = manager.unregister(old_hotkey); let _ = manager.unregister(old_hotkey);
} }
} }
} }
fn register_shortcut(state: &Arc<Mutex<HotkeyState>>, shortcut: &[String]) -> Result<(), Box<dyn std::error::Error>> { fn register_shortcut(state: &Arc<Mutex<HotkeyState>>, shortcut: &[String]) -> Result<(), Box<dyn std::error::Error>> {
let hotkey = parse_hotkey(shortcut)?; let hotkey = parse_hotkey(shortcut)?;
let mut hotkey_state = state.lock(); let mut hotkey_state = state.lock();
if let Some(manager) = &hotkey_state.manager { if let Some(manager) = &hotkey_state.manager {
manager.register(hotkey.clone())?; manager.register(hotkey.clone())?;
hotkey_state.registered_hotkey = Some(hotkey); hotkey_state.registered_hotkey = Some(hotkey);
Ok(()) Ok(())
} else { } else {
Err("Hotkey manager not initialized".into()) Err("Hotkey manager not initialized".into())
} }
} }
fn parse_hotkey(shortcut: &[String]) -> Result<HotKey, Box<dyn std::error::Error>> { fn parse_hotkey(shortcut: &[String]) -> Result<HotKey, Box<dyn std::error::Error>> {
let mut modifiers = Modifiers::empty(); let mut modifiers = Modifiers::empty();
let mut code = None; let mut code = None;
for part in shortcut { for part in shortcut {
match part.as_str() { match part.as_str() {
"ControlLeft" => modifiers |= Modifiers::CONTROL, "ControlLeft" => modifiers |= Modifiers::CONTROL,
"AltLeft" => modifiers |= Modifiers::ALT, "AltLeft" => modifiers |= Modifiers::ALT,
"ShiftLeft" => modifiers |= Modifiers::SHIFT, "ShiftLeft" => modifiers |= Modifiers::SHIFT,
"MetaLeft" => modifiers |= Modifiers::META, "MetaLeft" => modifiers |= Modifiers::META,
key => code = Some(Code::from(KeyCode::from_str(key)?)), key => code = Some(Code::from(KeyCode::from_str(key)?)),
} }
} }
let key_code = code.ok_or_else(|| "No valid key code found".to_string())?; let key_code = code.ok_or_else(|| "No valid key code found".to_string())?;
Ok(HotKey::new(Some(modifiers), key_code)) Ok(HotKey::new(Some(modifiers), key_code))
} }
fn handle_hotkey_event(app_handle: &AppHandle) { fn handle_hotkey_event(app_handle: &AppHandle) {
let window = app_handle.get_webview_window("main").unwrap(); let window = app_handle.get_webview_window("main").unwrap();
if window.is_visible().unwrap() { if window.is_visible().unwrap() {
window.hide().unwrap(); window.hide().unwrap();
} else { } else {
window.set_always_on_top(true).unwrap(); window.set_always_on_top(true).unwrap();
window.show().unwrap(); window.show().unwrap();
window.set_focus().unwrap(); window.set_focus().unwrap();
let window_clone = window.clone(); let window_clone = window.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(100)); std::thread::sleep(std::time::Duration::from_millis(100));
window_clone.set_always_on_top(false).unwrap(); window_clone.set_always_on_top(false).unwrap();
}); });
center_window_on_current_monitor(&window); center_window_on_current_monitor(&window);
} }
let _ = app_handle.track_event( let _ = app_handle.track_event(
"hotkey_triggered", "hotkey_triggered",
Some( Some(
serde_json::json!({ serde_json::json!({
"action": if window.is_visible().unwrap() { "hide" } else { "show" } "action": if window.is_visible().unwrap() { "hide" } else { "show" }
}) })
) )
); );
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,12 +1,12 @@
CREATE TABLE IF NOT EXISTS settings ( CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
value TEXT NOT NULL value TEXT NOT NULL
); );
CREATE TABLE IF NOT EXISTS history ( CREATE TABLE IF NOT EXISTS history (
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
content_type TEXT NOT NULL, content_type TEXT NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
favicon TEXT, favicon TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP 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 TEXT DEFAULT 'System' NOT NULL;
ALTER TABLE history ADD COLUMN source_icon TEXT; ALTER TABLE history ADD COLUMN source_icon TEXT;
ALTER TABLE history ADD COLUMN language 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 database;
pub mod history; pub mod history;
pub mod settings; pub mod settings;

View file

@ -1,87 +1,87 @@
use serde::{ Deserialize, Serialize }; use serde::{ Deserialize, Serialize };
use serde_json; use serde_json;
use sqlx::Row; use sqlx::Row;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tauri::{ Emitter, Manager }; use tauri::{ Emitter, Manager };
use tauri_plugin_aptabase::EventTracker; use tauri_plugin_aptabase::EventTracker;
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
struct KeybindSetting { struct KeybindSetting {
keybind: Vec<String>, keybind: Vec<String>,
} }
pub async fn initialize_settings(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> { pub async fn initialize_settings(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
let default_keybind = KeybindSetting { let default_keybind = KeybindSetting {
keybind: vec!["Meta".to_string(), "V".to_string()], keybind: vec!["Meta".to_string(), "V".to_string()],
}; };
let json = serde_json::to_string(&default_keybind)?; let json = serde_json::to_string(&default_keybind)?;
sqlx sqlx
::query("INSERT INTO settings (key, value) VALUES ('keybind', ?)") ::query("INSERT INTO settings (key, value) VALUES ('keybind', ?)")
.bind(json) .bind(json)
.execute(pool).await?; .execute(pool).await?;
Ok(()) Ok(())
} }
#[tauri::command] #[tauri::command]
pub async fn get_setting( pub async fn get_setting(
pool: tauri::State<'_, SqlitePool>, pool: tauri::State<'_, SqlitePool>,
key: String key: String
) -> Result<String, String> { ) -> Result<String, String> {
let row = sqlx let row = sqlx
::query("SELECT value FROM settings WHERE key = ?") ::query("SELECT value FROM settings WHERE key = ?")
.bind(key) .bind(key)
.fetch_optional(&*pool).await .fetch_optional(&*pool).await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
Ok(row.map(|r| r.get("value")).unwrap_or_default()) Ok(row.map(|r| r.get("value")).unwrap_or_default())
} }
#[tauri::command] #[tauri::command]
pub async fn save_setting( pub async fn save_setting(
app_handle: tauri::AppHandle, app_handle: tauri::AppHandle,
pool: tauri::State<'_, SqlitePool>, pool: tauri::State<'_, SqlitePool>,
key: String, key: String,
value: String value: String
) -> Result<(), String> { ) -> Result<(), String> {
sqlx sqlx
::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)") ::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
.bind(key.clone()) .bind(key.clone())
.bind(value.clone()) .bind(value.clone())
.execute(&*pool).await .execute(&*pool).await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let _ = app_handle.track_event( let _ = app_handle.track_event(
"setting_saved", "setting_saved",
Some(serde_json::json!({ Some(serde_json::json!({
"key": key "key": key
})) }))
); );
if key == "keybind" { if key == "keybind" {
let _ = app_handle.emit("update-shortcut", &value).map_err(|e| e.to_string())?; let _ = app_handle.emit("update-shortcut", &value).map_err(|e| e.to_string())?;
} }
Ok(()) Ok(())
} }
#[tauri::command] #[tauri::command]
pub async fn get_keybind(app_handle: tauri::AppHandle) -> Result<Vec<String>, String> { pub async fn get_keybind(app_handle: tauri::AppHandle) -> Result<Vec<String>, String> {
let pool = app_handle.state::<SqlitePool>(); let pool = app_handle.state::<SqlitePool>();
let row = sqlx let row = sqlx
::query("SELECT value FROM settings WHERE key = 'keybind'") ::query("SELECT value FROM settings WHERE key = 'keybind'")
.fetch_optional(&*pool).await .fetch_optional(&*pool).await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let json = row let json = row
.map(|r| r.get::<String, _>("value")) .map(|r| r.get::<String, _>("value"))
.unwrap_or_else(|| { .unwrap_or_else(|| {
serde_json serde_json
::to_string(&vec!["MetaLeft".to_string(), "KeyV".to_string()]) ::to_string(&vec!["MetaLeft".to_string(), "KeyV".to_string()])
.expect("Failed to serialize default keybind") .expect("Failed to serialize default keybind")
}); });
serde_json::from_str::<Vec<String>>(&json).map_err(|e| e.to_string()) 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")] #![cfg_attr(all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows")]
mod api; mod api;
mod db; mod db;
mod utils; mod utils;
use sqlx::sqlite::SqlitePoolOptions; use sqlx::sqlite::SqlitePoolOptions;
use std::fs; use std::fs;
use tauri::Manager; use tauri::Manager;
use tauri_plugin_aptabase::{ EventTracker, InitOptions }; use tauri_plugin_aptabase::{ EventTracker, InitOptions };
use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_prevent_default::Flags; use tauri_plugin_prevent_default::Flags;
fn main() { fn main() {
let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime"); let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
let _guard = runtime.enter(); let _guard = runtime.enter();
tauri::Builder tauri::Builder
::default() ::default()
.plugin(tauri_plugin_clipboard::init()) .plugin(tauri_plugin_clipboard::init())
.plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_sql::Builder::default().build()) .plugin(tauri_plugin_sql::Builder::default().build())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_updater::Builder::default().build()) .plugin(tauri_plugin_updater::Builder::default().build())
.plugin( .plugin(
tauri_plugin_aptabase::Builder tauri_plugin_aptabase::Builder
::new("A-SH-8937252746") ::new("A-SH-8937252746")
.with_options(InitOptions { .with_options(InitOptions {
host: Some("https://aptabase.pandadev.net".to_string()), host: Some("https://aptabase.pandadev.net".to_string()),
flush_interval: None, flush_interval: None,
}) })
.with_panic_hook( .with_panic_hook(
Box::new(|client, info, msg| { Box::new(|client, info, msg| {
let location = info let location = info
.location() .location()
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column())) .map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
.unwrap_or_else(|| "".to_string()); .unwrap_or_else(|| "".to_string());
let _ = client.track_event( let _ = client.track_event(
"panic", "panic",
Some( Some(
serde_json::json!({ serde_json::json!({
"info": format!("{} ({})", msg, location), "info": format!("{} ({})", msg, location),
}) })
) )
); );
}) })
) )
.build() .build()
) )
.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec![]))) .plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec![])))
.plugin( .plugin(
tauri_plugin_prevent_default::Builder tauri_plugin_prevent_default::Builder
::new() ::new()
.with_flags(Flags::all().difference(Flags::CONTEXT_MENU)) .with_flags(Flags::all().difference(Flags::CONTEXT_MENU))
.build() .build()
) )
.setup(|app| { .setup(|app| {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory); app.set_activation_policy(tauri::ActivationPolicy::Accessory);
let app_data_dir = app.path().app_data_dir().unwrap(); let app_data_dir = app.path().app_data_dir().unwrap();
utils::logger::init_logger(&app_data_dir).expect("Failed to initialize logger"); 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"); fs::create_dir_all(&app_data_dir).expect("Failed to create app data directory");
let db_path = app_data_dir.join("data.db"); let db_path = app_data_dir.join("data.db");
let is_new_db = !db_path.exists(); let is_new_db = !db_path.exists();
if is_new_db { if is_new_db {
fs::File::create(&db_path).expect("Failed to create database file"); fs::File::create(&db_path).expect("Failed to create database file");
} }
let db_url = format!("sqlite:{}", db_path.to_str().unwrap()); let db_url = format!("sqlite:{}", db_path.to_str().unwrap());
let app_handle = app.handle().clone(); let app_handle = app.handle().clone();
let app_handle_clone = app_handle.clone(); let app_handle_clone = app_handle.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let pool = SqlitePoolOptions::new() let pool = SqlitePoolOptions::new()
.max_connections(5) .max_connections(5)
.connect(&db_url).await .connect(&db_url).await
.expect("Failed to create pool"); .expect("Failed to create pool");
app_handle_clone.manage(pool); app_handle_clone.manage(pool);
}); });
let main_window = app.get_webview_window("main"); let main_window = app.get_webview_window("main");
let _ = db::database::setup(app); let _ = db::database::setup(app);
api::hotkeys::setup(app_handle.clone()); api::hotkeys::setup(app_handle.clone());
api::tray::setup(app)?; api::tray::setup(app)?;
api::clipboard::setup(app.handle()); api::clipboard::setup(app.handle());
let _ = api::clipboard::start_monitor(app_handle.clone()); let _ = api::clipboard::start_monitor(app_handle.clone());
utils::commands::center_window_on_current_monitor(main_window.as_ref().unwrap()); utils::commands::center_window_on_current_monitor(main_window.as_ref().unwrap());
main_window main_window
.as_ref() .as_ref()
.map(|w| w.hide()) .map(|w| w.hide())
.unwrap_or(Ok(()))?; .unwrap_or(Ok(()))?;
let _ = app.track_event("app_started", None); let _ = app.track_event("app_started", None);
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
api::updater::check_for_updates(app_handle, false).await; api::updater::check_for_updates(app_handle, false).await;
}); });
Ok(()) Ok(())
}) })
.on_window_event(|_app, _event| { .on_window_event(|_app, _event| {
#[cfg(not(dev))] #[cfg(not(dev))]
if let tauri::WindowEvent::Focused(false) = _event { if let tauri::WindowEvent::Focused(false) = _event {
if let Some(window) = _app.get_webview_window("main") { if let Some(window) = _app.get_webview_window("main") {
let _ = window.hide(); let _ = window.hide();
} }
} }
}) })
.invoke_handler( .invoke_handler(
tauri::generate_handler![ tauri::generate_handler![
api::clipboard::write_and_paste, api::clipboard::write_and_paste,
db::history::get_history, db::history::get_history,
db::history::add_history_item, db::history::add_history_item,
db::history::search_history, db::history::search_history,
db::history::load_history_chunk, db::history::load_history_chunk,
db::history::delete_history_item, db::history::delete_history_item,
db::history::clear_history, db::history::clear_history,
db::history::read_image, db::history::read_image,
db::settings::get_setting, db::settings::get_setting,
db::settings::save_setting, db::settings::save_setting,
utils::commands::fetch_page_meta, utils::commands::fetch_page_meta,
utils::commands::get_app_info utils::commands::get_app_info
] ]
) )
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
{ {
// https://nuxt.com/docs/guide/concepts/typescript // https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json" "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'; import type { Key as WaraduKey, useKeyboard } from '@waradu/keyboard';
declare module '#app' { declare module '#app' {
interface NuxtApp { interface NuxtApp {
$keyboard: { $keyboard: {
listen: ReturnType<typeof useKeyboard>['listen']; listen: ReturnType<typeof useKeyboard>['listen'];
init: ReturnType<typeof useKeyboard>['init']; init: ReturnType<typeof useKeyboard>['init'];
Key: typeof WaraduKey; Key: typeof WaraduKey;
currentOS: string; currentOS: string;
clearAll: () => void; clearAll: () => void;
}; };
} }
} }
declare module 'vue' { declare module 'vue' {
interface ComponentCustomProperties { interface ComponentCustomProperties {
$keyboard: { $keyboard: {
listen: ReturnType<typeof useKeyboard>['listen']; listen: ReturnType<typeof useKeyboard>['listen'];
init: ReturnType<typeof useKeyboard>['init']; init: ReturnType<typeof useKeyboard>['init'];
Key: typeof WaraduKey; Key: typeof WaraduKey;
currentOS: string; currentOS: string;
clearAll: () => void; clearAll: () => void;
}; };
} }
} }
export {}; export {};

View file

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

View file

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