mirror of
https://github.com/0PandaDEV/Qopy.git
synced 2025-06-15 19:37:37 +02:00
Compare commits
2 commits
aa928f7094
...
30d6eb60db
Author | SHA1 | Date | |
---|---|---|---|
![]() |
30d6eb60db | ||
![]() |
97c023df91 |
79 changed files with 8565 additions and 8778 deletions
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
@ -1 +1 @@
|
|||
github: 0pandadev
|
||||
github: 0pandadev
|
||||
|
|
150
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
150
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -1,75 +1,75 @@
|
|||
name: "\U0001F41E Bug report"
|
||||
description: Create a report to help me improve Qopy
|
||||
labels: [Bug]
|
||||
assignees:
|
||||
- 0PandaDEV
|
||||
body:
|
||||
#
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
#
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Steps to reproduce the behavior
|
||||
value: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots to help explain your problem.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating system
|
||||
options:
|
||||
- Windows
|
||||
- Linux
|
||||
- macOS
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version of Qopy
|
||||
placeholder: e.g. 0.1.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
validations:
|
||||
required: false
|
||||
name: "\U0001F41E Bug report"
|
||||
description: Create a report to help me improve Qopy
|
||||
labels: [Bug]
|
||||
assignees:
|
||||
- 0PandaDEV
|
||||
body:
|
||||
#
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
#
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Steps to reproduce the behavior
|
||||
value: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots to help explain your problem.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating system
|
||||
options:
|
||||
- Windows
|
||||
- Linux
|
||||
- macOS
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version of Qopy
|
||||
placeholder: e.g. 0.1.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
validations:
|
||||
required: false
|
||||
|
|
38
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
38
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
@ -1,19 +1,19 @@
|
|||
name: "\U0001F4A1 Feature request"
|
||||
description: Suggest an idea for Qopy
|
||||
labels: [Feature]
|
||||
assignees:
|
||||
- 0PandaDEV
|
||||
body:
|
||||
#
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this feature request!
|
||||
#
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe your requested feature
|
||||
description: Give as many details as possible about your feature idea.
|
||||
validations:
|
||||
required: true
|
||||
name: "\U0001F4A1 Feature request"
|
||||
description: Suggest an idea for Qopy
|
||||
labels: [Feature]
|
||||
assignees:
|
||||
- 0PandaDEV
|
||||
body:
|
||||
#
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this feature request!
|
||||
#
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe your requested feature
|
||||
description: Give as many details as possible about your feature idea.
|
||||
validations:
|
||||
required: true
|
||||
|
|
80
.github/scripts/macOS.sh
vendored
80
.github/scripts/macOS.sh
vendored
|
@ -1,40 +1,40 @@
|
|||
#!/bin/bash
|
||||
|
||||
if [ -f .env ]; then
|
||||
export $(cat .env | grep -v '^#' | xargs)
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
required_vars=("APPLE_CERTIFICATE" "APPLE_CERTIFICATE_PASSWORD" "APPLE_ID" "APPLE_ID_PASSWORD" "KEYCHAIN_PASSWORD" "APP_BUNDLE_ID")
|
||||
for var in "${required_vars[@]}"; do
|
||||
if [ -z "${!var}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
bun run tauri build
|
||||
|
||||
rm -f certificate.p12
|
||||
echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12 2>/dev/null
|
||||
security import certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A 2>/dev/null
|
||||
|
||||
SIGNING_IDENTITY=$(security find-identity -v -p codesigning | grep "Apple Development" | head -1 | awk -F '"' '{print $2}')
|
||||
|
||||
if [ -z "$SIGNING_IDENTITY" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
codesign --force --options runtime --sign "$SIGNING_IDENTITY" src-tauri/target/release/bundle/macos/*.app 2>/dev/null
|
||||
|
||||
rm -f certificate.p12
|
||||
|
||||
hdiutil create -volname "Qopy" -srcfolder src-tauri/target/release/bundle/dmg -ov -format UDZO Qopy.dmg
|
||||
|
||||
codesign --force --sign "$APPLE_CERTIFICATE" Qopy.dmg 2>/dev/null
|
||||
|
||||
xcrun notarytool submit Qopy.dmg --apple-id "$APPLE_ID" --password "$APPLE_ID_PASSWORD" --team-id "$APPLE_CERTIFICATE" --wait
|
||||
|
||||
xcrun stapler staple Qopy.dmg
|
||||
|
||||
exit 0
|
||||
#!/bin/bash
|
||||
|
||||
if [ -f .env ]; then
|
||||
export $(cat .env | grep -v '^#' | xargs)
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
required_vars=("APPLE_CERTIFICATE" "APPLE_CERTIFICATE_PASSWORD" "APPLE_ID" "APPLE_ID_PASSWORD" "KEYCHAIN_PASSWORD" "APP_BUNDLE_ID")
|
||||
for var in "${required_vars[@]}"; do
|
||||
if [ -z "${!var}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
bun run tauri build
|
||||
|
||||
rm -f certificate.p12
|
||||
echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12 2>/dev/null
|
||||
security import certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A 2>/dev/null
|
||||
|
||||
SIGNING_IDENTITY=$(security find-identity -v -p codesigning | grep "Apple Development" | head -1 | awk -F '"' '{print $2}')
|
||||
|
||||
if [ -z "$SIGNING_IDENTITY" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
codesign --force --options runtime --sign "$SIGNING_IDENTITY" src-tauri/target/release/bundle/macos/*.app 2>/dev/null
|
||||
|
||||
rm -f certificate.p12
|
||||
|
||||
hdiutil create -volname "Qopy" -srcfolder src-tauri/target/release/bundle/dmg -ov -format UDZO Qopy.dmg
|
||||
|
||||
codesign --force --sign "$APPLE_CERTIFICATE" Qopy.dmg 2>/dev/null
|
||||
|
||||
xcrun notarytool submit Qopy.dmg --apple-id "$APPLE_ID" --password "$APPLE_ID_PASSWORD" --team-id "$APPLE_CERTIFICATE" --wait
|
||||
|
||||
xcrun stapler staple Qopy.dmg
|
||||
|
||||
exit 0
|
||||
|
|
496
.github/workflows/build.yml
vendored
496
.github/workflows/build.yml
vendored
|
@ -1,249 +1,249 @@
|
|||
name: "Nightly Builds"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.VERSION }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: echo "VERSION=$(node -p "require('./src-tauri/tauri.conf.json').version")" >> $GITHUB_OUTPUT
|
||||
|
||||
build-macos:
|
||||
needs: prepare
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- args: "--target aarch64-apple-darwin"
|
||||
arch: "arm64"
|
||||
- args: "--target x86_64-apple-darwin"
|
||||
arch: "x64"
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Redact Sensitive Information
|
||||
run: |
|
||||
function redact_output {
|
||||
sed -e "s/${{ secrets.APPLE_ID }}/REDACTED/g;s/${{ secrets.APPLE_ID_PASSWORD }}/REDACTED/g;s/${{ secrets.APPLE_CERTIFICATE }}/REDACTED/g;s/${{ secrets.APPLE_CERTIFICATE_PASSWORD }}/REDACTED/g;s/${{ secrets.KEYCHAIN_PASSWORD }}/REDACTED/g;s/${{ secrets.PAT }}/REDACTED/g;s/${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}/REDACTED/g"
|
||||
}
|
||||
exec > >(redact_output) 2>&1
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: aarch64-apple-darwin,x86_64-apple-darwin
|
||||
- uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "src-tauri -> target"
|
||||
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
|
||||
shared-key: "macos-rust-cache"
|
||||
save-if: "true"
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
- run: npm install -g pnpm && pnpm install
|
||||
- name: Import Apple Developer Certificate
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
run: |
|
||||
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security set-keychain-settings -lut 7200 build.keychain
|
||||
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security find-identity -v -p codesigning build.keychain
|
||||
- name: Verify Certificate
|
||||
run: |
|
||||
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Apple Development")
|
||||
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
||||
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
|
||||
echo "Certificate imported."
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
||||
with:
|
||||
args: ${{ matrix.args }}
|
||||
- name: Debug Signing Process
|
||||
if: failure()
|
||||
run: |
|
||||
echo "Attempting manual signing:"
|
||||
timeout 300 codesign --force --options runtime --sign "$CERT_ID" --entitlements src-tauri/entitlements.plist src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/macos/Qopy.app
|
||||
echo "Verifying signature:"
|
||||
codesign -dv --verbose=4 "src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/macos/Qopy.app" | sed 's/.*Authority=.*/Authority=REDACTED/'
|
||||
- name: Set architecture label
|
||||
run: |
|
||||
if [[ "${{ matrix.args }}" == "--target aarch64-apple-darwin" ]]; then
|
||||
echo "ARCH_LABEL=aarch64-apple-darwin" >> $GITHUB_ENV
|
||||
else
|
||||
echo "ARCH_LABEL=x86_64-apple-darwin" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Rename and Publish macOS Artifacts
|
||||
run: |
|
||||
mv src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/dmg/*.dmg src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/dmg/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.dmg
|
||||
mv src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/*.app.tar.gz src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.app.tar.gz
|
||||
mv src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/*.app.tar.gz.sig src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.app.tar.gz.sig
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-dmg-${{ matrix.arch }}
|
||||
path: "src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/dmg/*.dmg"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: updater-macos-${{ matrix.arch }}
|
||||
path: |
|
||||
src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/*.app.tar.gz
|
||||
src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/*.app.tar.gz.sig
|
||||
|
||||
build-windows:
|
||||
needs: prepare
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- args: "--target x86_64-pc-windows-msvc"
|
||||
arch: "x64"
|
||||
target: "x86_64-pc-windows-msvc"
|
||||
- args: "--target aarch64-pc-windows-msvc"
|
||||
arch: "arm64"
|
||||
target: "aarch64-pc-windows-msvc"
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: x86_64-pc-windows-msvc,aarch64-pc-windows-msvc
|
||||
- uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "src-tauri -> target"
|
||||
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
|
||||
shared-key: "windows-rust-cache"
|
||||
save-if: "true"
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
- run: npm install -g pnpm && pnpm install
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: ${{ matrix.args }}
|
||||
- name: List Bundle Directory
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Output "Checking build directories..."
|
||||
Get-ChildItem -Path "src-tauri/target" -Recurse -Directory | Where-Object { $_.Name -eq "msi" } | ForEach-Object {
|
||||
Write-Output "Found MSI directory: $($_.FullName)"
|
||||
Get-ChildItem -Path $_.FullName -Filter "*.msi" | ForEach-Object {
|
||||
Write-Output "Found MSI file: $($_.FullName)"
|
||||
}
|
||||
}
|
||||
- name: Rename and Publish Windows Artifacts
|
||||
run: |
|
||||
mv src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi src-tauri/target/${{ matrix.target }}/release/bundle/msi/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.msi
|
||||
mv src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi.sig src-tauri/target/${{ matrix.target }}/release/bundle/msi/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.msi.sig
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-${{ matrix.arch }}
|
||||
path: src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: updater-windows-${{ matrix.arch }}
|
||||
path: |
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi.sig
|
||||
|
||||
build-ubuntu:
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: x86_64-unknown-linux-gnu
|
||||
- uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "src-tauri -> target"
|
||||
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
|
||||
shared-key: "ubuntu-rust-cache"
|
||||
save-if: "true"
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y libwebkit2gtk-4.1-dev build-essential curl wget file libssl-dev libayatana-appindicator3-dev librsvg2-dev libasound2-dev rpm
|
||||
echo "PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig" >> $GITHUB_ENV
|
||||
- run: npm install -g pnpm && pnpm install
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: --target x86_64-unknown-linux-gnu
|
||||
- name: Rename Linux Artifacts
|
||||
run: |
|
||||
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/Qopy-${{ needs.prepare.outputs.version }}.deb
|
||||
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/Qopy-${{ needs.prepare.outputs.version }}.AppImage
|
||||
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/Qopy-${{ needs.prepare.outputs.version }}.AppImage.sig
|
||||
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/Qopy-${{ needs.prepare.outputs.version }}.rpm
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ubuntu-deb
|
||||
path: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ubuntu-appimage
|
||||
path: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ubuntu-rpm
|
||||
path: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: updater-ubuntu
|
||||
path: |
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
|
||||
name: "Nightly Builds"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.VERSION }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: echo "VERSION=$(node -p "require('./src-tauri/tauri.conf.json').version")" >> $GITHUB_OUTPUT
|
||||
|
||||
build-macos:
|
||||
needs: prepare
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- args: "--target aarch64-apple-darwin"
|
||||
arch: "arm64"
|
||||
- args: "--target x86_64-apple-darwin"
|
||||
arch: "x64"
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Redact Sensitive Information
|
||||
run: |
|
||||
function redact_output {
|
||||
sed -e "s/${{ secrets.APPLE_ID }}/REDACTED/g;s/${{ secrets.APPLE_ID_PASSWORD }}/REDACTED/g;s/${{ secrets.APPLE_CERTIFICATE }}/REDACTED/g;s/${{ secrets.APPLE_CERTIFICATE_PASSWORD }}/REDACTED/g;s/${{ secrets.KEYCHAIN_PASSWORD }}/REDACTED/g;s/${{ secrets.PAT }}/REDACTED/g;s/${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}/REDACTED/g"
|
||||
}
|
||||
exec > >(redact_output) 2>&1
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: aarch64-apple-darwin,x86_64-apple-darwin
|
||||
- uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "src-tauri -> target"
|
||||
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
|
||||
shared-key: "macos-rust-cache"
|
||||
save-if: "true"
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
- run: npm install -g pnpm && pnpm install
|
||||
- name: Import Apple Developer Certificate
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
run: |
|
||||
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security set-keychain-settings -lut 7200 build.keychain
|
||||
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security find-identity -v -p codesigning build.keychain
|
||||
- name: Verify Certificate
|
||||
run: |
|
||||
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Apple Development")
|
||||
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
||||
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
|
||||
echo "Certificate imported."
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
||||
with:
|
||||
args: ${{ matrix.args }}
|
||||
- name: Debug Signing Process
|
||||
if: failure()
|
||||
run: |
|
||||
echo "Attempting manual signing:"
|
||||
timeout 300 codesign --force --options runtime --sign "$CERT_ID" --entitlements src-tauri/entitlements.plist src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/macos/Qopy.app
|
||||
echo "Verifying signature:"
|
||||
codesign -dv --verbose=4 "src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/macos/Qopy.app" | sed 's/.*Authority=.*/Authority=REDACTED/'
|
||||
- name: Set architecture label
|
||||
run: |
|
||||
if [[ "${{ matrix.args }}" == "--target aarch64-apple-darwin" ]]; then
|
||||
echo "ARCH_LABEL=aarch64-apple-darwin" >> $GITHUB_ENV
|
||||
else
|
||||
echo "ARCH_LABEL=x86_64-apple-darwin" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Rename and Publish macOS Artifacts
|
||||
run: |
|
||||
mv src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/dmg/*.dmg src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/dmg/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.dmg
|
||||
mv src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/*.app.tar.gz src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.app.tar.gz
|
||||
mv src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/*.app.tar.gz.sig src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.app.tar.gz.sig
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-dmg-${{ matrix.arch }}
|
||||
path: "src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/dmg/*.dmg"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: updater-macos-${{ matrix.arch }}
|
||||
path: |
|
||||
src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/*.app.tar.gz
|
||||
src-tauri/target/${{ env.ARCH_LABEL }}/release/bundle/macos/*.app.tar.gz.sig
|
||||
|
||||
build-windows:
|
||||
needs: prepare
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- args: "--target x86_64-pc-windows-msvc"
|
||||
arch: "x64"
|
||||
target: "x86_64-pc-windows-msvc"
|
||||
- args: "--target aarch64-pc-windows-msvc"
|
||||
arch: "arm64"
|
||||
target: "aarch64-pc-windows-msvc"
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: x86_64-pc-windows-msvc,aarch64-pc-windows-msvc
|
||||
- uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "src-tauri -> target"
|
||||
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
|
||||
shared-key: "windows-rust-cache"
|
||||
save-if: "true"
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
- run: npm install -g pnpm && pnpm install
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: ${{ matrix.args }}
|
||||
- name: List Bundle Directory
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Output "Checking build directories..."
|
||||
Get-ChildItem -Path "src-tauri/target" -Recurse -Directory | Where-Object { $_.Name -eq "msi" } | ForEach-Object {
|
||||
Write-Output "Found MSI directory: $($_.FullName)"
|
||||
Get-ChildItem -Path $_.FullName -Filter "*.msi" | ForEach-Object {
|
||||
Write-Output "Found MSI file: $($_.FullName)"
|
||||
}
|
||||
}
|
||||
- name: Rename and Publish Windows Artifacts
|
||||
run: |
|
||||
mv src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi src-tauri/target/${{ matrix.target }}/release/bundle/msi/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.msi
|
||||
mv src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi.sig src-tauri/target/${{ matrix.target }}/release/bundle/msi/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.msi.sig
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-${{ matrix.arch }}
|
||||
path: src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: updater-windows-${{ matrix.arch }}
|
||||
path: |
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi.sig
|
||||
|
||||
build-ubuntu:
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: x86_64-unknown-linux-gnu
|
||||
- uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "src-tauri -> target"
|
||||
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
|
||||
shared-key: "ubuntu-rust-cache"
|
||||
save-if: "true"
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y libwebkit2gtk-4.1-dev build-essential curl wget file libssl-dev libayatana-appindicator3-dev librsvg2-dev libasound2-dev rpm
|
||||
echo "PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig" >> $GITHUB_ENV
|
||||
- run: npm install -g pnpm && pnpm install
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: --target x86_64-unknown-linux-gnu
|
||||
- name: Rename Linux Artifacts
|
||||
run: |
|
||||
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/Qopy-${{ needs.prepare.outputs.version }}.deb
|
||||
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/Qopy-${{ needs.prepare.outputs.version }}.AppImage
|
||||
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/Qopy-${{ needs.prepare.outputs.version }}.AppImage.sig
|
||||
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/Qopy-${{ needs.prepare.outputs.version }}.rpm
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ubuntu-deb
|
||||
path: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ubuntu-appimage
|
||||
path: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ubuntu-rpm
|
||||
path: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: updater-ubuntu
|
||||
path: |
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
|
648
.github/workflows/release.yml
vendored
648
.github/workflows/release.yml
vendored
|
@ -1,325 +1,325 @@
|
|||
name: "Release"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
permissions: write-all
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.VERSION }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=$(node -p 'require("./src-tauri/tauri.conf.json").version')
|
||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
build-macos:
|
||||
permissions: write-all
|
||||
needs: prepare
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- args: "--target aarch64-apple-darwin"
|
||||
arch: "silicon"
|
||||
- args: "--target x86_64-apple-darwin"
|
||||
arch: "intel"
|
||||
runs-on: macos-latest
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Redact Sensitive Information
|
||||
run: |
|
||||
function redact_output {
|
||||
sed -e "s/${{ secrets.REDACT_PATTERN }}/REDACTED/g"
|
||||
}
|
||||
exec > >(redact_output) 2>&1
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: aarch64-apple-darwin,x86_64-apple-darwin
|
||||
- uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "src-tauri -> target"
|
||||
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
|
||||
shared-key: "macos-rust-cache"
|
||||
save-if: "true"
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
- run: npm install -g pnpm && pnpm install
|
||||
- name: Import Apple Developer Certificate
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
run: |
|
||||
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
with:
|
||||
args: ${{ matrix.args }}
|
||||
|
||||
- name: Rename macOS Artifacts
|
||||
run: |
|
||||
mv src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/dmg/*.dmg src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/dmg/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.dmg
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-${{ matrix.arch }}-binaries
|
||||
path: |
|
||||
src-tauri/target/**/release/bundle/dmg/*.dmg
|
||||
|
||||
build-windows:
|
||||
permissions: write-all
|
||||
needs: prepare
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- args: "--target x86_64-pc-windows-msvc"
|
||||
arch: "x64"
|
||||
target: "x86_64-pc-windows-msvc"
|
||||
- args: "--target aarch64-pc-windows-msvc"
|
||||
arch: "arm64"
|
||||
target: "aarch64-pc-windows-msvc"
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: x86_64-pc-windows-msvc,aarch64-pc-windows-msvc
|
||||
- uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "src-tauri -> target"
|
||||
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
|
||||
shared-key: "windows-rust-cache"
|
||||
save-if: "true"
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
- run: npm install -g pnpm && pnpm install
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: ${{ matrix.args }}
|
||||
- name: List Bundle Directory
|
||||
shell: pwsh
|
||||
run: |
|
||||
$bundlePath = "src-tauri/target/${{ matrix.target }}/release/bundle/msi"
|
||||
if (Test-Path $bundlePath) {
|
||||
Write-Output "Contents of ${bundlePath}:"
|
||||
Get-ChildItem -Path $bundlePath
|
||||
} else {
|
||||
Write-Output "Path ${bundlePath} does not exist."
|
||||
}
|
||||
- name: Rename Windows Artifacts
|
||||
shell: pwsh
|
||||
run: |
|
||||
$bundlePath = "src-tauri/target/${{ matrix.target }}/release/bundle/msi"
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
$arch = "${{ matrix.arch }}"
|
||||
if (Test-Path $bundlePath) {
|
||||
$msiFiles = Get-ChildItem -Path "$bundlePath/*.msi"
|
||||
foreach ($file in $msiFiles) {
|
||||
$newName = "Qopy-$version`_$arch.msi"
|
||||
Rename-Item -Path $file.FullName -NewName $newName
|
||||
}
|
||||
} else {
|
||||
Write-Error "Path ${bundlePath} does not exist."
|
||||
exit 1
|
||||
}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-${{ matrix.arch }}-binaries
|
||||
path: src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi
|
||||
|
||||
build-linux:
|
||||
permissions: write-all
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "src-tauri -> target"
|
||||
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
|
||||
shared-key: "linux-rust-cache"
|
||||
save-if: "true"
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y libwebkit2gtk-4.1-dev build-essential curl wget file libssl-dev libayatana-appindicator3-dev librsvg2-dev libasound2-dev rpm
|
||||
echo "PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig" >> $GITHUB_ENV
|
||||
- run: npm install -g pnpm && pnpm install
|
||||
- name: Generate Changelog
|
||||
id: changelog
|
||||
run: |
|
||||
CHANGELOG=$(git log $(git describe --tags --abbrev=0)..HEAD --pretty=format:"- %s")
|
||||
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
|
||||
echo "$CHANGELOG" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: --target x86_64-unknown-linux-gnu
|
||||
- name: Rename Linux Artifacts
|
||||
run: |
|
||||
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/Qopy-${{ needs.prepare.outputs.version }}.deb
|
||||
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/Qopy-${{ needs.prepare.outputs.version }}.AppImage
|
||||
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/Qopy-${{ needs.prepare.outputs.version }}.rpm
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-binaries
|
||||
path: |
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
|
||||
|
||||
create-release:
|
||||
permissions: write-all
|
||||
needs: [prepare, build-macos, build-windows, build-linux]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.PAT }}
|
||||
|
||||
- name: Check if release already exists
|
||||
id: check_release
|
||||
run: |
|
||||
VERSION="${{ needs.prepare.outputs.version }}"
|
||||
RELEASE_EXISTS=$(gh release view v$VERSION --json id --jq '.id' 2>/dev/null || echo "")
|
||||
if [ -n "$RELEASE_EXISTS" ]; then
|
||||
echo "SKIP_RELEASE=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "SKIP_RELEASE=false" >> $GITHUB_ENV
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
|
||||
- name: Download all artifacts
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Update CHANGELOG
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
id: changelog
|
||||
uses: requarks/changelog-action@v1
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
tag: ${{ github.ref_name }}
|
||||
|
||||
- name: Generate Release Body
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
id: release_body
|
||||
run: |
|
||||
VERSION="${{ needs.prepare.outputs.version }}"
|
||||
|
||||
# Calculate hashes with corrected paths
|
||||
WINDOWS_ARM_HASH=$(sha256sum "artifacts/windows-arm64-binaries/Qopy-${VERSION}_arm64.msi" | awk '{ print $1 }')
|
||||
WINDOWS_64_HASH=$(sha256sum "artifacts/windows-x64-binaries/Qopy-${VERSION}_x64.msi" | awk '{ print $1 }')
|
||||
MAC_SILICON_HASH=$(sha256sum "artifacts/macos-silicon-binaries/aarch64-apple-darwin/release/bundle/dmg/Qopy-${VERSION}_silicon.dmg" | awk '{ print $1 }')
|
||||
MAC_INTEL_HASH=$(sha256sum "artifacts/macos-intel-binaries/x86_64-apple-darwin/release/bundle/dmg/Qopy-${VERSION}_intel.dmg" | awk '{ print $1 }')
|
||||
DEBIAN_HASH=$(sha256sum "artifacts/linux-binaries/deb/Qopy-${VERSION}.deb" | awk '{ print $1 }')
|
||||
APPIMAGE_HASH=$(sha256sum "artifacts/linux-binaries/appimage/Qopy-${VERSION}.AppImage" | awk '{ print $1 }')
|
||||
REDHAT_HASH=$(sha256sum "artifacts/linux-binaries/rpm/Qopy-${VERSION}.rpm" | awk '{ print $1 }')
|
||||
|
||||
# Debug output
|
||||
echo "Calculated hashes:"
|
||||
echo "Windows ARM: $WINDOWS_ARM_HASH"
|
||||
echo "Windows x64: $WINDOWS_64_HASH"
|
||||
echo "Mac Silicon: $MAC_SILICON_HASH"
|
||||
echo "Mac Intel: $MAC_INTEL_HASH"
|
||||
echo "Debian: $DEBIAN_HASH"
|
||||
echo "AppImage: $APPIMAGE_HASH"
|
||||
echo "Red Hat: $REDHAT_HASH"
|
||||
|
||||
RELEASE_BODY=$(cat <<-EOF
|
||||
|
||||
${{ needs.create-release.outputs.changelog }}
|
||||
|
||||
## ⬇️ Downloads
|
||||
|
||||
- [Windows (x64)](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}_x64.msi) - ${WINDOWS_64_HASH}
|
||||
- [Windows (ARM64)](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}_arm64.msi) - ${WINDOWS_ARM_HASH}
|
||||
- [macOS (Silicon)](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}_silicon.dmg) - ${MAC_SILICON_HASH}
|
||||
- [macOS (Intel)](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}_intel.dmg) - ${MAC_INTEL_HASH}
|
||||
- [Debian](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}.deb) - ${DEBIAN_HASH}
|
||||
- [AppImage](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}.AppImage) - ${APPIMAGE_HASH}
|
||||
- [Red Hat](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}.rpm) - ${REDHAT_HASH}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "RELEASE_BODY<<EOF" >> $GITHUB_ENV
|
||||
echo "$RELEASE_BODY" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Release
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
with:
|
||||
draft: true
|
||||
tag_name: v${{ needs.prepare.outputs.version }}
|
||||
name: v${{ needs.prepare.outputs.version }}
|
||||
files: |
|
||||
artifacts/**/*.dmg
|
||||
artifacts/**/*.msi
|
||||
artifacts/**/*.deb
|
||||
artifacts/**/*.AppImage
|
||||
artifacts/**/*.rpm
|
||||
name: "Release"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
permissions: write-all
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.VERSION }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=$(node -p 'require("./src-tauri/tauri.conf.json").version')
|
||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
build-macos:
|
||||
permissions: write-all
|
||||
needs: prepare
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- args: "--target aarch64-apple-darwin"
|
||||
arch: "silicon"
|
||||
- args: "--target x86_64-apple-darwin"
|
||||
arch: "intel"
|
||||
runs-on: macos-latest
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Redact Sensitive Information
|
||||
run: |
|
||||
function redact_output {
|
||||
sed -e "s/${{ secrets.REDACT_PATTERN }}/REDACTED/g"
|
||||
}
|
||||
exec > >(redact_output) 2>&1
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: aarch64-apple-darwin,x86_64-apple-darwin
|
||||
- uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "src-tauri -> target"
|
||||
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
|
||||
shared-key: "macos-rust-cache"
|
||||
save-if: "true"
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
- run: npm install -g pnpm && pnpm install
|
||||
- name: Import Apple Developer Certificate
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
run: |
|
||||
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
with:
|
||||
args: ${{ matrix.args }}
|
||||
|
||||
- name: Rename macOS Artifacts
|
||||
run: |
|
||||
mv src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/dmg/*.dmg src-tauri/target/${{ matrix.args == '--target aarch64-apple-darwin' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin' }}/release/bundle/dmg/Qopy-${{ needs.prepare.outputs.version }}_${{ matrix.arch }}.dmg
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-${{ matrix.arch }}-binaries
|
||||
path: |
|
||||
src-tauri/target/**/release/bundle/dmg/*.dmg
|
||||
|
||||
build-windows:
|
||||
permissions: write-all
|
||||
needs: prepare
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- args: "--target x86_64-pc-windows-msvc"
|
||||
arch: "x64"
|
||||
target: "x86_64-pc-windows-msvc"
|
||||
- args: "--target aarch64-pc-windows-msvc"
|
||||
arch: "arm64"
|
||||
target: "aarch64-pc-windows-msvc"
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: x86_64-pc-windows-msvc,aarch64-pc-windows-msvc
|
||||
- uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "src-tauri -> target"
|
||||
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
|
||||
shared-key: "windows-rust-cache"
|
||||
save-if: "true"
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
- run: npm install -g pnpm && pnpm install
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: ${{ matrix.args }}
|
||||
- name: List Bundle Directory
|
||||
shell: pwsh
|
||||
run: |
|
||||
$bundlePath = "src-tauri/target/${{ matrix.target }}/release/bundle/msi"
|
||||
if (Test-Path $bundlePath) {
|
||||
Write-Output "Contents of ${bundlePath}:"
|
||||
Get-ChildItem -Path $bundlePath
|
||||
} else {
|
||||
Write-Output "Path ${bundlePath} does not exist."
|
||||
}
|
||||
- name: Rename Windows Artifacts
|
||||
shell: pwsh
|
||||
run: |
|
||||
$bundlePath = "src-tauri/target/${{ matrix.target }}/release/bundle/msi"
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
$arch = "${{ matrix.arch }}"
|
||||
if (Test-Path $bundlePath) {
|
||||
$msiFiles = Get-ChildItem -Path "$bundlePath/*.msi"
|
||||
foreach ($file in $msiFiles) {
|
||||
$newName = "Qopy-$version`_$arch.msi"
|
||||
Rename-Item -Path $file.FullName -NewName $newName
|
||||
}
|
||||
} else {
|
||||
Write-Error "Path ${bundlePath} does not exist."
|
||||
exit 1
|
||||
}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-${{ matrix.arch }}-binaries
|
||||
path: src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi
|
||||
|
||||
build-linux:
|
||||
permissions: write-all
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "src-tauri -> target"
|
||||
cache-directories: "~/.cargo/registry/index/,~/.cargo/registry/cache/,~/.cargo/git/db/"
|
||||
shared-key: "linux-rust-cache"
|
||||
save-if: "true"
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y libwebkit2gtk-4.1-dev build-essential curl wget file libssl-dev libayatana-appindicator3-dev librsvg2-dev libasound2-dev rpm
|
||||
echo "PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig" >> $GITHUB_ENV
|
||||
- run: npm install -g pnpm && pnpm install
|
||||
- name: Generate Changelog
|
||||
id: changelog
|
||||
run: |
|
||||
CHANGELOG=$(git log $(git describe --tags --abbrev=0)..HEAD --pretty=format:"- %s")
|
||||
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
|
||||
echo "$CHANGELOG" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: --target x86_64-unknown-linux-gnu
|
||||
- name: Rename Linux Artifacts
|
||||
run: |
|
||||
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/Qopy-${{ needs.prepare.outputs.version }}.deb
|
||||
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/Qopy-${{ needs.prepare.outputs.version }}.AppImage
|
||||
mv src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/Qopy-${{ needs.prepare.outputs.version }}.rpm
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-binaries
|
||||
path: |
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
|
||||
|
||||
create-release:
|
||||
permissions: write-all
|
||||
needs: [prepare, build-macos, build-windows, build-linux]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.PAT }}
|
||||
|
||||
- name: Check if release already exists
|
||||
id: check_release
|
||||
run: |
|
||||
VERSION="${{ needs.prepare.outputs.version }}"
|
||||
RELEASE_EXISTS=$(gh release view v$VERSION --json id --jq '.id' 2>/dev/null || echo "")
|
||||
if [ -n "$RELEASE_EXISTS" ]; then
|
||||
echo "SKIP_RELEASE=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "SKIP_RELEASE=false" >> $GITHUB_ENV
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
|
||||
- name: Download all artifacts
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Update CHANGELOG
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
id: changelog
|
||||
uses: requarks/changelog-action@v1
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
tag: ${{ github.ref_name }}
|
||||
|
||||
- name: Generate Release Body
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
id: release_body
|
||||
run: |
|
||||
VERSION="${{ needs.prepare.outputs.version }}"
|
||||
|
||||
# Calculate hashes with corrected paths
|
||||
WINDOWS_ARM_HASH=$(sha256sum "artifacts/windows-arm64-binaries/Qopy-${VERSION}_arm64.msi" | awk '{ print $1 }')
|
||||
WINDOWS_64_HASH=$(sha256sum "artifacts/windows-x64-binaries/Qopy-${VERSION}_x64.msi" | awk '{ print $1 }')
|
||||
MAC_SILICON_HASH=$(sha256sum "artifacts/macos-silicon-binaries/aarch64-apple-darwin/release/bundle/dmg/Qopy-${VERSION}_silicon.dmg" | awk '{ print $1 }')
|
||||
MAC_INTEL_HASH=$(sha256sum "artifacts/macos-intel-binaries/x86_64-apple-darwin/release/bundle/dmg/Qopy-${VERSION}_intel.dmg" | awk '{ print $1 }')
|
||||
DEBIAN_HASH=$(sha256sum "artifacts/linux-binaries/deb/Qopy-${VERSION}.deb" | awk '{ print $1 }')
|
||||
APPIMAGE_HASH=$(sha256sum "artifacts/linux-binaries/appimage/Qopy-${VERSION}.AppImage" | awk '{ print $1 }')
|
||||
REDHAT_HASH=$(sha256sum "artifacts/linux-binaries/rpm/Qopy-${VERSION}.rpm" | awk '{ print $1 }')
|
||||
|
||||
# Debug output
|
||||
echo "Calculated hashes:"
|
||||
echo "Windows ARM: $WINDOWS_ARM_HASH"
|
||||
echo "Windows x64: $WINDOWS_64_HASH"
|
||||
echo "Mac Silicon: $MAC_SILICON_HASH"
|
||||
echo "Mac Intel: $MAC_INTEL_HASH"
|
||||
echo "Debian: $DEBIAN_HASH"
|
||||
echo "AppImage: $APPIMAGE_HASH"
|
||||
echo "Red Hat: $REDHAT_HASH"
|
||||
|
||||
RELEASE_BODY=$(cat <<-EOF
|
||||
|
||||
${{ needs.create-release.outputs.changelog }}
|
||||
|
||||
## ⬇️ Downloads
|
||||
|
||||
- [Windows (x64)](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}_x64.msi) - ${WINDOWS_64_HASH}
|
||||
- [Windows (ARM64)](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}_arm64.msi) - ${WINDOWS_ARM_HASH}
|
||||
- [macOS (Silicon)](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}_silicon.dmg) - ${MAC_SILICON_HASH}
|
||||
- [macOS (Intel)](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}_intel.dmg) - ${MAC_INTEL_HASH}
|
||||
- [Debian](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}.deb) - ${DEBIAN_HASH}
|
||||
- [AppImage](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}.AppImage) - ${APPIMAGE_HASH}
|
||||
- [Red Hat](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}.rpm) - ${REDHAT_HASH}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "RELEASE_BODY<<EOF" >> $GITHUB_ENV
|
||||
echo "$RELEASE_BODY" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Release
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
with:
|
||||
draft: true
|
||||
tag_name: v${{ needs.prepare.outputs.version }}
|
||||
name: v${{ needs.prepare.outputs.version }}
|
||||
files: |
|
||||
artifacts/**/*.dmg
|
||||
artifacts/**/*.msi
|
||||
artifacts/**/*.deb
|
||||
artifacts/**/*.AppImage
|
||||
artifacts/**/*.rpm
|
||||
body: ${{ env.RELEASE_BODY }}
|
54
.gitignore
vendored
54
.gitignore
vendored
|
@ -1,28 +1,28 @@
|
|||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
bun.lockb
|
||||
.gitignore
|
||||
.vscode
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
bun.lockb
|
||||
.gitignore
|
||||
.vscode
|
||||
bun.lock
|
|
@ -1,23 +1,23 @@
|
|||
# Get Started
|
||||
|
||||
The default hotkey for Qopy is Windows+V which is also the hotkey for the default clipboard manager to turn that off follow [this guide](https://github.com/0PandaDEV/Qopy/blob/main/GET_STARTED.md#disable-windowsv-for-default-clipboard-manager).
|
||||
|
||||
All the data of Qopy is stored inside of a SQLite database.
|
||||
|
||||
| Operating System | Path |
|
||||
|------------------|-----------------------------------------------------------------|
|
||||
| Windows | `C:\Users\USERNAME\AppData\Roaming\net.pandadev.qopy` |
|
||||
| macOS | `/Users/USERNAME/Library/Application Support/net.pandadev.qopy` |
|
||||
| Linux | `/home/USERNAME/.local/share/net.pandadev.qopy` |
|
||||
|
||||
## Disable Windows+V for default clipboard manager
|
||||
|
||||
<video src="https://github.com/user-attachments/assets/723f9e07-3190-46ec-9bb7-15dfc112f620" controls title="Disable Windows+V for default clipboard manager"></video>
|
||||
|
||||
To disable the default clipboard manager popup from windows open Command prompt and run this command
|
||||
|
||||
```cmd
|
||||
reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\System" /v AllowClipboardHistory /t REG_DWORD /d 0 /f
|
||||
```
|
||||
|
||||
After that a restart may be reqired.
|
||||
# Get Started
|
||||
|
||||
The default hotkey for Qopy is Windows+V which is also the hotkey for the default clipboard manager to turn that off follow [this guide](https://github.com/0PandaDEV/Qopy/blob/main/GET_STARTED.md#disable-windowsv-for-default-clipboard-manager).
|
||||
|
||||
All the data of Qopy is stored inside of a SQLite database.
|
||||
|
||||
| Operating System | Path |
|
||||
|------------------|-----------------------------------------------------------------|
|
||||
| Windows | `C:\Users\USERNAME\AppData\Roaming\net.pandadev.qopy` |
|
||||
| macOS | `/Users/USERNAME/Library/Application Support/net.pandadev.qopy` |
|
||||
| Linux | `/home/USERNAME/.local/share/net.pandadev.qopy` |
|
||||
|
||||
## Disable Windows+V for default clipboard manager
|
||||
|
||||
<video src="https://github.com/user-attachments/assets/723f9e07-3190-46ec-9bb7-15dfc112f620" controls title="Disable Windows+V for default clipboard manager"></video>
|
||||
|
||||
To disable the default clipboard manager popup from windows open Command prompt and run this command
|
||||
|
||||
```cmd
|
||||
reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\System" /v AllowClipboardHistory /t REG_DWORD /d 0 /f
|
||||
```
|
||||
|
||||
After that a restart may be reqired.
|
||||
|
|
258
README.md
258
README.md
|
@ -1,129 +1,129 @@
|
|||
<div align="center">
|
||||
|
||||
<img align="center" width="128px" src="src-tauri/icons/icon.png" />
|
||||
<h1 align="center"><b>Qopy</b></h1>
|
||||
|
||||
The fixed and simple clipboard manager for both Windows and Linux.
|
||||
|
||||
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_x64.msi">
|
||||
<img src="./public/windows.png"> Windows (x64)
|
||||
</a>
|
||||
•
|
||||
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_arm64.msi">
|
||||
Windows (arm64)
|
||||
</a>
|
||||
<br>
|
||||
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3.deb">
|
||||
<img src="./public/linux.png"> Linux (deb)
|
||||
</a>
|
||||
•
|
||||
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3.rpm">
|
||||
Linux (rpm)
|
||||
</a>
|
||||
•
|
||||
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3.AppImage">
|
||||
Linux (AppImage)
|
||||
</a>
|
||||
<br>
|
||||
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_silicon.dmg">
|
||||
<img src="./public/apple.png"> macOS (Silicon)
|
||||
</a>
|
||||
•
|
||||
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_intel.dmg">
|
||||
macOS (Intel)
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
<sup>Nightly releases can be found <a href="https://github.com/0PandaDEV/qopy/actions/workflows/build.yml">here</a> </sup>
|
||||
|
||||
</div>
|
||||
|
||||
[discord »](https://discord.gg/invite/Y7SbYphVw9)
|
||||
|
||||
> \[!IMPORTANT]
|
||||
>
|
||||
> **Star this project**, You will receive all release notifications from GitHub without any delay \~ ⭐️
|
||||
|
||||
<details>
|
||||
<summary><kbd>Star History</kbd></summary>
|
||||
<a href="https://starchart.cc/0PandaDEV/Qopy">
|
||||
<picture>
|
||||
<img width="100%" src="https://starchart.cc/0PandaDEV/Qopy.svg?variant=adaptive">
|
||||
</picture>
|
||||
</a>
|
||||
</details>
|
||||
|
||||
[](https://wakatime.com/badge/user/018ce503-097f-4057-9599-db20b190920c/project/fe76359d-56c2-4a13-8413-55207b6ad298)
|
||||
|
||||
## 📋 What is Qopy
|
||||
|
||||
Qopy is a fixed clipboard manager designed as a simple alternative to the standard clipboard on Windows. It aims to provide a faster, more reliable experience while providing an extensive set of features compared to its Windows counterpart.
|
||||
|
||||
## 🚧 Roadmap
|
||||
- [x] [Setup guide](https://github.com/0PandaDEV/Qopy/blob/main/GET_STARTED.md)
|
||||
- [ ] Sync Clipboard across devices https://github.com/0PandaDEV/Qopy/issues/8
|
||||
- [x] Settings https://github.com/0PandaDEV/Qopy/issues/2
|
||||
- [x] Metadata for copied items https://github.com/0PandaDEV/Qopy/issues/5
|
||||
- [ ] Code highlighting https://github.com/0PandaDEV/Qopy/issues/7
|
||||
- [ ] Streamshare integration https://github.com/0PandaDEV/Qopy/issues/4
|
||||
- [ ] Content type filter https://github.com/0PandaDEV/Qopy/issues/16
|
||||
- [ ] Preview for copied files https://github.com/0PandaDEV/Qopy/issues/15
|
||||
- [ ] Convert files to other formats https://github.com/0PandaDEV/Qopy/issues/17
|
||||
- [x] Option for custom keybind https://github.com/0PandaDEV/Qopy/issues/3
|
||||
- [x] macOS Support https://github.com/0PandaDEV/Qopy/issues/13
|
||||
|
||||
<sup>If you have ideas for features to include, please write a feature request [here](https://github.com/0pandadev/Qopy/issues).</sup>
|
||||
|
||||
## 📦 Concepts
|
||||
|
||||
Here you can see a few concepts these might not be implemented:
|
||||
|
||||

|
||||

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

|
||||

|
||||
|
||||
|
||||
## ❤️ Donations & Support
|
||||
|
||||
Qopy is open-source and free to use. I appreciate donations to support ongoing development and improvements. Your contributions are voluntary and help me enhance the app for everyone.
|
||||
|
||||
<a href="https://buymeacoffee.com/pandadev_"><img src="https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black"/></a>
|
||||
|
||||
## ⌨️ Local development
|
||||
|
||||
You can use GitHub Codespaces for online development:
|
||||
|
||||
[![][codespaces-shield]][codespaces-link]
|
||||
|
||||
Or to get Qopy set up on your machine, you'll need to have Rust and bun installed. Then, follow these steps:
|
||||
|
||||
```zsh
|
||||
git clone https://github.com/0pandadev/Qopy.git
|
||||
cd Qopy
|
||||
bun i
|
||||
bun dev
|
||||
```
|
||||
|
||||
> \[!TIP]
|
||||
>
|
||||
> If you are interested in contributing code, feel free to check out the [Issues](https://github.com/0pandadev/Qopy/issues) section.
|
||||
|
||||
## 🔨 Building for production
|
||||
|
||||
To build for production simply execute:
|
||||
|
||||
```zsh
|
||||
bun build
|
||||
```
|
||||
|
||||
> \[!NOTE]
|
||||
>
|
||||
> Don't worry, it will fail at the end because it can not detect a Private key, but the installer files will be generated regardless of that.
|
||||
>
|
||||
> You can find them in `src-tauri/target/release/bundle`.
|
||||
|
||||
## 📝 License
|
||||
|
||||
Qopy is licensed under AGPL-3. See the [LICENSE file](./LICENCE) for more information.
|
||||
|
||||
[codespaces-link]: https://codespaces.new/0pandadev/Qopy
|
||||
[codespaces-shield]: https://github.com/codespaces/badge.svg
|
||||
|
|
260
README_ru.md
260
README_ru.md
|
@ -1,130 +1,130 @@
|
|||
<div align="center">
|
||||
|
||||
<img align="center" width="128px" src="src-tauri/icons/icon.png" />
|
||||
<h1 align="center"><b>Qopy</b></h1>
|
||||
|
||||
Простой и исправленный менеджер буфера обмена как для Windows, так и для Linux.
|
||||
|
||||
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1_x64.msi">
|
||||
<img src="./public/windows.png"> Windows (x64)
|
||||
</a>
|
||||
•
|
||||
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1_arm64.msi">
|
||||
Windows (arm64)
|
||||
</a>
|
||||
<br>
|
||||
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1.deb">
|
||||
<img src="./public/linux.png"> Linux (deb)
|
||||
</a>
|
||||
•
|
||||
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1.rpm">
|
||||
Linux (rpm)
|
||||
</a>
|
||||
•
|
||||
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1.AppImage">
|
||||
Linux (AppImage)
|
||||
</a>
|
||||
<br>
|
||||
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1_silicon.dmg">
|
||||
<img src="./public/apple.png"> macOS (Silicon)
|
||||
</a>
|
||||
•
|
||||
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1_intel.dmg">
|
||||
macOS (Intel)
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
<sup>Тестовые версии можно найти <a href="https://github.com/0PandaDEV/qopy/actions/workflows/build.yml">тут</a> </sup>
|
||||
|
||||
</div>
|
||||
|
||||
[discord »](https://discord.gg/invite/Y7SbYphVw9)
|
||||
|
||||
> \[!IMPORTANT]
|
||||
>
|
||||
> **Нажав на звезду**, Вы будете получать все уведомления от Github о новых версиях без задержек \~ ⭐️
|
||||
|
||||
<details>
|
||||
<summary><kbd>Star History</kbd></summary>
|
||||
<a href="https://star-history.com/#0pandadev/qopy&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=0pandadev/qopy&theme=dark&type=Date">
|
||||
<img width="100%" src="https://api.star-history.com/svg?repos=0pandadev/qopy&type=Date">
|
||||
</picture>
|
||||
</a>
|
||||
</details>
|
||||
|
||||
[](https://wakatime.com/badge/user/018ce503-097f-4057-9599-db20b190920c/project/fe76359d-56c2-4a13-8413-55207b6ad298)
|
||||
|
||||
## 📋 Что такое Qopy
|
||||
|
||||
Qopy представляет собой исправленный менеджер буфера обмена, разработанный как простая альтернатива стандартному буферу обмена в Windows. Его цель - обеспечить более быструю и надежную работу, предоставляя при этом обширный набор функций по сравнению со своим аналогом в Windows.
|
||||
|
||||
## 🚧 Дорожная карта
|
||||
- [ ] [Руководство по установке](https://github.com/0PandaDEV/Qopy/blob/main/GET_STARTED.md)
|
||||
- [ ] Синхронизация буфера обмена между устройствами https://github.com/0PandaDEV/Qopy/issues/8
|
||||
- [ ] Настройки https://github.com/0PandaDEV/Qopy/issues/2
|
||||
- [x] Метаданные для скопированных элементов https://github.com/0PandaDEV/Qopy/issues/5
|
||||
- [ ] Выделение кода https://github.com/0PandaDEV/Qopy/issues/7
|
||||
- [ ] Интеграция Streamshare https://github.com/0PandaDEV/Qopy/issues/4
|
||||
- [ ] Фильтр типов контента https://github.com/0PandaDEV/Qopy/issues/16
|
||||
- [ ] Превью для скопированных файлов https://github.com/0PandaDEV/Qopy/issues/15
|
||||
- [ ] Конвертация файлов в другие форматы https://github.com/0PandaDEV/Qopy/issues/17
|
||||
- [x] Опция для пользовательской привязки клавиш https://github.com/0PandaDEV/Qopy/issues/3
|
||||
- [x] Поддержка macOS https://github.com/0PandaDEV/Qopy/issues/13
|
||||
|
||||
<sup>Если у вас есть идеи для функций, которые можно добавить в будущем, пожалуйста, напишите об этом [здесь](https://github.com/0pandadev/Qopy/issues).</sup>
|
||||
|
||||
## 📦 Концепты
|
||||
|
||||
Здесь вы можете увидеть несколько концепцов, которые могут быть не реализованы:
|
||||
|
||||

|
||||

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

|
||||

|
||||
|
||||
|
||||
## ❤️ Пожертвования и Поддержка
|
||||
|
||||
Qopy имеет открытый исходный код и бесплатен для использования. Я ценю пожертвования в поддержку постоянной разработки и улучшений. Ваши взносы являются добровольными и помогают мне улучшить приложение для всех.
|
||||
|
||||
<a href="https://buymeacoffee.com/pandadev_"><img src="https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black"/></a>
|
||||
|
||||
## ⌨️ Локальная разработка
|
||||
|
||||
Вы можете использовать GitHub Codespaces для онлайн-разработки:
|
||||
|
||||
[![][codespaces-shield]][codespaces-link]
|
||||
|
||||
Или, чтобы настроить Qopy на вашем компьютере, вам необходимо установить Rust и bun. Затем выполните следующие действия:
|
||||
|
||||
```zsh
|
||||
git clone https://github.com/0pandadev/Qopy.git
|
||||
cd Qopy
|
||||
bun i
|
||||
bun dev
|
||||
```
|
||||
|
||||
> \[!Tip]
|
||||
>
|
||||
> Если вы заинтересованы во внесении кода, не стесняйтесь смотреть здесь [Issues](https://github.com/0pandadev/Qopy/issues).
|
||||
|
||||
## 🔨 Сборка для продакшена
|
||||
|
||||
Чтобы собрать для продакшена,просто выполните:
|
||||
|
||||
```zsh
|
||||
bun build
|
||||
```
|
||||
|
||||
> \[!NOTE]
|
||||
>
|
||||
> Не волнуйтесь, в конце произойдет сбой, потому что он не сможет обнаружить Приватный ключ, но установочные файлы будут сгенерированы независимо от этого.
|
||||
>
|
||||
> Вы можете найти его в `src-tauri/target/release/bundle`.
|
||||
|
||||
## 📝 Лицензия
|
||||
|
||||
Qopy лицензирован под GPL-3. Смотрите [LICENSE file](./LICENCE) для дополнительной информации.
|
||||
|
||||
[codespaces-link]: https://codespaces.new/0pandadev/Qopy
|
||||
[codespaces-shield]: https://github.com/codespaces/badge.svg
|
||||
|
|
218
app.vue
218
app.vue
|
@ -1,109 +1,109 @@
|
|||
<template>
|
||||
<div>
|
||||
<Noise />
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { app, window } from "@tauri-apps/api";
|
||||
import { disable, enable } from "@tauri-apps/plugin-autostart";
|
||||
import { onMounted } from "vue";
|
||||
import { keyboard } from "wrdu-keyboard";
|
||||
|
||||
const { $settings } = useNuxtApp();
|
||||
keyboard.init();
|
||||
|
||||
onMounted(async () => {
|
||||
await listen("settings", async () => {
|
||||
await navigateTo("/settings");
|
||||
await app.show();
|
||||
await window.getCurrentWindow().show();
|
||||
});
|
||||
|
||||
if ((await $settings.getSetting("autostart")) === "true") {
|
||||
await enable();
|
||||
} else {
|
||||
await disable();
|
||||
}
|
||||
|
||||
await listen("main_route", async () => {
|
||||
await navigateTo("/");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@font-face {
|
||||
font-family: SFRoundedRegular;
|
||||
font-display: swap;
|
||||
src: url("/fonts/SFRoundedRegular.otf") format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: SFRoundedMedium;
|
||||
font-display: swap;
|
||||
src: url("/fonts/SFRoundedMedium.otf") format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: SFRoundedSemiBold;
|
||||
font-display: swap;
|
||||
src: url("/fonts/SFRoundedSemiBold.otf") format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: CommitMono;
|
||||
font-display: swap;
|
||||
src: url("/fonts/CommitMono.woff2") format("woff2");
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: #2e2d2b;
|
||||
--accent: #feb453;
|
||||
--border: #ffffff0d;
|
||||
|
||||
--red: #F84E4E;
|
||||
|
||||
--text: #e5dfd5;
|
||||
--text-secondary: #ada9a1;
|
||||
--text-muted: #78756f;
|
||||
|
||||
--sidebar-width: 286px;
|
||||
--bottom-bar-height: 39px;
|
||||
--info-panel-height: 160px;
|
||||
--content-view-height: calc(
|
||||
100% - var(--search-height) - var(--info-panel-height) -
|
||||
var(--bottom-bar-height)
|
||||
);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
text-decoration: none;
|
||||
font-family: SFRoundedRegular;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: thin;
|
||||
user-select: none;
|
||||
|
||||
--os-handle-bg: #ada9a1;
|
||||
--os-handle-bg-hover: #78756f;
|
||||
--os-handle-bg-active: #78756f;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: transparent;
|
||||
width: 750px;
|
||||
height: 474px;
|
||||
z-index: -1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.os-scrollbar-horizontal {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div>
|
||||
<Noise />
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { app, window } from "@tauri-apps/api";
|
||||
import { disable, enable } from "@tauri-apps/plugin-autostart";
|
||||
import { onMounted } from "vue";
|
||||
import { keyboard } from "wrdu-keyboard";
|
||||
|
||||
const { $settings } = useNuxtApp();
|
||||
keyboard.init();
|
||||
|
||||
onMounted(async () => {
|
||||
await listen("settings", async () => {
|
||||
await navigateTo("/settings");
|
||||
await app.show();
|
||||
await window.getCurrentWindow().show();
|
||||
});
|
||||
|
||||
if ((await $settings.getSetting("autostart")) === "true") {
|
||||
await enable();
|
||||
} else {
|
||||
await disable();
|
||||
}
|
||||
|
||||
await listen("main_route", async () => {
|
||||
await navigateTo("/");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@font-face {
|
||||
font-family: SFRoundedRegular;
|
||||
font-display: swap;
|
||||
src: url("/fonts/SFRoundedRegular.otf") format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: SFRoundedMedium;
|
||||
font-display: swap;
|
||||
src: url("/fonts/SFRoundedMedium.otf") format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: SFRoundedSemiBold;
|
||||
font-display: swap;
|
||||
src: url("/fonts/SFRoundedSemiBold.otf") format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: CommitMono;
|
||||
font-display: swap;
|
||||
src: url("/fonts/CommitMono.woff2") format("woff2");
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: #2e2d2b;
|
||||
--accent: #feb453;
|
||||
--border: #ffffff0d;
|
||||
|
||||
--red: #F84E4E;
|
||||
|
||||
--text: #e5dfd5;
|
||||
--text-secondary: #ada9a1;
|
||||
--text-muted: #78756f;
|
||||
|
||||
--sidebar-width: 286px;
|
||||
--bottom-bar-height: 39px;
|
||||
--info-panel-height: 160px;
|
||||
--content-view-height: calc(
|
||||
100% - var(--search-height) - var(--info-panel-height) -
|
||||
var(--bottom-bar-height)
|
||||
);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
text-decoration: none;
|
||||
font-family: SFRoundedRegular;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: thin;
|
||||
user-select: none;
|
||||
|
||||
--os-handle-bg: #ada9a1;
|
||||
--os-handle-bg-hover: #78756f;
|
||||
--os-handle-bg-active: #78756f;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: transparent;
|
||||
width: 750px;
|
||||
height: 474px;
|
||||
z-index: -1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.os-scrollbar-horizontal {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,152 +1,152 @@
|
|||
<template>
|
||||
<div class="bottombar">
|
||||
<div class="branding">
|
||||
<img src="/logo.png" alt="logo" class="logo" />
|
||||
<p class="name">Qopy</p>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div v-if="primaryAction" class="paste" @click="handlePrimaryClick">
|
||||
<p class="text">{{ primaryAction.text }}</p>
|
||||
<div class="keys">
|
||||
<Key v-if="(os === 'windows' || os === 'linux') && primaryAction.showModifier" :input="'Ctrl'" />
|
||||
<IconsCmd v-if="os === 'macos' && primaryAction.showModifier" />
|
||||
<component :is="primaryAction.icon" :input="primaryAction.input" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="secondaryAction" class="divider"></div>
|
||||
<div v-if="secondaryAction" class="actions" @click="handleSecondaryClick">
|
||||
<p class="text">{{ secondaryAction.text }}</p>
|
||||
<div class="keys">
|
||||
<Key v-if="(os === 'windows' || os === 'linux') && secondaryAction.showModifier" :input="'Ctrl'" />
|
||||
<IconsCmd v-if="os === 'macos' && secondaryAction.showModifier" />
|
||||
<component :is="secondaryAction.icon" :input="secondaryAction.input" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import IconsCmd from './Keys/Cmd.vue';
|
||||
import Key from './Keys/Key.vue';
|
||||
|
||||
interface Action {
|
||||
text: string;
|
||||
icon: any;
|
||||
onClick?: () => void;
|
||||
showModifier?: boolean;
|
||||
input?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
primaryAction?: Action;
|
||||
secondaryAction?: Action;
|
||||
}>();
|
||||
|
||||
const os = ref<string>("");
|
||||
|
||||
const handlePrimaryClick = (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
if (props.primaryAction?.onClick) {
|
||||
props.primaryAction.onClick();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSecondaryClick = (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
if (props.secondaryAction?.onClick) {
|
||||
props.secondaryAction.onClick();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
os.value = await platform();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bottombar {
|
||||
min-height: 40px;
|
||||
width: 100%;
|
||||
border-top: 1px solid var(--border);
|
||||
backdrop-filter: blur(18px);
|
||||
border-radius: 0 0 11px 11px;
|
||||
background-color: rgba(46, 45, 43, 0.051);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 11px;
|
||||
padding-right: 6px;
|
||||
justify-content: space-between;
|
||||
|
||||
.branding {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: var(--text-secondary);
|
||||
|
||||
.logo {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.text {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.keys {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 2px;
|
||||
height: 12px;
|
||||
background-color: var(--border);
|
||||
margin-left: 8px;
|
||||
margin-right: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.paste,
|
||||
.actions {
|
||||
padding: 4px;
|
||||
padding-left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 7px;
|
||||
background-color: transparent;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.paste:hover,
|
||||
.actions:hover {
|
||||
background-color: var(--border);
|
||||
}
|
||||
|
||||
.paste:active,
|
||||
.actions:active {
|
||||
background-color: var(--border-active, #444);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&:hover .paste:hover ~ .divider,
|
||||
&:hover .divider:has(+ .actions:hover) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: SFRoundedMedium;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="bottombar">
|
||||
<div class="branding">
|
||||
<img src="/logo.png" alt="logo" class="logo" />
|
||||
<p class="name">Qopy</p>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div v-if="primaryAction" class="paste" @click="handlePrimaryClick">
|
||||
<p class="text">{{ primaryAction.text }}</p>
|
||||
<div class="keys">
|
||||
<Key v-if="(os === 'windows' || os === 'linux') && primaryAction.showModifier" :input="'Ctrl'" />
|
||||
<IconsCmd v-if="os === 'macos' && primaryAction.showModifier" />
|
||||
<component :is="primaryAction.icon" :input="primaryAction.input" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="secondaryAction" class="divider"></div>
|
||||
<div v-if="secondaryAction" class="actions" @click="handleSecondaryClick">
|
||||
<p class="text">{{ secondaryAction.text }}</p>
|
||||
<div class="keys">
|
||||
<Key v-if="(os === 'windows' || os === 'linux') && secondaryAction.showModifier" :input="'Ctrl'" />
|
||||
<IconsCmd v-if="os === 'macos' && secondaryAction.showModifier" />
|
||||
<component :is="secondaryAction.icon" :input="secondaryAction.input" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import IconsCmd from './Keys/Cmd.vue';
|
||||
import Key from './Keys/Key.vue';
|
||||
|
||||
interface Action {
|
||||
text: string;
|
||||
icon: any;
|
||||
onClick?: () => void;
|
||||
showModifier?: boolean;
|
||||
input?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
primaryAction?: Action;
|
||||
secondaryAction?: Action;
|
||||
}>();
|
||||
|
||||
const os = ref<string>("");
|
||||
|
||||
const handlePrimaryClick = (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
if (props.primaryAction?.onClick) {
|
||||
props.primaryAction.onClick();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSecondaryClick = (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
if (props.secondaryAction?.onClick) {
|
||||
props.secondaryAction.onClick();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
os.value = await platform();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bottombar {
|
||||
min-height: 40px;
|
||||
width: 100%;
|
||||
border-top: 1px solid var(--border);
|
||||
backdrop-filter: blur(18px);
|
||||
border-radius: 0 0 11px 11px;
|
||||
background-color: rgba(46, 45, 43, 0.051);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 11px;
|
||||
padding-right: 6px;
|
||||
justify-content: space-between;
|
||||
|
||||
.branding {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: var(--text-secondary);
|
||||
|
||||
.logo {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.text {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.keys {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 2px;
|
||||
height: 12px;
|
||||
background-color: var(--border);
|
||||
margin-left: 8px;
|
||||
margin-right: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.paste,
|
||||
.actions {
|
||||
padding: 4px;
|
||||
padding-left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 7px;
|
||||
background-color: transparent;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.paste:hover,
|
||||
.actions:hover {
|
||||
background-color: var(--border);
|
||||
}
|
||||
|
||||
.paste:active,
|
||||
.actions:active {
|
||||
background-color: var(--border-active, #444);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&:hover .paste:hover ~ .divider,
|
||||
&:hover .divider:has(+ .actions:hover) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: SFRoundedMedium;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<path fill="#F84E4E" fill-rule="evenodd"
|
||||
d="M9 2H7a.5.5 0 0 0-.5.5V3h3v-.5A.5.5 0 0 0 9 2m2 1v-.5a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2V3H2.251a.75.75 0 0 0 0 1.5h.312l.317 7.625A3 3 0 0 0 5.878 15h4.245a3 3 0 0 0 2.997-2.875l.318-7.625h.312a.75.75 0 0 0 0-1.5zm.936 1.5H4.064l.315 7.562A1.5 1.5 0 0 0 5.878 13.5h4.245a1.5 1.5 0 0 0 1.498-1.438zm-6.186 2v5a.75.75 0 0 0 1.5 0v-5a.75.75 0 0 0-1.5 0m3.75-.75a.75.75 0 0 1 .75.75v5a.75.75 0 0 1-1.5 0v-5a.75.75 0 0 1 .75-.75"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<path fill="#F84E4E" fill-rule="evenodd"
|
||||
d="M9 2H7a.5.5 0 0 0-.5.5V3h3v-.5A.5.5 0 0 0 9 2m2 1v-.5a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2V3H2.251a.75.75 0 0 0 0 1.5h.312l.317 7.625A3 3 0 0 0 5.878 15h4.245a3 3 0 0 0 2.997-2.875l.318-7.625h.312a.75.75 0 0 0 0-1.5zm.936 1.5H4.064l.315 7.562A1.5 1.5 0 0 0 5.878 13.5h4.245a1.5 1.5 0 0 0 1.498-1.438zm-6.186 2v5a.75.75 0 0 0 1.5 0v-5a.75.75 0 0 0-1.5 0m3.75-.75a.75.75 0 0 1 .75.75v5a.75.75 0 0 1-1.5 0v-5a.75.75 0 0 1 .75-.75"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
|
@ -1,14 +1,14 @@
|
|||
<template>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path
|
||||
d="M0 1.38272C0 0.619063 0.596954 0 1.33333 0C1.33333 0 2.66667 0 2.66667 0C3.40305 0 4 0.619063 4 1.38272C4 1.38272 4 1.38272 4 1.38272C4 2.14637 3.40305 2.76543 2.66667 2.76543C2.66667 2.76543 1.33333 2.76543 1.33333 2.76543C0.596954 2.76543 0 2.14637 0 1.38272"
|
||||
fill="none" stroke-width="1.5" stroke="#E5DFD5" stroke-linecap="round" stroke-linejoin="round"
|
||||
transform="translate(5 0.778)" />
|
||||
<path
|
||||
d="M2.66667 0C2.66667 0 1.33333 0 1.33333 0C0.596954 0 0 0.619063 0 1.38272C0 1.38272 0 9.67901 0 9.67901C0 10.4427 0.596954 11.0617 1.33333 11.0617C1.33333 11.0617 8 11.0617 8 11.0617C8.73638 11.0617 9.33333 10.4427 9.33333 9.67901C9.33333 9.67901 9.33333 1.38272 9.33333 1.38272C9.33333 0.619063 8.73638 0 8 0C8 0 6.66667 0 6.66667 0"
|
||||
fill="none" stroke-width="1.5" stroke="#E5DFD5" stroke-linecap="round" stroke-linejoin="round"
|
||||
transform="translate(2.333 2.161)" />
|
||||
</g>
|
||||
</svg>
|
||||
<template>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path
|
||||
d="M0 1.38272C0 0.619063 0.596954 0 1.33333 0C1.33333 0 2.66667 0 2.66667 0C3.40305 0 4 0.619063 4 1.38272C4 1.38272 4 1.38272 4 1.38272C4 2.14637 3.40305 2.76543 2.66667 2.76543C2.66667 2.76543 1.33333 2.76543 1.33333 2.76543C0.596954 2.76543 0 2.14637 0 1.38272"
|
||||
fill="none" stroke-width="1.5" stroke="#E5DFD5" stroke-linecap="round" stroke-linejoin="round"
|
||||
transform="translate(5 0.778)" />
|
||||
<path
|
||||
d="M2.66667 0C2.66667 0 1.33333 0 1.33333 0C0.596954 0 0 0.619063 0 1.38272C0 1.38272 0 9.67901 0 9.67901C0 10.4427 0.596954 11.0617 1.33333 11.0617C1.33333 11.0617 8 11.0617 8 11.0617C8.73638 11.0617 9.33333 10.4427 9.33333 9.67901C9.33333 9.67901 9.33333 1.38272 9.33333 1.38272C9.33333 0.619063 8.73638 0 8 0C8 0 6.66667 0 6.66667 0"
|
||||
fill="none" stroke-width="1.5" stroke="#E5DFD5" stroke-linecap="round" stroke-linejoin="round"
|
||||
transform="translate(2.333 2.161)" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||
d="M8.922 9.842q.077.425.078.896c0 1.907-1.387 3.66-3.79 3.894a4.78 4.78 0 0 1-4.208-1.774a2 2 0 0 1-.21-.333c-.231-.461-.292-1-.292-1.528c.312.047.599.045.852 0c.635-.112 1.061-.487 1.148-1C2.73 8.637 3.572 7 5.76 7q.224 0 .435.028l3.417-4.784a2.971 2.971 0 1 1 4.145 4.145zm-.56-1.444l2.819-2.013A2.7 2.7 0 0 0 9.615 4.82L7.626 7.605q.43.324.737.793m4.066-2.904l.457-.326a1.471 1.471 0 1 0-2.052-2.052l-.326.457a4.2 4.2 0 0 1 1.921 1.921M3.98 10.247c.086-.507.272-.962.54-1.264c.225-.254.572-.483 1.242-.483c.517 0 .913.197 1.198.523c.297.34.541.906.541 1.715c0 1.121-.786 2.24-2.435 2.4a3.3 3.3 0 0 1-2.63-.922c.76-.337 1.374-.965 1.544-1.969"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||
d="M8.922 9.842q.077.425.078.896c0 1.907-1.387 3.66-3.79 3.894a4.78 4.78 0 0 1-4.208-1.774a2 2 0 0 1-.21-.333c-.231-.461-.292-1-.292-1.528c.312.047.599.045.852 0c.635-.112 1.061-.487 1.148-1C2.73 8.637 3.572 7 5.76 7q.224 0 .435.028l3.417-4.784a2.971 2.971 0 1 1 4.145 4.145zm-.56-1.444l2.819-2.013A2.7 2.7 0 0 0 9.615 4.82L7.626 7.605q.43.324.737.793m4.066-2.904l.457-.326a1.471 1.471 0 1 0-2.052-2.052l-.326.457a4.2 4.2 0 0 1 1.921 1.921M3.98 10.247c.086-.507.272-.962.54-1.264c.225-.254.572-.483 1.242-.483c.517 0 .913.197 1.198.523c.297.34.541.906.541 1.715c0 1.121-.786 2.24-2.435 2.4a3.3 3.3 0 0 1-2.63-.922c.76-.337 1.374-.965 1.544-1.969"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
|
@ -1,16 +1,16 @@
|
|||
<template>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path
|
||||
d="M3.75 16.0714L11.25 16.0714C12.2855 16.0714 13.125 15.208 13.125 14.1429L13.125 8.02672C13.1248 7.5147 12.9269 7.02399 12.5748 6.66239L8.52375 2.493C8.17225 2.13169 7.69568 1.92868 7.19875 1.92857L3.75 1.92857C2.71447 1.92857 1.875 2.79202 1.875 3.85715L1.875 14.1429C1.875 15.208 2.71447 16.0714 3.75 16.0714M15 8.02672C15.0003 7.00424 14.6053 6.02271 13.9018 5.29904L9.85 1.13143C9.1465 0.406921 8.19178 -0.000123228 7.19625 0L3.75 0C1.67893 0 0 1.7269 0 3.85714L0 14.1429C2.38419e-07 16.2731 1.67893 18 3.75 18L11.25 18C13.3211 18 15 16.2731 15 14.1429L15 8.02672ZM8.40003 12.2529C8.03446 11.8764 8.03446 11.2665 8.40003 10.89L9.61253 9.64286L8.40003 8.39571C8.05583 8.01577 8.06598 7.4237 8.423 7.05648C8.78002 6.68926 9.35564 6.67882 9.72503 7.03286L11.6 8.96143C11.9656 9.33791 11.9656 9.94781 11.6 10.3243L9.72503 12.2529C9.35901 12.6289 8.76605 12.6289 8.40003 12.2529M6.60003 8.39571C6.94423 8.01577 6.93407 7.4237 6.57706 7.05649C6.22004 6.68927 5.64442 6.67882 5.27503 7.03286L3.40003 8.96143C3.03446 9.33791 3.03446 9.94781 3.40003 10.3243L5.27503 12.2529C5.50874 12.5108 5.86072 12.617 6.19289 12.5298C6.52505 12.4425 6.78443 12.1757 6.86926 11.8341C6.95409 11.4924 6.85084 11.1304 6.60003 10.89L5.38753 9.64286L6.60003 8.39571Z"
|
||||
fill="#E5DFD5"
|
||||
fill-rule="evenodd"
|
||||
transform="translate(1.5 0)" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
<template>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path
|
||||
d="M3.75 16.0714L11.25 16.0714C12.2855 16.0714 13.125 15.208 13.125 14.1429L13.125 8.02672C13.1248 7.5147 12.9269 7.02399 12.5748 6.66239L8.52375 2.493C8.17225 2.13169 7.69568 1.92868 7.19875 1.92857L3.75 1.92857C2.71447 1.92857 1.875 2.79202 1.875 3.85715L1.875 14.1429C1.875 15.208 2.71447 16.0714 3.75 16.0714M15 8.02672C15.0003 7.00424 14.6053 6.02271 13.9018 5.29904L9.85 1.13143C9.1465 0.406921 8.19178 -0.000123228 7.19625 0L3.75 0C1.67893 0 0 1.7269 0 3.85714L0 14.1429C2.38419e-07 16.2731 1.67893 18 3.75 18L11.25 18C13.3211 18 15 16.2731 15 14.1429L15 8.02672ZM8.40003 12.2529C8.03446 11.8764 8.03446 11.2665 8.40003 10.89L9.61253 9.64286L8.40003 8.39571C8.05583 8.01577 8.06598 7.4237 8.423 7.05648C8.78002 6.68926 9.35564 6.67882 9.72503 7.03286L11.6 8.96143C11.9656 9.33791 11.9656 9.94781 11.6 10.3243L9.72503 12.2529C9.35901 12.6289 8.76605 12.6289 8.40003 12.2529M6.60003 8.39571C6.94423 8.01577 6.93407 7.4237 6.57706 7.05649C6.22004 6.68927 5.64442 6.67882 5.27503 7.03286L3.40003 8.96143C3.03446 9.33791 3.03446 9.94781 3.40003 10.3243L5.27503 12.2529C5.50874 12.5108 5.86072 12.617 6.19289 12.5298C6.52505 12.4425 6.78443 12.1757 6.86926 11.8341C6.95409 11.4924 6.85084 11.1304 6.60003 10.89L5.38753 9.64286L6.60003 8.39571Z"
|
||||
fill="#E5DFD5"
|
||||
fill-rule="evenodd"
|
||||
transform="translate(1.5 0)" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<g fill="none">
|
||||
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||
d="M7.47 1.22a.75.75 0 0 1 1.06 0l1.75 1.75a.75.75 0 1 1-1.06 1.06l-.47-.47v8.88l.47-.47a.75.75 0 1 1 1.06 1.06l-1.75 1.75a.75.75 0 0 1-1.06 0l-1.75-1.75a.75.75 0 1 1 1.06-1.06l.47.47V3.56l-.47.47a.75.75 0 0 1-1.06-1.06zM1.22 7.47a.75.75 0 0 0 0 1.06l1.75 1.75a.75.75 0 1 0 1.06-1.06L2.81 8l1.22-1.22a.75.75 0 0 0-1.06-1.06zm13.56 1.06l-1.75 1.75a.75.75 0 1 1-1.06-1.06L13.19 8l-1.22-1.22a.75.75 0 0 1 1.06-1.06l1.75 1.75a.75.75 0 0 1 0 1.06"
|
||||
clip-rule="evenodd" />
|
||||
<path stroke="#E5DFD5" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.5 8h11" />
|
||||
</g>
|
||||
</svg>
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<g fill="none">
|
||||
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||
d="M7.47 1.22a.75.75 0 0 1 1.06 0l1.75 1.75a.75.75 0 1 1-1.06 1.06l-.47-.47v8.88l.47-.47a.75.75 0 1 1 1.06 1.06l-1.75 1.75a.75.75 0 0 1-1.06 0l-1.75-1.75a.75.75 0 1 1 1.06-1.06l.47.47V3.56l-.47.47a.75.75 0 0 1-1.06-1.06zM1.22 7.47a.75.75 0 0 0 0 1.06l1.75 1.75a.75.75 0 1 0 1.06-1.06L2.81 8l1.22-1.22a.75.75 0 0 0-1.06-1.06zm13.56 1.06l-1.75 1.75a.75.75 0 1 1-1.06-1.06L13.19 8l-1.22-1.22a.75.75 0 0 1 1.06-1.06l1.75 1.75a.75.75 0 0 1 0 1.06"
|
||||
clip-rule="evenodd" />
|
||||
<path stroke="#E5DFD5" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.5 8h11" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
|
@ -1,16 +1,16 @@
|
|||
<template>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path
|
||||
d="M11.25 16.0714L3.75 16.0714C2.71447 16.0714 1.875 15.208 1.875 14.1429L1.875 3.85714C1.875 2.79202 2.71447 1.92857 3.75 1.92857L6.25 1.92857L6.25 5.14286C6.25 7.2731 7.92893 9 10 9L13.125 9L13.125 14.1429C13.125 15.208 12.2855 16.0714 11.25 16.0714M12.8788 7.07143C12.7961 6.92188 12.6944 6.78437 12.5763 6.66257L8.5225 2.493C8.40408 2.37151 8.2704 2.26687 8.125 2.18186L8.125 5.14286C8.125 6.20798 8.96447 7.07143 10 7.07143L12.8788 7.07143ZM13.9013 5.29843C14.6049 6.02193 15.0001 7.00338 15 8.02672L15 14.1429C15 16.2731 13.3211 18 11.25 18L3.75 18C1.67893 18 0 16.2731 0 14.1429L0 3.85714C-5.96046e-07 1.7269 1.67893 0 3.75 0L7.19625 0C8.19116 -0.000122309 9.14535 0.406423 9.84875 1.13014L13.9013 5.29843Z"
|
||||
fill="#E5DFD5"
|
||||
fill-rule="evenodd"
|
||||
transform="translate(1.5 0)" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
<template>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path
|
||||
d="M11.25 16.0714L3.75 16.0714C2.71447 16.0714 1.875 15.208 1.875 14.1429L1.875 3.85714C1.875 2.79202 2.71447 1.92857 3.75 1.92857L6.25 1.92857L6.25 5.14286C6.25 7.2731 7.92893 9 10 9L13.125 9L13.125 14.1429C13.125 15.208 12.2855 16.0714 11.25 16.0714M12.8788 7.07143C12.7961 6.92188 12.6944 6.78437 12.5763 6.66257L8.5225 2.493C8.40408 2.37151 8.2704 2.26687 8.125 2.18186L8.125 5.14286C8.125 6.20798 8.96447 7.07143 10 7.07143L12.8788 7.07143ZM13.9013 5.29843C14.6049 6.02193 15.0001 7.00338 15 8.02672L15 14.1429C15 16.2731 13.3211 18 11.25 18L3.75 18C1.67893 18 0 16.2731 0 14.1429L0 3.85714C-5.96046e-07 1.7269 1.67893 0 3.75 0L7.19625 0C8.19116 -0.000122309 9.14535 0.406423 9.84875 1.13014L13.9013 5.29843Z"
|
||||
fill="#E5DFD5"
|
||||
fill-rule="evenodd"
|
||||
transform="translate(1.5 0)" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||
d="M7.199 2H8.8a.2.2 0 0 1 .2.2c0 1.808 1.958 2.939 3.524 2.034a.2.2 0 0 1 .271.073l.802 1.388a.2.2 0 0 1-.073.272c-1.566.904-1.566 3.164 0 4.069a.2.2 0 0 1 .073.271l-.802 1.388a.2.2 0 0 1-.271.073C10.958 10.863 9 11.993 9 13.8a.2.2 0 0 1-.199.2H7.2a.2.2 0 0 1-.2-.2c0-1.808-1.958-2.938-3.524-2.034a.2.2 0 0 1-.272-.073l-.8-1.388a.2.2 0 0 1 .072-.271c1.566-.905 1.566-3.165 0-4.07a.2.2 0 0 1-.073-.27l.801-1.389a.2.2 0 0 1 .272-.072C5.042 5.138 7 4.007 7 2.199c0-.11.089-.199.199-.199M5.5 2.2c0-.94.76-1.7 1.699-1.7H8.8c.94 0 1.7.76 1.7 1.7a.85.85 0 0 0 1.274.735a1.7 1.7 0 0 1 2.32.622l.802 1.388c.469.813.19 1.851-.622 2.32a.85.85 0 0 0 0 1.472a1.7 1.7 0 0 1 .622 2.32l-.802 1.388a1.7 1.7 0 0 1-2.32.622a.85.85 0 0 0-1.274.735c0 .939-.76 1.7-1.699 1.7H7.2a1.7 1.7 0 0 1-1.699-1.7a.85.85 0 0 0-1.274-.735a1.7 1.7 0 0 1-2.32-.622l-.802-1.388a1.7 1.7 0 0 1 .622-2.32a.85.85 0 0 0 0-1.471a1.7 1.7 0 0 1-.622-2.32l.801-1.389a1.7 1.7 0 0 1 2.32-.622A.85.85 0 0 0 5.5 2.2m4 5.8a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0M11 8a3 3 0 1 1-6 0a3 3 0 0 1 6 0"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||
d="M7.199 2H8.8a.2.2 0 0 1 .2.2c0 1.808 1.958 2.939 3.524 2.034a.2.2 0 0 1 .271.073l.802 1.388a.2.2 0 0 1-.073.272c-1.566.904-1.566 3.164 0 4.069a.2.2 0 0 1 .073.271l-.802 1.388a.2.2 0 0 1-.271.073C10.958 10.863 9 11.993 9 13.8a.2.2 0 0 1-.199.2H7.2a.2.2 0 0 1-.2-.2c0-1.808-1.958-2.938-3.524-2.034a.2.2 0 0 1-.272-.073l-.8-1.388a.2.2 0 0 1 .072-.271c1.566-.905 1.566-3.165 0-4.07a.2.2 0 0 1-.073-.27l.801-1.389a.2.2 0 0 1 .272-.072C5.042 5.138 7 4.007 7 2.199c0-.11.089-.199.199-.199M5.5 2.2c0-.94.76-1.7 1.699-1.7H8.8c.94 0 1.7.76 1.7 1.7a.85.85 0 0 0 1.274.735a1.7 1.7 0 0 1 2.32.622l.802 1.388c.469.813.19 1.851-.622 2.32a.85.85 0 0 0 0 1.472a1.7 1.7 0 0 1 .622 2.32l-.802 1.388a1.7 1.7 0 0 1-2.32.622a.85.85 0 0 0-1.274.735c0 .939-.76 1.7-1.699 1.7H7.2a1.7 1.7 0 0 1-1.699-1.7a.85.85 0 0 0-1.274-.735a1.7 1.7 0 0 1-2.32-.622l-.802-1.388a1.7 1.7 0 0 1 .622-2.32a.85.85 0 0 0 0-1.471a1.7 1.7 0 0 1-.622-2.32l.801-1.389a1.7 1.7 0 0 1 2.32-.622A.85.85 0 0 0 5.5 2.2m4 5.8a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0M11 8a3 3 0 1 1-6 0a3 3 0 0 1 6 0"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||
d="M9.208 12.346c-.485 1-.953 1.154-1.208 1.154s-.723-.154-1.208-1.154c-.372-.768-.647-1.858-.749-3.187a21 21 0 0 0 3.914 0c-.102 1.329-.377 2.419-.75 3.187m.788-4.699C9.358 7.714 8.69 7.75 8 7.75s-1.358-.036-1.996-.103c.037-1.696.343-3.075.788-3.993C7.277 2.654 7.745 2.5 8 2.5s.723.154 1.208 1.154c.445.918.75 2.297.788 3.993m1.478 1.306c-.085 1.516-.375 2.848-.836 3.874a5.5 5.5 0 0 0 2.843-4.364c-.621.199-1.295.364-2.007.49m1.918-2.043c-.572.204-1.21.379-1.901.514c-.056-1.671-.354-3.14-.853-4.251a5.5 5.5 0 0 1 2.754 3.737m-8.883.514c.056-1.671.354-3.14.853-4.251A5.5 5.5 0 0 0 2.608 6.91c.572.204 1.21.379 1.901.514M2.52 8.463a5.5 5.5 0 0 0 2.843 4.364c-.46-1.026-.75-2.358-.836-3.874a15.5 15.5 0 0 1-2.007-.49M15 8A7 7 0 1 0 1 8a7 7 0 0 0 14 0"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||
d="M9.208 12.346c-.485 1-.953 1.154-1.208 1.154s-.723-.154-1.208-1.154c-.372-.768-.647-1.858-.749-3.187a21 21 0 0 0 3.914 0c-.102 1.329-.377 2.419-.75 3.187m.788-4.699C9.358 7.714 8.69 7.75 8 7.75s-1.358-.036-1.996-.103c.037-1.696.343-3.075.788-3.993C7.277 2.654 7.745 2.5 8 2.5s.723.154 1.208 1.154c.445.918.75 2.297.788 3.993m1.478 1.306c-.085 1.516-.375 2.848-.836 3.874a5.5 5.5 0 0 0 2.843-4.364c-.621.199-1.295.364-2.007.49m1.918-2.043c-.572.204-1.21.379-1.901.514c-.056-1.671-.354-3.14-.853-4.251a5.5 5.5 0 0 1 2.754 3.737m-8.883.514c.056-1.671.354-3.14.853-4.251A5.5 5.5 0 0 0 2.608 6.91c.572.204 1.21.379 1.901.514M2.52 8.463a5.5 5.5 0 0 0 2.843 4.364c-.46-1.026-.75-2.358-.836-3.874a15.5 15.5 0 0 1-2.007-.49M15 8A7 7 0 1 0 1 8a7 7 0 0 0 14 0"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
|
@ -1,15 +1,15 @@
|
|||
<template>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path
|
||||
d="M13.8462 2.07692L4.15385 2.07692C3.00679 2.07692 2.07692 3.00679 2.07692 4.15385L2.07692 11.1143L3.40892 10.1451C4.26934 9.51991 5.43685 9.5289 6.28754 10.1672L7.57246 11.1309L10.8512 8.32016C11.7843 7.52111 13.1676 7.54669 14.0705 8.37969L15.9231 10.0897L15.9231 4.15385C15.9231 3.00679 14.9932 2.07692 13.8462 2.07692M18 12.4588L18 4.15385C18 1.85974 16.1403 0 13.8462 0L4.15385 0C1.85974 0 0 1.85974 0 4.15385L0 13.8462C3.30118e-07 16.1403 1.85974 18 4.15385 18L13.8462 18C16.1403 18 18 16.1403 18 13.8462L18 12.4588ZM15.9231 12.9157L12.6623 9.90554C12.5333 9.78671 12.3358 9.78314 12.2026 9.89723L8.29108 13.2508L7.65831 13.7935L6.99231 13.2937L5.04 11.8302C4.91867 11.7398 4.75269 11.7386 4.63015 11.8274L2.07692 13.6814L2.07692 13.8462C2.07692 14.9932 3.00679 15.9231 4.15385 15.9231L13.8462 15.9231C14.9932 15.9231 15.9231 14.9932 15.9231 13.8462L15.9231 12.9157ZM8.30769 6.23077C8.30769 7.37782 7.37782 8.30769 6.23077 8.30769C5.08372 8.30769 4.15385 7.37782 4.15385 6.23077C4.15385 5.08372 5.08372 4.15385 6.23077 4.15385C7.37782 4.15385 8.30769 5.08372 8.30769 6.23077"
|
||||
fill="#E5DFD5"
|
||||
fill-rule="evenodd" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
<template>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path
|
||||
d="M13.8462 2.07692L4.15385 2.07692C3.00679 2.07692 2.07692 3.00679 2.07692 4.15385L2.07692 11.1143L3.40892 10.1451C4.26934 9.51991 5.43685 9.5289 6.28754 10.1672L7.57246 11.1309L10.8512 8.32016C11.7843 7.52111 13.1676 7.54669 14.0705 8.37969L15.9231 10.0897L15.9231 4.15385C15.9231 3.00679 14.9932 2.07692 13.8462 2.07692M18 12.4588L18 4.15385C18 1.85974 16.1403 0 13.8462 0L4.15385 0C1.85974 0 0 1.85974 0 4.15385L0 13.8462C3.30118e-07 16.1403 1.85974 18 4.15385 18L13.8462 18C16.1403 18 18 16.1403 18 13.8462L18 12.4588ZM15.9231 12.9157L12.6623 9.90554C12.5333 9.78671 12.3358 9.78314 12.2026 9.89723L8.29108 13.2508L7.65831 13.7935L6.99231 13.2937L5.04 11.8302C4.91867 11.7398 4.75269 11.7386 4.63015 11.8274L2.07692 13.6814L2.07692 13.8462C2.07692 14.9932 3.00679 15.9231 4.15385 15.9231L13.8462 15.9231C14.9932 15.9231 15.9231 14.9932 15.9231 13.8462L15.9231 12.9157ZM8.30769 6.23077C8.30769 7.37782 7.37782 8.30769 6.23077 8.30769C5.08372 8.30769 4.15385 7.37782 4.15385 6.23077C4.15385 5.08372 5.08372 4.15385 6.23077 4.15385C7.37782 4.15385 8.30769 5.08372 8.30769 6.23077"
|
||||
fill="#E5DFD5"
|
||||
fill-rule="evenodd" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
<template>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path
|
||||
d="M2.68978 6.95235C3.0999 6.55662 3.75151 6.56259 4.1543 6.96577C4.5571 7.36895 4.56246 8.02056 4.16634 8.43031C4.16634 8.43031 3.15364 9.443 3.15364 9.443C1.68651 10.9393 1.69832 13.3381 3.18012 14.8199C4.66192 16.3017 7.06072 16.3135 8.55703 14.8464C8.55703 14.8464 9.56973 13.8337 9.56973 13.8337C9.98137 13.4501 10.6228 13.4614 11.0207 13.8593C11.4185 14.2571 11.4299 14.8986 11.0463 15.3102C11.0463 15.3102 10.035 16.3229 10.035 16.3229C7.71847 18.5799 4.01808 18.5559 1.73113 16.2689C-0.555826 13.982 -0.579909 10.2816 1.67708 7.96504C1.67708 7.96504 2.68978 6.95235 2.68978 6.95235ZM13.8337 9.56973C13.4501 9.98138 13.4614 10.6228 13.8593 11.0207C14.2571 11.4185 14.8986 11.4299 15.3103 11.0463C15.3103 11.0463 16.323 10.035 16.323 10.035C18.58 7.71847 18.5559 4.01808 16.2689 1.73113C13.982 -0.555826 10.2816 -0.579908 7.96505 1.67708C7.96505 1.67708 6.95235 2.68978 6.95235 2.68978C6.55662 3.0999 6.56259 3.75151 6.96577 4.15431C7.36895 4.55711 8.02056 4.56247 8.43031 4.16635C8.43031 4.16635 9.44301 3.15365 9.44301 3.15365C10.9393 1.68652 13.3381 1.69833 14.8199 3.18013C16.3017 4.66192 16.3135 7.06073 14.8464 8.55704C14.8464 8.55704 13.8337 9.56973 13.8337 9.56973ZM12.5242 6.9523C12.8038 6.69186 12.9188 6.29961 12.8243 5.92945C12.7297 5.55928 12.4407 5.27024 12.0705 5.1757C11.7004 5.08117 11.3081 5.19623 11.0477 5.47574C11.0477 5.47574 5.47574 11.0477 5.47574 11.0477C5.19623 11.3081 5.08117 11.7004 5.1757 12.0705C5.27024 12.4407 5.55928 12.7297 5.92945 12.8243C6.29961 12.9188 6.69186 12.8037 6.9523 12.5242C6.9523 12.5242 12.5242 6.9523 12.5242 6.9523Z"
|
||||
fill="#E5DFD5"
|
||||
fill-rule="evenodd" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
<template>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path
|
||||
d="M2.68978 6.95235C3.0999 6.55662 3.75151 6.56259 4.1543 6.96577C4.5571 7.36895 4.56246 8.02056 4.16634 8.43031C4.16634 8.43031 3.15364 9.443 3.15364 9.443C1.68651 10.9393 1.69832 13.3381 3.18012 14.8199C4.66192 16.3017 7.06072 16.3135 8.55703 14.8464C8.55703 14.8464 9.56973 13.8337 9.56973 13.8337C9.98137 13.4501 10.6228 13.4614 11.0207 13.8593C11.4185 14.2571 11.4299 14.8986 11.0463 15.3102C11.0463 15.3102 10.035 16.3229 10.035 16.3229C7.71847 18.5799 4.01808 18.5559 1.73113 16.2689C-0.555826 13.982 -0.579909 10.2816 1.67708 7.96504C1.67708 7.96504 2.68978 6.95235 2.68978 6.95235ZM13.8337 9.56973C13.4501 9.98138 13.4614 10.6228 13.8593 11.0207C14.2571 11.4185 14.8986 11.4299 15.3103 11.0463C15.3103 11.0463 16.323 10.035 16.323 10.035C18.58 7.71847 18.5559 4.01808 16.2689 1.73113C13.982 -0.555826 10.2816 -0.579908 7.96505 1.67708C7.96505 1.67708 6.95235 2.68978 6.95235 2.68978C6.55662 3.0999 6.56259 3.75151 6.96577 4.15431C7.36895 4.55711 8.02056 4.56247 8.43031 4.16635C8.43031 4.16635 9.44301 3.15365 9.44301 3.15365C10.9393 1.68652 13.3381 1.69833 14.8199 3.18013C16.3017 4.66192 16.3135 7.06073 14.8464 8.55704C14.8464 8.55704 13.8337 9.56973 13.8337 9.56973ZM12.5242 6.9523C12.8038 6.69186 12.9188 6.29961 12.8243 5.92945C12.7297 5.55928 12.4407 5.27024 12.0705 5.1757C11.7004 5.08117 11.3081 5.19623 11.0477 5.47574C11.0477 5.47574 5.47574 11.0477 5.47574 11.0477C5.19623 11.3081 5.08117 11.7004 5.1757 12.0705C5.27024 12.4407 5.55928 12.7297 5.92945 12.8243C6.29961 12.9188 6.69186 12.8037 6.9523 12.5242C6.9523 12.5242 12.5242 6.9523 12.5242 6.9523Z"
|
||||
fill="#E5DFD5"
|
||||
fill-rule="evenodd" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||
d="M10 1.5A.75.75 0 0 0 10 3h1.94L6.97 7.97a.75.75 0 0 0 1.06 1.06L13 4.06V6a.75.75 0 0 0 1.5 0V2.25a.75.75 0 0 0-.75-.75zM7.5 3.25a.75.75 0 0 0-.75-.75H4.5a3 3 0 0 0-3 3v6a3 3 0 0 0 3 3h6a3 3 0 0 0 3-3V9.25a.75.75 0 0 0-1.5 0v2.25a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 3 11.5v-6A1.5 1.5 0 0 1 4.5 4h2.25a.75.75 0 0 0 .75-.75"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||
d="M10 1.5A.75.75 0 0 0 10 3h1.94L6.97 7.97a.75.75 0 0 0 1.06 1.06L13 4.06V6a.75.75 0 0 0 1.5 0V2.25a.75.75 0 0 0-.75-.75zM7.5 3.25a.75.75 0 0 0-.75-.75H4.5a3 3 0 0 0-3 3v6a3 3 0 0 0 3 3h6a3 3 0 0 0 3-3V9.25a.75.75 0 0 0-1.5 0v2.25a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 3 11.5v-6A1.5 1.5 0 0 1 4.5 4h2.25a.75.75 0 0 0 .75-.75"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||
d="M11.423 1A3.577 3.577 0 0 1 15 4.577c0 .27-.108.53-.3.722l-.528.529l-1.971 1.971l-5.059 5.059a3 3 0 0 1-1.533.82l-2.638.528a1 1 0 0 1-1.177-1.177l.528-2.638a3 3 0 0 1 .82-1.533l5.059-5.059l2.5-2.5c.191-.191.451-.299.722-.299m-2.31 4.009l-4.91 4.91a1.5 1.5 0 0 0-.41.766l-.38 1.903l1.902-.38a1.5 1.5 0 0 0 .767-.41l4.91-4.91a2.08 2.08 0 0 0-1.88-1.88m3.098.658a3.6 3.6 0 0 0-1.878-1.879l1.28-1.28c.995.09 1.788.884 1.878 1.88z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||
d="M11.423 1A3.577 3.577 0 0 1 15 4.577c0 .27-.108.53-.3.722l-.528.529l-1.971 1.971l-5.059 5.059a3 3 0 0 1-1.533.82l-2.638.528a1 1 0 0 1-1.177-1.177l.528-2.638a3 3 0 0 1 .82-1.533l5.059-5.059l2.5-2.5c.191-.191.451-.299.722-.299m-2.31 4.009l-4.91 4.91a1.5 1.5 0 0 0-.41.766l-.38 1.903l1.902-.38a1.5 1.5 0 0 0 .767-.41l4.91-4.91a2.08 2.08 0 0 0-1.88-1.88m3.098.658a3.6 3.6 0 0 0-1.878-1.879l1.28-1.28c.995.09 1.788.884 1.878 1.88z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||
d="M8 1.5a6.5 6.5 0 1 0 6.445 7.348a.75.75 0 1 0-1.487-.194A5.001 5.001 0 1 1 11.57 4.5h-1.32a.75.75 0 0 0 0 1.5h3a.75.75 0 0 0 .75-.75v-3a.75.75 0 0 0-1.5 0v1.06A6.48 6.48 0 0 0 8 1.5"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||
d="M8 1.5a6.5 6.5 0 1 0 6.445 7.348a.75.75 0 1 0-1.487-.194A5.001 5.001 0 1 1 11.57 4.5h-1.32a.75.75 0 0 0 0 1.5h3a.75.75 0 0 0 .75-.75v-3a.75.75 0 0 0-1.5 0v1.06A6.48 6.48 0 0 0 8 1.5"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||
d="M3.279 2.544A.75.75 0 0 1 4 2h8a.75.75 0 0 1 .721.544l.5 1.75a.75.75 0 1 1-1.442.412L11.434 3.5H8.75l-.004 9H9.5a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5h.746l.004-9H4.566L4.22 4.706a.75.75 0 1 1-1.442-.412z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||
d="M3.279 2.544A.75.75 0 0 1 4 2h8a.75.75 0 0 1 .721.544l.5 1.75a.75.75 0 1 1-1.442.412L11.434 3.5H8.75l-.004 9H9.5a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5h.746l.004-9H4.566L4.22 4.706a.75.75 0 1 1-1.442-.412z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
|
@ -1,16 +1,16 @@
|
|||
<template>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path
|
||||
d="M3.75 16.0714L11.25 16.0714C12.2855 16.0714 13.125 15.208 13.125 14.1429L13.125 8.02672C13.1248 7.5147 12.9269 7.02399 12.5748 6.66239L8.52375 2.493C8.17225 2.13169 7.69568 1.92868 7.19875 1.92857L3.75 1.92857C2.71447 1.92857 1.875 2.79202 1.875 3.85714L1.875 14.1429C1.875 15.208 2.71447 16.0714 3.75 16.0714M15 8.02672C15.0003 7.00424 14.6053 6.02271 13.9018 5.29904L9.85 1.13143C9.1465 0.406921 8.19178 -0.000123228 7.19625 0L3.75 0C1.67893 0 0 1.7269 0 3.85714L0 14.1429C2.38419e-07 16.2731 1.67893 18 3.75 18L11.25 18C13.3211 18 15 16.2731 15 14.1429L15 8.02672ZM3.75 9.32143C3.75 8.78887 4.16973 8.35714 4.6875 8.35714L10.3125 8.35714C10.8303 8.35714 11.25 8.78887 11.25 9.32143C11.25 9.85399 10.8303 10.2857 10.3125 10.2857L4.6875 10.2857C4.16973 10.2857 3.75 9.85399 3.75 9.32143M4.6875 12.2143C4.35256 12.2143 4.04307 12.3981 3.8756 12.6964C3.70813 12.9948 3.70813 13.3624 3.8756 13.6607C4.04307 13.9591 4.35256 14.1429 4.6875 14.1429L7.8125 14.1429C8.14744 14.1429 8.45693 13.9591 8.6244 13.6607C8.79187 13.3624 8.79187 12.9948 8.6244 12.6964C8.45693 12.3981 8.14744 12.2143 7.8125 12.2143L4.6875 12.2143Z"
|
||||
fill="#E5DFD5"
|
||||
fill-rule="evenodd"
|
||||
transform="translate(1.5 0)" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
<template>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path
|
||||
d="M3.75 16.0714L11.25 16.0714C12.2855 16.0714 13.125 15.208 13.125 14.1429L13.125 8.02672C13.1248 7.5147 12.9269 7.02399 12.5748 6.66239L8.52375 2.493C8.17225 2.13169 7.69568 1.92868 7.19875 1.92857L3.75 1.92857C2.71447 1.92857 1.875 2.79202 1.875 3.85714L1.875 14.1429C1.875 15.208 2.71447 16.0714 3.75 16.0714M15 8.02672C15.0003 7.00424 14.6053 6.02271 13.9018 5.29904L9.85 1.13143C9.1465 0.406921 8.19178 -0.000123228 7.19625 0L3.75 0C1.67893 0 0 1.7269 0 3.85714L0 14.1429C2.38419e-07 16.2731 1.67893 18 3.75 18L11.25 18C13.3211 18 15 16.2731 15 14.1429L15 8.02672ZM3.75 9.32143C3.75 8.78887 4.16973 8.35714 4.6875 8.35714L10.3125 8.35714C10.8303 8.35714 11.25 8.78887 11.25 9.32143C11.25 9.85399 10.8303 10.2857 10.3125 10.2857L4.6875 10.2857C4.16973 10.2857 3.75 9.85399 3.75 9.32143M4.6875 12.2143C4.35256 12.2143 4.04307 12.3981 3.8756 12.6964C3.70813 12.9948 3.70813 13.3624 3.8756 13.6607C4.04307 13.9591 4.35256 14.1429 4.6875 14.1429L7.8125 14.1429C8.14744 14.1429 8.45693 13.9591 8.6244 13.6607C8.79187 13.3624 8.79187 12.9948 8.6244 12.6964C8.45693 12.3981 8.14744 12.2143 7.8125 12.2143L4.6875 12.2143Z"
|
||||
fill="#E5DFD5"
|
||||
fill-rule="evenodd"
|
||||
transform="translate(1.5 0)" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||
d="M11 13.5H5A1.5 1.5 0 0 1 3.5 12V4A1.5 1.5 0 0 1 5 2.5h.5v.75c0 .414.336.75.75.75H7v2h-.75a.75.75 0 0 0-.75.75v.5c0 .414.336.75.75.75H7v2h-.75a.75.75 0 0 0-.75.75v.5c0 .414.336.75.75.75H7v-2h.75a.75.75 0 0 0 .75-.75v-.5A.75.75 0 0 0 7.75 8H7V6h.75a.75.75 0 0 0 .75-.75v-.5A.75.75 0 0 0 7.75 4H7V2.5h.757a1.5 1.5 0 0 1 1.061.44l3.243 3.242a1.5 1.5 0 0 1 .439 1.06V12a1.5 1.5 0 0 1-1.5 1.5m2.121-8.379A3 3 0 0 1 14 7.243V12a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V4a3 3 0 0 1 3-3h2.757a3 3 0 0 1 2.122.879z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||
d="M11 13.5H5A1.5 1.5 0 0 1 3.5 12V4A1.5 1.5 0 0 1 5 2.5h.5v.75c0 .414.336.75.75.75H7v2h-.75a.75.75 0 0 0-.75.75v.5c0 .414.336.75.75.75H7v2h-.75a.75.75 0 0 0-.75.75v.5c0 .414.336.75.75.75H7v-2h.75a.75.75 0 0 0 .75-.75v-.5A.75.75 0 0 0 7.75 8H7V6h.75a.75.75 0 0 0 .75-.75v-.5A.75.75 0 0 0 7.75 4H7V2.5h.757a1.5 1.5 0 0 1 1.061.44l3.243 3.242a1.5 1.5 0 0 1 .439 1.06V12a1.5 1.5 0 0 1-1.5 1.5m2.121-8.379A3 3 0 0 1 14 7.243V12a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V4a3 3 0 0 1 3-3h2.757a3 3 0 0 1 2.122.879z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
|
@ -1,39 +1,39 @@
|
|||
<template>
|
||||
<svg
|
||||
width="24px"
|
||||
height="20px"
|
||||
viewBox="0 0 24 20"
|
||||
version="1.1"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<path d="M0 0L24 0L24 20L0 20L0 0Z" id="path_1" />
|
||||
<clipPath id="clip_1">
|
||||
<use
|
||||
xlink:href="#path_1"
|
||||
clip-rule="evenodd"
|
||||
fill-rule="evenodd"
|
||||
transform="translate(0, -2.133523)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="cmd">
|
||||
<path
|
||||
d="M-751 -2016L-751 -2016L-751 -1996L-775 -1996L-775 -2016L-751 -2016Z"
|
||||
id="cmd"
|
||||
fill="none"
|
||||
stroke="none" />
|
||||
<path
|
||||
d="M12 0L17.6 0C19.8402 0 20.9603 0 21.816 0.435974Q22.3804 0.723593 22.8284 1.17157Q23.2764 1.61955 23.564 2.18404C24 3.03969 24 4.15979 24 6.4L24 13.6C24 15.8402 24 16.9603 23.564 17.816Q23.2764 18.3804 22.8284 18.8284Q22.3804 19.2764 21.816 19.564C20.9603 20 19.8402 20 17.6 20L6.4 20C4.15979 20 3.03969 20 2.18404 19.564Q1.61955 19.2764 1.17157 18.8284Q0.723594 18.3804 0.435974 17.816C0 16.9603 0 15.8402 0 13.6L0 6.4C0 4.15979 0 3.03969 0.435974 2.18404Q0.723594 1.61955 1.17157 1.17157Q1.61955 0.723594 2.18404 0.435974C3.03969 0 4.15979 0 6.4 0L12 0Z"
|
||||
id="Rectangle"
|
||||
fill="#FFFFFF"
|
||||
fill-opacity="0.050980393"
|
||||
stroke="none" />
|
||||
<g id="⌘" clip-path="url(#clip_1)" transform="translate(0 2.133523)">
|
||||
<g transform="translate(5.5692472, 0)" id="⌘" fill="#E5DFD5">
|
||||
<path
|
||||
d="M3.55007 12.8061Q2.98224 12.8061 2.51598 12.5268Q2.04972 12.2475 1.77042 11.7789Q1.49112 11.3104 1.49112 10.7472Q1.49112 10.1747 1.77042 9.70614Q2.04972 9.23757 2.51598 8.95827Q2.98224 8.67898 3.55007 8.67898L4.5657 8.67898L4.5657 7.04474L3.55007 7.04474Q2.98224 7.04474 2.51598 6.76775Q2.04972 6.49077 1.77042 6.02219Q1.49112 5.55362 1.49112 4.98579Q1.49112 4.41797 1.77042 3.9494Q2.04972 3.48082 2.51598 3.20383Q2.98224 2.92685 3.55007 2.92684Q4.1179 2.92684 4.58647 3.20383Q5.05504 3.48082 5.33434 3.9494Q5.61364 4.41797 5.61364 4.98579L5.61364 5.99219L7.25249 5.99219L7.25249 4.98579Q7.25249 4.41797 7.52947 3.9494Q7.80646 3.48082 8.27504 3.20383Q8.74361 2.92685 9.31144 2.92684Q9.87926 2.92684 10.3455 3.20383Q10.8118 3.48082 11.0888 3.9494Q11.3658 4.41797 11.3658 4.98579Q11.3658 5.55362 11.0888 6.02219Q10.8118 6.49077 10.3455 6.76775Q9.87926 7.04474 9.31144 7.04474L8.30043 7.04474L8.30043 8.67898L9.31144 8.67898Q9.87926 8.67898 10.3455 8.95827Q10.8118 9.23757 11.0888 9.70614Q11.3658 10.1747 11.3658 10.7472Q11.3658 11.3104 11.0888 11.7789Q10.8118 12.2475 10.3455 12.5268Q9.87926 12.8061 9.31144 12.8061Q8.74361 12.8061 8.27504 12.5268Q7.80646 12.2475 7.52947 11.7789Q7.25249 11.3104 7.25249 10.7472L7.25249 9.73153L5.61364 9.73153L5.61364 10.7472Q5.61364 11.3104 5.33434 11.7789Q5.05504 12.2475 4.58647 12.5268Q4.1179 12.8061 3.55007 12.8061ZM3.55007 11.7536Q3.97017 11.7536 4.26563 11.4604Q4.56108 11.1673 4.5657 10.7472L4.5657 9.73153L3.55007 9.73153Q3.12997 9.73615 2.83452 10.0293Q2.53906 10.3224 2.53906 10.7472Q2.53906 11.1673 2.83452 11.4604Q3.12997 11.7536 3.55007 11.7536ZM9.31144 11.7536Q9.72692 11.7536 10.0224 11.4604Q10.3178 11.1673 10.3178 10.7472Q10.3178 10.3224 10.0224 10.0293Q9.72692 9.73615 9.31144 9.73153L8.30043 9.73153L8.30043 10.7472Q8.29581 11.1673 8.59357 11.4604Q8.89133 11.7536 9.31144 11.7536ZM3.55007 5.99219L4.5657 5.99219L4.5657 4.98579Q4.56108 4.56569 4.26563 4.27255Q3.97017 3.9794 3.55007 3.9794Q3.12997 3.9794 2.83452 4.27255Q2.53906 4.56569 2.53906 4.98579Q2.53906 5.40589 2.83452 5.70135Q3.12997 5.9968 3.55007 5.99219ZM8.30043 5.99219L9.31144 5.99219Q9.72692 5.9968 10.0224 5.70135Q10.3178 5.40589 10.3178 4.98579Q10.3178 4.56569 10.0224 4.27255Q9.72692 3.9794 9.31144 3.9794Q8.89133 3.9794 8.59357 4.27255Q8.29581 4.56569 8.30043 4.98579L8.30043 5.99219ZM5.61364 8.67898L7.25249 8.67898L7.25249 7.04474L5.61364 7.04474L5.61364 8.67898Z" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
<template>
|
||||
<svg
|
||||
width="24px"
|
||||
height="20px"
|
||||
viewBox="0 0 24 20"
|
||||
version="1.1"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<path d="M0 0L24 0L24 20L0 20L0 0Z" id="path_1" />
|
||||
<clipPath id="clip_1">
|
||||
<use
|
||||
xlink:href="#path_1"
|
||||
clip-rule="evenodd"
|
||||
fill-rule="evenodd"
|
||||
transform="translate(0, -2.133523)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="cmd">
|
||||
<path
|
||||
d="M-751 -2016L-751 -2016L-751 -1996L-775 -1996L-775 -2016L-751 -2016Z"
|
||||
id="cmd"
|
||||
fill="none"
|
||||
stroke="none" />
|
||||
<path
|
||||
d="M12 0L17.6 0C19.8402 0 20.9603 0 21.816 0.435974Q22.3804 0.723593 22.8284 1.17157Q23.2764 1.61955 23.564 2.18404C24 3.03969 24 4.15979 24 6.4L24 13.6C24 15.8402 24 16.9603 23.564 17.816Q23.2764 18.3804 22.8284 18.8284Q22.3804 19.2764 21.816 19.564C20.9603 20 19.8402 20 17.6 20L6.4 20C4.15979 20 3.03969 20 2.18404 19.564Q1.61955 19.2764 1.17157 18.8284Q0.723594 18.3804 0.435974 17.816C0 16.9603 0 15.8402 0 13.6L0 6.4C0 4.15979 0 3.03969 0.435974 2.18404Q0.723594 1.61955 1.17157 1.17157Q1.61955 0.723594 2.18404 0.435974C3.03969 0 4.15979 0 6.4 0L12 0Z"
|
||||
id="Rectangle"
|
||||
fill="#FFFFFF"
|
||||
fill-opacity="0.050980393"
|
||||
stroke="none" />
|
||||
<g id="⌘" clip-path="url(#clip_1)" transform="translate(0 2.133523)">
|
||||
<g transform="translate(5.5692472, 0)" id="⌘" fill="#E5DFD5">
|
||||
<path
|
||||
d="M3.55007 12.8061Q2.98224 12.8061 2.51598 12.5268Q2.04972 12.2475 1.77042 11.7789Q1.49112 11.3104 1.49112 10.7472Q1.49112 10.1747 1.77042 9.70614Q2.04972 9.23757 2.51598 8.95827Q2.98224 8.67898 3.55007 8.67898L4.5657 8.67898L4.5657 7.04474L3.55007 7.04474Q2.98224 7.04474 2.51598 6.76775Q2.04972 6.49077 1.77042 6.02219Q1.49112 5.55362 1.49112 4.98579Q1.49112 4.41797 1.77042 3.9494Q2.04972 3.48082 2.51598 3.20383Q2.98224 2.92685 3.55007 2.92684Q4.1179 2.92684 4.58647 3.20383Q5.05504 3.48082 5.33434 3.9494Q5.61364 4.41797 5.61364 4.98579L5.61364 5.99219L7.25249 5.99219L7.25249 4.98579Q7.25249 4.41797 7.52947 3.9494Q7.80646 3.48082 8.27504 3.20383Q8.74361 2.92685 9.31144 2.92684Q9.87926 2.92684 10.3455 3.20383Q10.8118 3.48082 11.0888 3.9494Q11.3658 4.41797 11.3658 4.98579Q11.3658 5.55362 11.0888 6.02219Q10.8118 6.49077 10.3455 6.76775Q9.87926 7.04474 9.31144 7.04474L8.30043 7.04474L8.30043 8.67898L9.31144 8.67898Q9.87926 8.67898 10.3455 8.95827Q10.8118 9.23757 11.0888 9.70614Q11.3658 10.1747 11.3658 10.7472Q11.3658 11.3104 11.0888 11.7789Q10.8118 12.2475 10.3455 12.5268Q9.87926 12.8061 9.31144 12.8061Q8.74361 12.8061 8.27504 12.5268Q7.80646 12.2475 7.52947 11.7789Q7.25249 11.3104 7.25249 10.7472L7.25249 9.73153L5.61364 9.73153L5.61364 10.7472Q5.61364 11.3104 5.33434 11.7789Q5.05504 12.2475 4.58647 12.5268Q4.1179 12.8061 3.55007 12.8061ZM3.55007 11.7536Q3.97017 11.7536 4.26563 11.4604Q4.56108 11.1673 4.5657 10.7472L4.5657 9.73153L3.55007 9.73153Q3.12997 9.73615 2.83452 10.0293Q2.53906 10.3224 2.53906 10.7472Q2.53906 11.1673 2.83452 11.4604Q3.12997 11.7536 3.55007 11.7536ZM9.31144 11.7536Q9.72692 11.7536 10.0224 11.4604Q10.3178 11.1673 10.3178 10.7472Q10.3178 10.3224 10.0224 10.0293Q9.72692 9.73615 9.31144 9.73153L8.30043 9.73153L8.30043 10.7472Q8.29581 11.1673 8.59357 11.4604Q8.89133 11.7536 9.31144 11.7536ZM3.55007 5.99219L4.5657 5.99219L4.5657 4.98579Q4.56108 4.56569 4.26563 4.27255Q3.97017 3.9794 3.55007 3.9794Q3.12997 3.9794 2.83452 4.27255Q2.53906 4.56569 2.53906 4.98579Q2.53906 5.40589 2.83452 5.70135Q3.12997 5.9968 3.55007 5.99219ZM8.30043 5.99219L9.31144 5.99219Q9.72692 5.9968 10.0224 5.70135Q10.3178 5.40589 10.3178 4.98579Q10.3178 4.56569 10.0224 4.27255Q9.72692 3.9794 9.31144 3.9794Q8.89133 3.9794 8.59357 4.27255Q8.29581 4.56569 8.30043 4.98579L8.30043 5.99219ZM5.61364 8.67898L7.25249 8.67898L7.25249 7.04474L5.61364 7.04474L5.61364 8.67898Z" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
|
@ -1,41 +1,41 @@
|
|||
<template>
|
||||
<svg
|
||||
width="24px"
|
||||
height="20px"
|
||||
viewBox="0 0 24 20"
|
||||
version="1.1"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Enter" fill-opacity="1">
|
||||
<path
|
||||
d="M-659 -2016L-659 -2016L-659 -1996L-683 -1996L-683 -2016L-659 -2016Z"
|
||||
id="Enter"
|
||||
fill="none"
|
||||
stroke="none" />
|
||||
<path
|
||||
d="M12 0L17.6 0C19.8402 0 20.9603 0 21.816 0.435974Q22.3804 0.723593 22.8284 1.17157Q23.2764 1.61955 23.564 2.18404C24 3.03969 24 4.15979 24 6.4L24 13.6C24 15.8402 24 16.9603 23.564 17.816Q23.2764 18.3804 22.8284 18.8284Q22.3804 19.2764 21.816 19.564C20.9603 20 19.8402 20 17.6 20L6.4 20C4.15979 20 3.03969 20 2.18404 19.564Q1.61955 19.2764 1.17157 18.8284Q0.723594 18.3804 0.435974 17.816C0 16.9603 0 15.8402 0 13.6L0 6.4C0 4.15979 0 3.03969 0.435974 2.18404Q0.723594 1.61955 1.17157 1.17157Q1.61955 0.723594 2.18404 0.435974C3.03969 0 4.15979 0 6.4 0L12 0Z"
|
||||
id="Rectangle"
|
||||
fill="#FFFFFF"
|
||||
fill-opacity="0.050980393"
|
||||
stroke="none" />
|
||||
<path
|
||||
d="M16.0597 5.48914L16.0597 10.5L7.5 10.5"
|
||||
id="Vector"
|
||||
fill="none"
|
||||
fill-rule="evenodd"
|
||||
stroke="#E5DFD5"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path
|
||||
d="M9.5 8.5L9.5 12.5035L7 10.5L9.5 8.5Z"
|
||||
id="Vector"
|
||||
fill="#E5DFD5"
|
||||
fill-rule="evenodd"
|
||||
stroke="#E5DFD5"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
<template>
|
||||
<svg
|
||||
width="24px"
|
||||
height="20px"
|
||||
viewBox="0 0 24 20"
|
||||
version="1.1"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Enter" fill-opacity="1">
|
||||
<path
|
||||
d="M-659 -2016L-659 -2016L-659 -1996L-683 -1996L-683 -2016L-659 -2016Z"
|
||||
id="Enter"
|
||||
fill="none"
|
||||
stroke="none" />
|
||||
<path
|
||||
d="M12 0L17.6 0C19.8402 0 20.9603 0 21.816 0.435974Q22.3804 0.723593 22.8284 1.17157Q23.2764 1.61955 23.564 2.18404C24 3.03969 24 4.15979 24 6.4L24 13.6C24 15.8402 24 16.9603 23.564 17.816Q23.2764 18.3804 22.8284 18.8284Q22.3804 19.2764 21.816 19.564C20.9603 20 19.8402 20 17.6 20L6.4 20C4.15979 20 3.03969 20 2.18404 19.564Q1.61955 19.2764 1.17157 18.8284Q0.723594 18.3804 0.435974 17.816C0 16.9603 0 15.8402 0 13.6L0 6.4C0 4.15979 0 3.03969 0.435974 2.18404Q0.723594 1.61955 1.17157 1.17157Q1.61955 0.723594 2.18404 0.435974C3.03969 0 4.15979 0 6.4 0L12 0Z"
|
||||
id="Rectangle"
|
||||
fill="#FFFFFF"
|
||||
fill-opacity="0.050980393"
|
||||
stroke="none" />
|
||||
<path
|
||||
d="M16.0597 5.48914L16.0597 10.5L7.5 10.5"
|
||||
id="Vector"
|
||||
fill="none"
|
||||
fill-rule="evenodd"
|
||||
stroke="#E5DFD5"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path
|
||||
d="M9.5 8.5L9.5 12.5035L7 10.5L9.5 8.5Z"
|
||||
id="Vector"
|
||||
fill="#E5DFD5"
|
||||
fill-rule="evenodd"
|
||||
stroke="#E5DFD5"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
<template>
|
||||
<div
|
||||
class="key-container"
|
||||
:style="{
|
||||
backgroundColor: 'var(--border)',
|
||||
padding: '0 7px',
|
||||
height: '20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '4px',
|
||||
minWidth: '22px'
|
||||
}"
|
||||
>
|
||||
<span
|
||||
:style="{
|
||||
color: '#E5E0D5',
|
||||
fontSize: '12px'
|
||||
}"
|
||||
>
|
||||
{{ input }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
input: string;
|
||||
}>();
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="key-container"
|
||||
:style="{
|
||||
backgroundColor: 'var(--border)',
|
||||
padding: '0 7px',
|
||||
height: '20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '4px',
|
||||
minWidth: '22px'
|
||||
}"
|
||||
>
|
||||
<span
|
||||
:style="{
|
||||
color: '#E5E0D5',
|
||||
fontSize: '12px'
|
||||
}"
|
||||
>
|
||||
{{ input }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
input: string;
|
||||
}>();
|
||||
</script>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<svg width="24" height="20" viewBox="0 0 24 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path
|
||||
d="M12 0L17.6 0C19.8402 0 20.9603 0 21.816 0.435974Q22.3804 0.723593 22.8284 1.17157Q23.2764 1.61955 23.564 2.18404C24 3.03969 24 4.15979 24 6.4L24 13.6C24 15.8402 24 16.9603 23.564 17.816Q23.2764 18.3804 22.8284 18.8284Q22.3804 19.2764 21.816 19.564C20.9603 20 19.8402 20 17.6 20L6.4 20C4.15979 20 3.03969 20 2.18404 19.564Q1.61955 19.2764 1.17157 18.8284Q0.723594 18.3804 0.435974 17.816C0 16.9603 0 15.8402 0 13.6L0 6.4C0 4.15979 0 3.03969 0.435974 2.18404Q0.723594 1.61955 1.17157 1.17157Q1.61955 0.723594 2.18404 0.435974C3.03969 0 4.15979 0 6.4 0L12 0Z"
|
||||
fill="#FFFFFF" fill-opacity="0.051" />
|
||||
<path
|
||||
d="M4.9427 0.0799475L0.154716 5.27144Q0.144837 5.28216 0.138427 5.29524Q0.132016 5.30833 0.129608 5.3227Q0.127199 5.33707 0.128993 5.35153Q0.130787 5.36599 0.136635 5.37934Q0.142482 5.39269 0.151896 5.40381Q0.16131 5.41493 0.173507 5.42291Q0.185705 5.43088 0.19967 5.43504Q0.213636 5.4392 0.228208 5.4392L3.06459 5.4392Q3.08448 5.4392 3.10285 5.44681Q3.12123 5.45442 3.13529 5.46848Q3.14935 5.48254 3.15696 5.50092Q3.16457 5.51929 3.16457 5.53917L3.16457 9.90003Q3.16457 9.91991 3.17218 9.93828Q3.17979 9.95666 3.19385 9.97072Q3.20791 9.98478 3.22629 9.99239Q3.24466 10 3.26455 10L6.74521 10Q6.7651 10 6.78347 9.99239Q6.80184 9.98478 6.8159 9.97072Q6.82997 9.95666 6.83758 9.93828Q6.84519 9.91991 6.84519 9.90003L6.84519 5.53917Q6.84519 5.51929 6.8528 5.50091Q6.86041 5.48254 6.87447 5.46848Q6.88853 5.45442 6.9069 5.44681Q6.92527 5.4392 6.94516 5.4392L9.77281 5.4392Q9.78736 5.4392 9.8013 5.43505Q9.81525 5.4309 9.82744 5.42295Q9.83962 5.415 9.84904 5.4039Q9.85845 5.39281 9.86431 5.37949Q9.87017 5.36617 9.87199 5.35173Q9.87382 5.3373 9.87145 5.32294Q9.86908 5.30858 9.86271 5.2955Q9.85635 5.28241 9.84652 5.27168L5.0899 0.0801888Q5.07573 0.064719 5.05653 0.0562526Q5.03734 0.0477862 5.01635 0.0477518Q4.99537 0.0477174 4.97615 0.0561208Q4.95692 0.0645242 4.9427 0.0799475Z"
|
||||
fill="none" stroke-width="1.3" stroke="#E5DFD5" transform="translate(7 5)" />
|
||||
</g>
|
||||
</svg>
|
||||
<template>
|
||||
<svg width="24" height="20" viewBox="0 0 24 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path
|
||||
d="M12 0L17.6 0C19.8402 0 20.9603 0 21.816 0.435974Q22.3804 0.723593 22.8284 1.17157Q23.2764 1.61955 23.564 2.18404C24 3.03969 24 4.15979 24 6.4L24 13.6C24 15.8402 24 16.9603 23.564 17.816Q23.2764 18.3804 22.8284 18.8284Q22.3804 19.2764 21.816 19.564C20.9603 20 19.8402 20 17.6 20L6.4 20C4.15979 20 3.03969 20 2.18404 19.564Q1.61955 19.2764 1.17157 18.8284Q0.723594 18.3804 0.435974 17.816C0 16.9603 0 15.8402 0 13.6L0 6.4C0 4.15979 0 3.03969 0.435974 2.18404Q0.723594 1.61955 1.17157 1.17157Q1.61955 0.723594 2.18404 0.435974C3.03969 0 4.15979 0 6.4 0L12 0Z"
|
||||
fill="#FFFFFF" fill-opacity="0.051" />
|
||||
<path
|
||||
d="M4.9427 0.0799475L0.154716 5.27144Q0.144837 5.28216 0.138427 5.29524Q0.132016 5.30833 0.129608 5.3227Q0.127199 5.33707 0.128993 5.35153Q0.130787 5.36599 0.136635 5.37934Q0.142482 5.39269 0.151896 5.40381Q0.16131 5.41493 0.173507 5.42291Q0.185705 5.43088 0.19967 5.43504Q0.213636 5.4392 0.228208 5.4392L3.06459 5.4392Q3.08448 5.4392 3.10285 5.44681Q3.12123 5.45442 3.13529 5.46848Q3.14935 5.48254 3.15696 5.50092Q3.16457 5.51929 3.16457 5.53917L3.16457 9.90003Q3.16457 9.91991 3.17218 9.93828Q3.17979 9.95666 3.19385 9.97072Q3.20791 9.98478 3.22629 9.99239Q3.24466 10 3.26455 10L6.74521 10Q6.7651 10 6.78347 9.99239Q6.80184 9.98478 6.8159 9.97072Q6.82997 9.95666 6.83758 9.93828Q6.84519 9.91991 6.84519 9.90003L6.84519 5.53917Q6.84519 5.51929 6.8528 5.50091Q6.86041 5.48254 6.87447 5.46848Q6.88853 5.45442 6.9069 5.44681Q6.92527 5.4392 6.94516 5.4392L9.77281 5.4392Q9.78736 5.4392 9.8013 5.43505Q9.81525 5.4309 9.82744 5.42295Q9.83962 5.415 9.84904 5.4039Q9.85845 5.39281 9.86431 5.37949Q9.87017 5.36617 9.87199 5.35173Q9.87382 5.3373 9.87145 5.32294Q9.86908 5.30858 9.86271 5.2955Q9.85635 5.28241 9.84652 5.27168L5.0899 0.0801888Q5.07573 0.064719 5.05653 0.0562526Q5.03734 0.0477862 5.01635 0.0477518Q4.99537 0.0477174 4.97615 0.0561208Q4.95692 0.0645242 4.9427 0.0799475Z"
|
||||
fill="none" stroke-width="1.3" stroke="#E5DFD5" transform="translate(7 5)" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
|
@ -1,23 +1,23 @@
|
|||
<template>
|
||||
<div class="noise"></div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.noise {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
width: calc(100vw - 2px);
|
||||
height: calc(100vh - 2px);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
background-image: url("/noise.webp");
|
||||
background-repeat: repeat;
|
||||
image-rendering: pixelated;
|
||||
overflow: hidden;
|
||||
mix-blend-mode: multiply;
|
||||
opacity: .5;
|
||||
border-radius: 12px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="noise"></div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.noise {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
width: calc(100vw - 2px);
|
||||
height: calc(100vh - 2px);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
background-image: url("/noise.webp");
|
||||
background-repeat: repeat;
|
||||
image-rendering: pixelated;
|
||||
overflow: hidden;
|
||||
mix-blend-mode: multiply;
|
||||
opacity: .5;
|
||||
border-radius: 12px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,122 +1,122 @@
|
|||
<template>
|
||||
<div
|
||||
:class="['result', { selected }]"
|
||||
@click="$emit('select')"
|
||||
:ref="el => { if (selected && el) $emit('setRef', el as HTMLElement) }">
|
||||
<template v-if="item.content_type === 'image'">
|
||||
<img
|
||||
v-if="imageUrl"
|
||||
:src="imageUrl"
|
||||
alt="Image"
|
||||
class="image"
|
||||
@error="$emit('imageError')" />
|
||||
<IconsImage v-else class="icon" />
|
||||
</template>
|
||||
<template v-else-if="hasFavicon(item.favicon ?? '')">
|
||||
<img
|
||||
v-if="item.favicon"
|
||||
:src="getFaviconFromDb(item.favicon)"
|
||||
alt="Favicon"
|
||||
class="favicon"
|
||||
@error="
|
||||
($event.target as HTMLImageElement).src = '/public/icons/Link.svg'
|
||||
" />
|
||||
<IconsLink v-else class="icon" />
|
||||
</template>
|
||||
<IconsFile
|
||||
class="icon"
|
||||
v-else-if="item.content_type === ContentType.File" />
|
||||
<IconsText
|
||||
class="icon"
|
||||
v-else-if="item.content_type === ContentType.Text" />
|
||||
<svg
|
||||
v-else-if="item.content_type === ContentType.Color"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<rect width="18" height="18" />
|
||||
<path
|
||||
d="M9 18C12.2154 18 15.1865 16.2846 16.7942 13.5C18.4019 10.7154 18.4019 7.28461 16.7942 4.5C15.1865 1.71539 12.2154 -1.22615e-06 9 0C5.78461 0 2.81347 1.71539 1.20577 4.5C-0.401925 7.28461 -0.401923 10.7154 1.20577 13.5C2.81347 16.2846 5.78461 18 9 18Z"
|
||||
fill="#E5DFD5" />
|
||||
<path
|
||||
d="M9 16C7.14348 16 5.36301 15.2625 4.05025 13.9497C2.7375 12.637 2 10.8565 2 9C2 7.14348 2.7375 5.36301 4.05025 4.05025C5.36301 2.7375 7.14348 2 9 2C10.8565 2 12.637 2.7375 13.9497 4.05025C15.2625 5.36301 16 7.14348 16 9C16 10.8565 15.2625 12.637 13.9497 13.9497C12.637 15.2625 10.8565 16 9 16Z"
|
||||
:fill="item.content" />
|
||||
</g>
|
||||
</svg>
|
||||
<IconsCode
|
||||
class="icon"
|
||||
v-else-if="item.content_type === ContentType.Code" />
|
||||
<span v-if="item.content_type === ContentType.Image">
|
||||
Image ({{ dimensions || "Loading..." }})
|
||||
</span>
|
||||
<span v-else>{{ truncateContent(item.content) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ContentType } from "~/types/types";
|
||||
import type { HistoryItem } from "~/types/types";
|
||||
|
||||
defineProps<{
|
||||
item: HistoryItem;
|
||||
selected: boolean;
|
||||
imageUrl?: string;
|
||||
dimensions?: string;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(e: "select"): void;
|
||||
(e: "imageError"): void;
|
||||
(e: "setRef", el: HTMLElement): void;
|
||||
}>();
|
||||
|
||||
const hasFavicon = (str: string): boolean => {
|
||||
return str.trim() !== "";
|
||||
};
|
||||
|
||||
const getFaviconFromDb = (favicon: string): string => {
|
||||
return `data:image/png;base64,${favicon}`;
|
||||
};
|
||||
|
||||
const truncateContent = (content: string): string => {
|
||||
const maxWidth = 284;
|
||||
const charWidth = 9;
|
||||
const maxChars = Math.floor(maxWidth / charWidth);
|
||||
return content.length > maxChars
|
||||
? content.slice(0, maxChars - 3) + "..."
|
||||
: content;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.result {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 11px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
|
||||
&.selected {
|
||||
background-color: var(--border);
|
||||
}
|
||||
|
||||
.favicon,
|
||||
.image,
|
||||
.icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div
|
||||
:class="['result', { selected }]"
|
||||
@click="$emit('select')"
|
||||
:ref="el => { if (selected && el) $emit('setRef', el as HTMLElement) }">
|
||||
<template v-if="item.content_type === 'image'">
|
||||
<img
|
||||
v-if="imageUrl"
|
||||
:src="imageUrl"
|
||||
alt="Image"
|
||||
class="image"
|
||||
@error="$emit('imageError')" />
|
||||
<IconsImage v-else class="icon" />
|
||||
</template>
|
||||
<template v-else-if="hasFavicon(item.favicon ?? '')">
|
||||
<img
|
||||
v-if="item.favicon"
|
||||
:src="getFaviconFromDb(item.favicon)"
|
||||
alt="Favicon"
|
||||
class="favicon"
|
||||
@error="
|
||||
($event.target as HTMLImageElement).src = '/public/icons/Link.svg'
|
||||
" />
|
||||
<IconsLink v-else class="icon" />
|
||||
</template>
|
||||
<IconsFile
|
||||
class="icon"
|
||||
v-else-if="item.content_type === ContentType.File" />
|
||||
<IconsText
|
||||
class="icon"
|
||||
v-else-if="item.content_type === ContentType.Text" />
|
||||
<svg
|
||||
v-else-if="item.content_type === ContentType.Color"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<rect width="18" height="18" />
|
||||
<path
|
||||
d="M9 18C12.2154 18 15.1865 16.2846 16.7942 13.5C18.4019 10.7154 18.4019 7.28461 16.7942 4.5C15.1865 1.71539 12.2154 -1.22615e-06 9 0C5.78461 0 2.81347 1.71539 1.20577 4.5C-0.401925 7.28461 -0.401923 10.7154 1.20577 13.5C2.81347 16.2846 5.78461 18 9 18Z"
|
||||
fill="#E5DFD5" />
|
||||
<path
|
||||
d="M9 16C7.14348 16 5.36301 15.2625 4.05025 13.9497C2.7375 12.637 2 10.8565 2 9C2 7.14348 2.7375 5.36301 4.05025 4.05025C5.36301 2.7375 7.14348 2 9 2C10.8565 2 12.637 2.7375 13.9497 4.05025C15.2625 5.36301 16 7.14348 16 9C16 10.8565 15.2625 12.637 13.9497 13.9497C12.637 15.2625 10.8565 16 9 16Z"
|
||||
:fill="item.content" />
|
||||
</g>
|
||||
</svg>
|
||||
<IconsCode
|
||||
class="icon"
|
||||
v-else-if="item.content_type === ContentType.Code" />
|
||||
<span v-if="item.content_type === ContentType.Image">
|
||||
Image ({{ dimensions || "Loading..." }})
|
||||
</span>
|
||||
<span v-else>{{ truncateContent(item.content) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ContentType } from "~/types/types";
|
||||
import type { HistoryItem } from "~/types/types";
|
||||
|
||||
defineProps<{
|
||||
item: HistoryItem;
|
||||
selected: boolean;
|
||||
imageUrl?: string;
|
||||
dimensions?: string;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(e: "select"): void;
|
||||
(e: "imageError"): void;
|
||||
(e: "setRef", el: HTMLElement): void;
|
||||
}>();
|
||||
|
||||
const hasFavicon = (str: string): boolean => {
|
||||
return str.trim() !== "";
|
||||
};
|
||||
|
||||
const getFaviconFromDb = (favicon: string): string => {
|
||||
return `data:image/png;base64,${favicon}`;
|
||||
};
|
||||
|
||||
const truncateContent = (content: string): string => {
|
||||
const maxWidth = 284;
|
||||
const charWidth = 9;
|
||||
const maxChars = Math.floor(maxWidth / charWidth);
|
||||
return content.length > maxChars
|
||||
? content.slice(0, maxChars - 3) + "..."
|
||||
: content;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.result {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 11px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
|
||||
&.selected {
|
||||
background-color: var(--border);
|
||||
}
|
||||
|
||||
.favicon,
|
||||
.image,
|
||||
.icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,57 +1,57 @@
|
|||
<template>
|
||||
<div class="topbar">
|
||||
<input
|
||||
ref="searchInput"
|
||||
v-model="searchQuery"
|
||||
@input="onInputChange"
|
||||
class="search"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
type="text"
|
||||
placeholder="Type to filter entries..." />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
const searchQuery = ref("");
|
||||
const searchInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "search", query: string): void;
|
||||
(e: "searchStarted"): void;
|
||||
(e: "focus"): void;
|
||||
}>();
|
||||
|
||||
const onInputChange = () => {
|
||||
emit("searchStarted");
|
||||
emit("search", searchQuery.value);
|
||||
};
|
||||
|
||||
defineExpose({ searchInput });
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.topbar {
|
||||
width: 100%;
|
||||
min-height: 56px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-inline: 16px;
|
||||
z-index: 100;
|
||||
|
||||
.search {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 18px;
|
||||
color: var(--text);
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
border: none;
|
||||
font-family: SFRoundedMedium;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="topbar">
|
||||
<input
|
||||
ref="searchInput"
|
||||
v-model="searchQuery"
|
||||
@input="onInputChange"
|
||||
class="search"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
type="text"
|
||||
placeholder="Type to filter entries..." />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
const searchQuery = ref("");
|
||||
const searchInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "search", query: string): void;
|
||||
(e: "searchStarted"): void;
|
||||
(e: "focus"): void;
|
||||
}>();
|
||||
|
||||
const onInputChange = () => {
|
||||
emit("searchStarted");
|
||||
emit("search", searchQuery.value);
|
||||
};
|
||||
|
||||
defineExpose({ searchInput });
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.topbar {
|
||||
width: 100%;
|
||||
min-height: 56px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-inline: 16px;
|
||||
z-index: 100;
|
||||
|
||||
.search {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 18px;
|
||||
color: var(--text);
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
border: none;
|
||||
font-family: SFRoundedMedium;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,222 +1,222 @@
|
|||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { HistoryItem } from "../types/types";
|
||||
|
||||
const { $history } = useNuxtApp();
|
||||
const { hideApp } = useAppControl();
|
||||
|
||||
export function useActions() {
|
||||
const isProcessing = ref(false);
|
||||
|
||||
const handleAction = async (action: string, item?: HistoryItem) => {
|
||||
if (!item && action !== "settings" && action !== "delete-all") return;
|
||||
|
||||
isProcessing.value = true;
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case "paste-to-app":
|
||||
await pasteToCurrentApp(item);
|
||||
break;
|
||||
case "copy":
|
||||
// await copyToClipboard(item);
|
||||
break;
|
||||
case "delete":
|
||||
await deleteEntry(item);
|
||||
break;
|
||||
case "delete-all":
|
||||
// await deleteAllEntries();
|
||||
break;
|
||||
case "settings":
|
||||
openSettings();
|
||||
break;
|
||||
case "paste-plain":
|
||||
// await pasteAsPlainText(item);
|
||||
break;
|
||||
case "edit-text":
|
||||
// openTextEditor(item);
|
||||
break;
|
||||
case "rotate-image":
|
||||
// await rotateImage(item);
|
||||
break;
|
||||
case "resize-image":
|
||||
// openImageResizer(item);
|
||||
break;
|
||||
case "compress-image":
|
||||
// await compressImage(item);
|
||||
break;
|
||||
case "open-file":
|
||||
// await openFile(item);
|
||||
break;
|
||||
case "compress-file":
|
||||
// await compressFile(item);
|
||||
break;
|
||||
case "open-link":
|
||||
// await openInBrowser(item);
|
||||
break;
|
||||
case "copy-hex":
|
||||
// await copyColorFormat(item, "hex");
|
||||
break;
|
||||
case "copy-rgba":
|
||||
// await copyColorFormat(item, "rgba");
|
||||
break;
|
||||
case "copy-hsla":
|
||||
// await copyColorFormat(item, "hsla");
|
||||
break;
|
||||
default:
|
||||
console.warn(`Action ${action} not implemented`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error executing action ${action}:`, error);
|
||||
} finally {
|
||||
isProcessing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const pasteToCurrentApp = async (item?: HistoryItem) => {
|
||||
if (!item) return;
|
||||
|
||||
let content = item.content;
|
||||
let contentType: string = item.content_type;
|
||||
if (contentType === "image") {
|
||||
try {
|
||||
content = await $history.readImage({ filename: content });
|
||||
} catch (error) {
|
||||
console.error("Error reading image file:", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await hideApp();
|
||||
await $history.writeAndPaste({ content, contentType });
|
||||
};
|
||||
|
||||
// const copyToClipboard = async (item?: HistoryItem) => {
|
||||
// if (!item) return;
|
||||
|
||||
// try {
|
||||
// switch (item.content_type) {
|
||||
// case ContentType.Text:
|
||||
// case ContentType.Link:
|
||||
// case ContentType.Code:
|
||||
// await writeText(item.content);
|
||||
// break;
|
||||
// case ContentType.Image:
|
||||
// await invoke("copy_image_to_clipboard", { path: item.file_path });
|
||||
// break;
|
||||
// case ContentType.File:
|
||||
// await invoke("copy_file_reference", { path: item.file_path });
|
||||
// break;
|
||||
// case ContentType.Color:
|
||||
// await writeText(item.content);
|
||||
// break;
|
||||
// default:
|
||||
// console.warn(`Copying type ${item.content_type} not implemented`);
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error("Failed to copy to clipboard:", error);
|
||||
// }
|
||||
// };
|
||||
|
||||
const deleteEntry = async (item?: HistoryItem) => {
|
||||
if (!item) return;
|
||||
try {
|
||||
await invoke("delete_history_item", { id: item.id });
|
||||
} catch (error) {
|
||||
console.error("Failed to delete entry:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// const deleteAllEntries = async () => {
|
||||
// try {
|
||||
// await invoke('delete_all_history');
|
||||
// } catch (error) {
|
||||
// console.error('Failed to delete all entries:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
const openSettings = () => {
|
||||
navigateTo("/settings");
|
||||
};
|
||||
|
||||
// const pasteAsPlainText = async (item?: HistoryItem) => {
|
||||
// if (!item) return;
|
||||
// try {
|
||||
// await invoke('paste_as_plain_text', { content: item.content });
|
||||
// } catch (error) {
|
||||
// console.error('Failed to paste as plain text:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const openTextEditor = (item?: HistoryItem) => {
|
||||
// if (!item) return;
|
||||
// // Implement logic to open text editor with the content
|
||||
// // This might use Nuxt router or a modal based on your app architecture
|
||||
// };
|
||||
|
||||
// const rotateImage = async (item?: HistoryItem) => {
|
||||
// if (!item || item.content_type !== ContentType.Image) return;
|
||||
// try {
|
||||
// await invoke('rotate_image', { path: item.file_path });
|
||||
// } catch (error) {
|
||||
// console.error('Failed to rotate image:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const openImageResizer = (item?: HistoryItem) => {
|
||||
// if (!item || item.content_type !== ContentType.Image) return;
|
||||
// // Implement logic to open image resizer UI for this image
|
||||
// };
|
||||
|
||||
// const compressImage = async (item?: HistoryItem) => {
|
||||
// if (!item || item.content_type !== ContentType.Image) return;
|
||||
// try {
|
||||
// await invoke('compress_image', { path: item.file_path });
|
||||
// } catch (error) {
|
||||
// console.error('Failed to compress image:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const openFile = async (item?: HistoryItem) => {
|
||||
// if (!item || item.content_type !== ContentType.File) return;
|
||||
// try {
|
||||
// await invoke('open_file', { path: item.file_path });
|
||||
// } catch (error) {
|
||||
// console.error('Failed to open file:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const compressFile = async (item?: HistoryItem) => {
|
||||
// if (!item || item.content_type !== ContentType.File) return;
|
||||
// try {
|
||||
// await invoke('compress_file', { path: item.file_path });
|
||||
// } catch (error) {
|
||||
// console.error('Failed to compress file:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const openInBrowser = async (item?: HistoryItem) => {
|
||||
// if (!item || item.content_type !== ContentType.Link) return;
|
||||
// try {
|
||||
// await invoke('open_url', { url: item.content });
|
||||
// } catch (error) {
|
||||
// console.error('Failed to open URL in browser:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const copyColorFormat = async (item?: HistoryItem, format: 'hex' | 'rgba' | 'hsla' = 'hex') => {
|
||||
// if (!item || item.content_type !== ContentType.Color) return;
|
||||
// try {
|
||||
// const formattedColor = await invoke('get_color_format', {
|
||||
// color: item.content,
|
||||
// format
|
||||
// });
|
||||
// await writeText(formattedColor as string);
|
||||
// } catch (error) {
|
||||
// console.error(`Failed to copy color as ${format}:`, error);
|
||||
// }
|
||||
// };
|
||||
|
||||
return {
|
||||
handleAction,
|
||||
isProcessing,
|
||||
};
|
||||
}
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { HistoryItem } from "../types/types";
|
||||
|
||||
const { $history } = useNuxtApp();
|
||||
const { hideApp } = useAppControl();
|
||||
|
||||
export function useActions() {
|
||||
const isProcessing = ref(false);
|
||||
|
||||
const handleAction = async (action: string, item?: HistoryItem) => {
|
||||
if (!item && action !== "settings" && action !== "delete-all") return;
|
||||
|
||||
isProcessing.value = true;
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case "paste-to-app":
|
||||
await pasteToCurrentApp(item);
|
||||
break;
|
||||
case "copy":
|
||||
// await copyToClipboard(item);
|
||||
break;
|
||||
case "delete":
|
||||
await deleteEntry(item);
|
||||
break;
|
||||
case "delete-all":
|
||||
// await deleteAllEntries();
|
||||
break;
|
||||
case "settings":
|
||||
openSettings();
|
||||
break;
|
||||
case "paste-plain":
|
||||
// await pasteAsPlainText(item);
|
||||
break;
|
||||
case "edit-text":
|
||||
// openTextEditor(item);
|
||||
break;
|
||||
case "rotate-image":
|
||||
// await rotateImage(item);
|
||||
break;
|
||||
case "resize-image":
|
||||
// openImageResizer(item);
|
||||
break;
|
||||
case "compress-image":
|
||||
// await compressImage(item);
|
||||
break;
|
||||
case "open-file":
|
||||
// await openFile(item);
|
||||
break;
|
||||
case "compress-file":
|
||||
// await compressFile(item);
|
||||
break;
|
||||
case "open-link":
|
||||
// await openInBrowser(item);
|
||||
break;
|
||||
case "copy-hex":
|
||||
// await copyColorFormat(item, "hex");
|
||||
break;
|
||||
case "copy-rgba":
|
||||
// await copyColorFormat(item, "rgba");
|
||||
break;
|
||||
case "copy-hsla":
|
||||
// await copyColorFormat(item, "hsla");
|
||||
break;
|
||||
default:
|
||||
console.warn(`Action ${action} not implemented`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error executing action ${action}:`, error);
|
||||
} finally {
|
||||
isProcessing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const pasteToCurrentApp = async (item?: HistoryItem) => {
|
||||
if (!item) return;
|
||||
|
||||
let content = item.content;
|
||||
let contentType: string = item.content_type;
|
||||
if (contentType === "image") {
|
||||
try {
|
||||
content = await $history.readImage({ filename: content });
|
||||
} catch (error) {
|
||||
console.error("Error reading image file:", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await hideApp();
|
||||
await $history.writeAndPaste({ content, contentType });
|
||||
};
|
||||
|
||||
// const copyToClipboard = async (item?: HistoryItem) => {
|
||||
// if (!item) return;
|
||||
|
||||
// try {
|
||||
// switch (item.content_type) {
|
||||
// case ContentType.Text:
|
||||
// case ContentType.Link:
|
||||
// case ContentType.Code:
|
||||
// await writeText(item.content);
|
||||
// break;
|
||||
// case ContentType.Image:
|
||||
// await invoke("copy_image_to_clipboard", { path: item.file_path });
|
||||
// break;
|
||||
// case ContentType.File:
|
||||
// await invoke("copy_file_reference", { path: item.file_path });
|
||||
// break;
|
||||
// case ContentType.Color:
|
||||
// await writeText(item.content);
|
||||
// break;
|
||||
// default:
|
||||
// console.warn(`Copying type ${item.content_type} not implemented`);
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error("Failed to copy to clipboard:", error);
|
||||
// }
|
||||
// };
|
||||
|
||||
const deleteEntry = async (item?: HistoryItem) => {
|
||||
if (!item) return;
|
||||
try {
|
||||
await invoke("delete_history_item", { id: item.id });
|
||||
} catch (error) {
|
||||
console.error("Failed to delete entry:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// const deleteAllEntries = async () => {
|
||||
// try {
|
||||
// await invoke('delete_all_history');
|
||||
// } catch (error) {
|
||||
// console.error('Failed to delete all entries:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
const openSettings = () => {
|
||||
navigateTo("/settings");
|
||||
};
|
||||
|
||||
// const pasteAsPlainText = async (item?: HistoryItem) => {
|
||||
// if (!item) return;
|
||||
// try {
|
||||
// await invoke('paste_as_plain_text', { content: item.content });
|
||||
// } catch (error) {
|
||||
// console.error('Failed to paste as plain text:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const openTextEditor = (item?: HistoryItem) => {
|
||||
// if (!item) return;
|
||||
// // Implement logic to open text editor with the content
|
||||
// // This might use Nuxt router or a modal based on your app architecture
|
||||
// };
|
||||
|
||||
// const rotateImage = async (item?: HistoryItem) => {
|
||||
// if (!item || item.content_type !== ContentType.Image) return;
|
||||
// try {
|
||||
// await invoke('rotate_image', { path: item.file_path });
|
||||
// } catch (error) {
|
||||
// console.error('Failed to rotate image:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const openImageResizer = (item?: HistoryItem) => {
|
||||
// if (!item || item.content_type !== ContentType.Image) return;
|
||||
// // Implement logic to open image resizer UI for this image
|
||||
// };
|
||||
|
||||
// const compressImage = async (item?: HistoryItem) => {
|
||||
// if (!item || item.content_type !== ContentType.Image) return;
|
||||
// try {
|
||||
// await invoke('compress_image', { path: item.file_path });
|
||||
// } catch (error) {
|
||||
// console.error('Failed to compress image:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const openFile = async (item?: HistoryItem) => {
|
||||
// if (!item || item.content_type !== ContentType.File) return;
|
||||
// try {
|
||||
// await invoke('open_file', { path: item.file_path });
|
||||
// } catch (error) {
|
||||
// console.error('Failed to open file:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const compressFile = async (item?: HistoryItem) => {
|
||||
// if (!item || item.content_type !== ContentType.File) return;
|
||||
// try {
|
||||
// await invoke('compress_file', { path: item.file_path });
|
||||
// } catch (error) {
|
||||
// console.error('Failed to compress file:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const openInBrowser = async (item?: HistoryItem) => {
|
||||
// if (!item || item.content_type !== ContentType.Link) return;
|
||||
// try {
|
||||
// await invoke('open_url', { url: item.content });
|
||||
// } catch (error) {
|
||||
// console.error('Failed to open URL in browser:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const copyColorFormat = async (item?: HistoryItem, format: 'hex' | 'rgba' | 'hsla' = 'hex') => {
|
||||
// if (!item || item.content_type !== ContentType.Color) return;
|
||||
// try {
|
||||
// const formattedColor = await invoke('get_color_format', {
|
||||
// color: item.content,
|
||||
// format
|
||||
// });
|
||||
// await writeText(formattedColor as string);
|
||||
// } catch (error) {
|
||||
// console.error(`Failed to copy color as ${format}:`, error);
|
||||
// }
|
||||
// };
|
||||
|
||||
return {
|
||||
handleAction,
|
||||
isProcessing,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { app, window } from "@tauri-apps/api";
|
||||
|
||||
export function useAppControl() {
|
||||
const hideApp = async (): Promise<void> => {
|
||||
await app.hide();
|
||||
await window.getCurrentWindow().hide();
|
||||
};
|
||||
|
||||
return {
|
||||
hideApp
|
||||
};
|
||||
import { app, window } from "@tauri-apps/api";
|
||||
|
||||
export function useAppControl() {
|
||||
const hideApp = async (): Promise<void> => {
|
||||
await app.hide();
|
||||
await window.getCurrentWindow().hide();
|
||||
};
|
||||
|
||||
return {
|
||||
hideApp
|
||||
};
|
||||
}
|
|
@ -1,24 +1,24 @@
|
|||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
devtools: { enabled: false },
|
||||
compatibilityDate: "2024-07-04",
|
||||
ssr: false,
|
||||
|
||||
app: {
|
||||
head: {
|
||||
charset: "utf-8",
|
||||
viewport:
|
||||
"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0",
|
||||
},
|
||||
},
|
||||
|
||||
vite: {
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: "modern-compiler",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
devtools: { enabled: false },
|
||||
compatibilityDate: "2024-07-04",
|
||||
ssr: false,
|
||||
|
||||
app: {
|
||||
head: {
|
||||
charset: "utf-8",
|
||||
viewport:
|
||||
"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0",
|
||||
},
|
||||
},
|
||||
|
||||
vite: {
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: "modern-compiler",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
56
package.json
56
package.json
|
@ -1,28 +1,28 @@
|
|||
{
|
||||
"name": "nuxt-app",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tauri build",
|
||||
"dev": "tauri dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "2.3.0",
|
||||
"@tauri-apps/cli": "2.3.1",
|
||||
"@tauri-apps/plugin-autostart": "2.2.0",
|
||||
"@tauri-apps/plugin-os": "2.2.1",
|
||||
"nuxt": "3.16.0",
|
||||
"overlayscrollbars": "2.11.1",
|
||||
"overlayscrollbars-vue": "0.5.9",
|
||||
"sass-embedded": "1.85.1",
|
||||
"uuid": "11.1.0",
|
||||
"vue": "3.5.13",
|
||||
"@waradu/keyboard": "4.2.0"
|
||||
},
|
||||
"overrides": {
|
||||
"chokidar": "^3.6.0"
|
||||
}
|
||||
}
|
||||
{
|
||||
"name": "nuxt-app",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tauri build",
|
||||
"dev": "tauri dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "2.3.0",
|
||||
"@tauri-apps/cli": "2.3.1",
|
||||
"@tauri-apps/plugin-autostart": "2.2.0",
|
||||
"@tauri-apps/plugin-os": "2.2.1",
|
||||
"nuxt": "3.16.0",
|
||||
"overlayscrollbars": "2.11.1",
|
||||
"overlayscrollbars-vue": "0.5.9",
|
||||
"sass-embedded": "1.85.1",
|
||||
"uuid": "11.1.0",
|
||||
"vue": "3.5.13",
|
||||
"@waradu/keyboard": "4.2.0"
|
||||
},
|
||||
"overrides": {
|
||||
"chokidar": "^3.6.0"
|
||||
}
|
||||
}
|
||||
|
|
1892
pages/index.vue
1892
pages/index.vue
File diff suppressed because it is too large
Load diff
|
@ -1,209 +1,209 @@
|
|||
<template>
|
||||
<main>
|
||||
<div class="top-bar">
|
||||
<NuxtLink to="/" class="back">
|
||||
<img src="../public/back_arrow.svg" />
|
||||
<p>Back</p>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="settings-container">
|
||||
<div class="settings">
|
||||
<div class="names">
|
||||
<p style="line-height: 14px">Startup</p>
|
||||
<p style="line-height: 34px">Qopy Hotkey</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="launch">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="launch"
|
||||
v-model="autostart"
|
||||
@change="toggleAutostart" />
|
||||
<label for="launch" class="checkmark">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<rect width="14" height="14" />
|
||||
<path
|
||||
id="Path"
|
||||
d="M0 2.00696L2.25015 4.25L6 0"
|
||||
fill="none"
|
||||
stroke-width="1.5"
|
||||
stroke="#E5DFD5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
transform="translate(4 5)" />
|
||||
</g>
|
||||
</svg>
|
||||
</label>
|
||||
<p for="launch">Launch Qopy at login</p>
|
||||
</div>
|
||||
<div
|
||||
@blur="onBlur"
|
||||
@focus="onFocus"
|
||||
class="keybind-input"
|
||||
ref="keybindInput"
|
||||
tabindex="0"
|
||||
:class="{ 'empty-keybind': showEmptyKeybindError }">
|
||||
<span class="key" v-if="keybind.length === 0">Click here</span>
|
||||
<template v-else>
|
||||
<span
|
||||
:key="index"
|
||||
class="key"
|
||||
:class="{ modifier: isModifier(key) }"
|
||||
v-for="(key, index) in keybind">
|
||||
{{ keyToLabel(key) }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BottomBar
|
||||
:primary-action="{
|
||||
text: 'Save',
|
||||
icon: IconsEnter,
|
||||
onClick: saveKeybind,
|
||||
showModifier: true,
|
||||
}" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, reactive, ref, watch } from "vue";
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import { useRouter } from "vue-router";
|
||||
import { KeyValues, KeyLabels } from "../types/keys";
|
||||
import { disable, enable } from "@tauri-apps/plugin-autostart";
|
||||
import { useNuxtApp } from "#app";
|
||||
import BottomBar from "../components/BottomBar.vue";
|
||||
import IconsEnter from "~/components/Keys/Enter.vue";
|
||||
|
||||
const activeModifiers = reactive<Set<KeyValues>>(new Set());
|
||||
const isKeybindInputFocused = ref(false);
|
||||
const keybind = ref<KeyValues[]>([]);
|
||||
const keybindInput = ref<HTMLElement | null>(null);
|
||||
const blurredByEscape = ref(false);
|
||||
const router = useRouter();
|
||||
const showEmptyKeybindError = ref(false);
|
||||
const autostart = ref(false);
|
||||
const { $settings, $keyboard } = useNuxtApp();
|
||||
|
||||
const listeners: Array<() => void> = [];
|
||||
|
||||
const modifierKeySet = new Set([
|
||||
KeyValues.AltLeft,
|
||||
KeyValues.AltRight,
|
||||
KeyValues.ControlLeft,
|
||||
KeyValues.ControlRight,
|
||||
KeyValues.MetaLeft,
|
||||
KeyValues.MetaRight,
|
||||
KeyValues.ShiftLeft,
|
||||
KeyValues.ShiftRight,
|
||||
]);
|
||||
|
||||
const isModifier = (key: KeyValues): boolean => {
|
||||
return modifierKeySet.has(key);
|
||||
};
|
||||
|
||||
const keyToLabel = (key: KeyValues): string => {
|
||||
return KeyLabels[key] || key;
|
||||
};
|
||||
|
||||
const updateKeybindDisplay = () => {
|
||||
const modifiers = Array.from(activeModifiers);
|
||||
const nonModifiers = keybind.value.filter((key) => !isModifier(key));
|
||||
const sortedModifiers = modifiers.sort();
|
||||
keybind.value = [...sortedModifiers, ...nonModifiers];
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
isKeybindInputFocused.value = false;
|
||||
showEmptyKeybindError.value = false;
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
isKeybindInputFocused.value = true;
|
||||
blurredByEscape.value = false;
|
||||
activeModifiers.clear();
|
||||
keybind.value = [];
|
||||
showEmptyKeybindError.value = false;
|
||||
|
||||
const unlistenAll = $keyboard.listen([$keyboard.Key.All], (event: KeyboardEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const key = event.code as KeyValues;
|
||||
|
||||
if (key === KeyValues.Escape) {
|
||||
blurredByEscape.value = true;
|
||||
keybindInput.value?.blur();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isModifier(key)) {
|
||||
activeModifiers.add(key);
|
||||
} else {
|
||||
const nonModifierKey = keybind.value.find(k => !isModifier(k));
|
||||
if (!nonModifierKey || nonModifierKey === key) {
|
||||
keybind.value = Array.from(activeModifiers);
|
||||
if (nonModifierKey !== key) keybind.value.push(key);
|
||||
} else {
|
||||
keybind.value = [ ...Array.from(activeModifiers), key];
|
||||
}
|
||||
}
|
||||
updateKeybindDisplay();
|
||||
showEmptyKeybindError.value = false;
|
||||
}, { prevent: true });
|
||||
listeners.push(unlistenAll);
|
||||
};
|
||||
|
||||
const saveKeybind = async () => {
|
||||
const finalKeybind = keybind.value.filter(k => k);
|
||||
if (finalKeybind.length > 0) {
|
||||
await $settings.saveSetting("keybind", JSON.stringify(finalKeybind));
|
||||
router.push("/");
|
||||
} else {
|
||||
showEmptyKeybindError.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAutostart = async () => {
|
||||
if (autostart.value === true) {
|
||||
await enable();
|
||||
} else {
|
||||
await disable();
|
||||
}
|
||||
await $settings.saveSetting("autostart", autostart.value ? "true" : "false");
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
autostart.value = (await $settings.getSetting("autostart")) === "true";
|
||||
|
||||
const metaOrCtrlKey = $keyboard.currentOS === "macos" ? $keyboard.Key.Meta : $keyboard.Key.Control;
|
||||
listeners.push(
|
||||
$keyboard.listen([metaOrCtrlKey, $keyboard.Key.Enter], saveKeybind, { prevent: true, ignoreIfEditable: true })
|
||||
);
|
||||
|
||||
listeners.push(
|
||||
$keyboard.listen([$keyboard.Key.Escape], () => {
|
||||
if (!isKeybindInputFocused.value && !blurredByEscape.value) {
|
||||
router.push("/");
|
||||
}
|
||||
if(blurredByEscape.value) blurredByEscape.value = false;
|
||||
}, { prevent: true })
|
||||
);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
listeners.forEach(unlisten => unlisten());
|
||||
listeners.length = 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "/styles/settings.scss";
|
||||
</style>
|
||||
<template>
|
||||
<main>
|
||||
<div class="top-bar">
|
||||
<NuxtLink to="/" class="back">
|
||||
<img src="../public/back_arrow.svg" />
|
||||
<p>Back</p>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="settings-container">
|
||||
<div class="settings">
|
||||
<div class="names">
|
||||
<p style="line-height: 14px">Startup</p>
|
||||
<p style="line-height: 34px">Qopy Hotkey</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="launch">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="launch"
|
||||
v-model="autostart"
|
||||
@change="toggleAutostart" />
|
||||
<label for="launch" class="checkmark">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<rect width="14" height="14" />
|
||||
<path
|
||||
id="Path"
|
||||
d="M0 2.00696L2.25015 4.25L6 0"
|
||||
fill="none"
|
||||
stroke-width="1.5"
|
||||
stroke="#E5DFD5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
transform="translate(4 5)" />
|
||||
</g>
|
||||
</svg>
|
||||
</label>
|
||||
<p for="launch">Launch Qopy at login</p>
|
||||
</div>
|
||||
<div
|
||||
@blur="onBlur"
|
||||
@focus="onFocus"
|
||||
class="keybind-input"
|
||||
ref="keybindInput"
|
||||
tabindex="0"
|
||||
:class="{ 'empty-keybind': showEmptyKeybindError }">
|
||||
<span class="key" v-if="keybind.length === 0">Click here</span>
|
||||
<template v-else>
|
||||
<span
|
||||
:key="index"
|
||||
class="key"
|
||||
:class="{ modifier: isModifier(key) }"
|
||||
v-for="(key, index) in keybind">
|
||||
{{ keyToLabel(key) }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BottomBar
|
||||
:primary-action="{
|
||||
text: 'Save',
|
||||
icon: IconsEnter,
|
||||
onClick: saveKeybind,
|
||||
showModifier: true,
|
||||
}" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, reactive, ref, watch } from "vue";
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import { useRouter } from "vue-router";
|
||||
import { KeyValues, KeyLabels } from "../types/keys";
|
||||
import { disable, enable } from "@tauri-apps/plugin-autostart";
|
||||
import { useNuxtApp } from "#app";
|
||||
import BottomBar from "../components/BottomBar.vue";
|
||||
import IconsEnter from "~/components/Keys/Enter.vue";
|
||||
|
||||
const activeModifiers = reactive<Set<KeyValues>>(new Set());
|
||||
const isKeybindInputFocused = ref(false);
|
||||
const keybind = ref<KeyValues[]>([]);
|
||||
const keybindInput = ref<HTMLElement | null>(null);
|
||||
const blurredByEscape = ref(false);
|
||||
const router = useRouter();
|
||||
const showEmptyKeybindError = ref(false);
|
||||
const autostart = ref(false);
|
||||
const { $settings, $keyboard } = useNuxtApp();
|
||||
|
||||
const listeners: Array<() => void> = [];
|
||||
|
||||
const modifierKeySet = new Set([
|
||||
KeyValues.AltLeft,
|
||||
KeyValues.AltRight,
|
||||
KeyValues.ControlLeft,
|
||||
KeyValues.ControlRight,
|
||||
KeyValues.MetaLeft,
|
||||
KeyValues.MetaRight,
|
||||
KeyValues.ShiftLeft,
|
||||
KeyValues.ShiftRight,
|
||||
]);
|
||||
|
||||
const isModifier = (key: KeyValues): boolean => {
|
||||
return modifierKeySet.has(key);
|
||||
};
|
||||
|
||||
const keyToLabel = (key: KeyValues): string => {
|
||||
return KeyLabels[key] || key;
|
||||
};
|
||||
|
||||
const updateKeybindDisplay = () => {
|
||||
const modifiers = Array.from(activeModifiers);
|
||||
const nonModifiers = keybind.value.filter((key) => !isModifier(key));
|
||||
const sortedModifiers = modifiers.sort();
|
||||
keybind.value = [...sortedModifiers, ...nonModifiers];
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
isKeybindInputFocused.value = false;
|
||||
showEmptyKeybindError.value = false;
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
isKeybindInputFocused.value = true;
|
||||
blurredByEscape.value = false;
|
||||
activeModifiers.clear();
|
||||
keybind.value = [];
|
||||
showEmptyKeybindError.value = false;
|
||||
|
||||
const unlistenAll = $keyboard.listen([$keyboard.Key.All], (event: KeyboardEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const key = event.code as KeyValues;
|
||||
|
||||
if (key === KeyValues.Escape) {
|
||||
blurredByEscape.value = true;
|
||||
keybindInput.value?.blur();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isModifier(key)) {
|
||||
activeModifiers.add(key);
|
||||
} else {
|
||||
const nonModifierKey = keybind.value.find(k => !isModifier(k));
|
||||
if (!nonModifierKey || nonModifierKey === key) {
|
||||
keybind.value = Array.from(activeModifiers);
|
||||
if (nonModifierKey !== key) keybind.value.push(key);
|
||||
} else {
|
||||
keybind.value = [ ...Array.from(activeModifiers), key];
|
||||
}
|
||||
}
|
||||
updateKeybindDisplay();
|
||||
showEmptyKeybindError.value = false;
|
||||
}, { prevent: true });
|
||||
listeners.push(unlistenAll);
|
||||
};
|
||||
|
||||
const saveKeybind = async () => {
|
||||
const finalKeybind = keybind.value.filter(k => k);
|
||||
if (finalKeybind.length > 0) {
|
||||
await $settings.saveSetting("keybind", JSON.stringify(finalKeybind));
|
||||
router.push("/");
|
||||
} else {
|
||||
showEmptyKeybindError.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAutostart = async () => {
|
||||
if (autostart.value === true) {
|
||||
await enable();
|
||||
} else {
|
||||
await disable();
|
||||
}
|
||||
await $settings.saveSetting("autostart", autostart.value ? "true" : "false");
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
autostart.value = (await $settings.getSetting("autostart")) === "true";
|
||||
|
||||
const metaOrCtrlKey = $keyboard.currentOS === "macos" ? $keyboard.Key.Meta : $keyboard.Key.Control;
|
||||
listeners.push(
|
||||
$keyboard.listen([metaOrCtrlKey, $keyboard.Key.Enter], saveKeybind, { prevent: true, ignoreIfEditable: true })
|
||||
);
|
||||
|
||||
listeners.push(
|
||||
$keyboard.listen([$keyboard.Key.Escape], () => {
|
||||
if (!isKeybindInputFocused.value && !blurredByEscape.value) {
|
||||
router.push("/");
|
||||
}
|
||||
if(blurredByEscape.value) blurredByEscape.value = false;
|
||||
}, { prevent: true })
|
||||
);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
listeners.forEach(unlisten => unlisten());
|
||||
listeners.length = 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "/styles/settings.scss";
|
||||
</style>
|
||||
|
|
|
@ -1,61 +1,61 @@
|
|||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { HistoryItem } from "~/types/types";
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
return {
|
||||
provide: {
|
||||
history: {
|
||||
async getHistory(): Promise<HistoryItem[]> {
|
||||
return await invoke<HistoryItem[]>("get_history");
|
||||
},
|
||||
|
||||
async addHistoryItem(item: HistoryItem): Promise<void> {
|
||||
await invoke<void>("add_history_item", { item });
|
||||
},
|
||||
|
||||
async searchHistory(query: string): Promise<HistoryItem[]> {
|
||||
try {
|
||||
return await invoke<HistoryItem[]>("search_history", { query });
|
||||
} catch (error) {
|
||||
console.error("Error searching history:", error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
async loadHistoryChunk(
|
||||
offset: number,
|
||||
limit: number
|
||||
): Promise<HistoryItem[]> {
|
||||
try {
|
||||
return await invoke<HistoryItem[]>("load_history_chunk", {
|
||||
offset,
|
||||
limit,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error loading history chunk:", error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
async deleteHistoryItem(id: string): Promise<void> {
|
||||
await invoke<void>("delete_history_item", { id });
|
||||
},
|
||||
|
||||
async clearHistory(): Promise<void> {
|
||||
await invoke<void>("clear_history");
|
||||
},
|
||||
|
||||
async writeAndPaste(data: {
|
||||
content: string;
|
||||
contentType: string;
|
||||
}): Promise<void> {
|
||||
await invoke<void>("write_and_paste", data);
|
||||
},
|
||||
|
||||
async readImage(data: { filename: string }): Promise<string> {
|
||||
return await invoke<string>("read_image", data);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { HistoryItem } from "~/types/types";
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
return {
|
||||
provide: {
|
||||
history: {
|
||||
async getHistory(): Promise<HistoryItem[]> {
|
||||
return await invoke<HistoryItem[]>("get_history");
|
||||
},
|
||||
|
||||
async addHistoryItem(item: HistoryItem): Promise<void> {
|
||||
await invoke<void>("add_history_item", { item });
|
||||
},
|
||||
|
||||
async searchHistory(query: string): Promise<HistoryItem[]> {
|
||||
try {
|
||||
return await invoke<HistoryItem[]>("search_history", { query });
|
||||
} catch (error) {
|
||||
console.error("Error searching history:", error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
async loadHistoryChunk(
|
||||
offset: number,
|
||||
limit: number
|
||||
): Promise<HistoryItem[]> {
|
||||
try {
|
||||
return await invoke<HistoryItem[]>("load_history_chunk", {
|
||||
offset,
|
||||
limit,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error loading history chunk:", error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
async deleteHistoryItem(id: string): Promise<void> {
|
||||
await invoke<void>("delete_history_item", { id });
|
||||
},
|
||||
|
||||
async clearHistory(): Promise<void> {
|
||||
await invoke<void>("clear_history");
|
||||
},
|
||||
|
||||
async writeAndPaste(data: {
|
||||
content: string;
|
||||
contentType: string;
|
||||
}): Promise<void> {
|
||||
await invoke<void>("write_and_paste", data);
|
||||
},
|
||||
|
||||
async readImage(data: { filename: string }): Promise<string> {
|
||||
return await invoke<string>("read_image", data);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import { useKeyboard, Key } from "@waradu/keyboard";
|
||||
|
||||
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
const keyboardInstance = useKeyboard();
|
||||
let currentOS = "windows";
|
||||
try {
|
||||
const osName = await Promise.resolve(platform());
|
||||
currentOS = osName.toLowerCase().includes("mac") ? "macos" : "windows";
|
||||
} catch (error) {
|
||||
console.error("Error detecting platform:", error);
|
||||
}
|
||||
|
||||
// Defer initialization until the app is mounted
|
||||
nuxtApp.hook('app:mounted', () => {
|
||||
keyboardInstance.init();
|
||||
});
|
||||
|
||||
nuxtApp.provide('keyboard', {
|
||||
listen: keyboardInstance.listen.bind(keyboardInstance),
|
||||
init: keyboardInstance.init.bind(keyboardInstance),
|
||||
Key,
|
||||
currentOS,
|
||||
// Provide a clear method if users need to manually clear all listeners from the instance
|
||||
clearAll: keyboardInstance.clear ? keyboardInstance.clear.bind(keyboardInstance) : () => { console.warn('@waradu/keyboard instance does not have a clear method'); }
|
||||
});
|
||||
});
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import { useKeyboard, Key } from "@waradu/keyboard";
|
||||
|
||||
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
const keyboardInstance = useKeyboard();
|
||||
let currentOS = "windows";
|
||||
try {
|
||||
const osName = await Promise.resolve(platform());
|
||||
currentOS = osName.toLowerCase().includes("mac") ? "macos" : "windows";
|
||||
} catch (error) {
|
||||
console.error("Error detecting platform:", error);
|
||||
}
|
||||
|
||||
// Defer initialization until the app is mounted
|
||||
nuxtApp.hook('app:mounted', () => {
|
||||
keyboardInstance.init();
|
||||
});
|
||||
|
||||
nuxtApp.provide('keyboard', {
|
||||
listen: keyboardInstance.listen.bind(keyboardInstance),
|
||||
init: keyboardInstance.init.bind(keyboardInstance),
|
||||
Key,
|
||||
currentOS,
|
||||
// Provide a clear method if users need to manually clear all listeners from the instance
|
||||
clearAll: keyboardInstance.clear ? keyboardInstance.clear.bind(keyboardInstance) : () => { console.warn('@waradu/keyboard instance does not have a clear method'); }
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,68 +1,68 @@
|
|||
import { ref, computed } from 'vue';
|
||||
import type { HistoryItem } from '~/types/types';
|
||||
|
||||
interface GroupedHistory {
|
||||
label: string;
|
||||
items: HistoryItem[];
|
||||
}
|
||||
|
||||
const selectedGroupIndex = ref(0);
|
||||
const selectedItemIndex = ref(0);
|
||||
const selectedElement = ref<HTMLElement | null>(null);
|
||||
|
||||
const useSelectedResult = (groupedHistory: Ref<GroupedHistory[]>) => {
|
||||
const selectedItem = computed<HistoryItem | undefined>(() => {
|
||||
const group = groupedHistory.value[selectedGroupIndex.value];
|
||||
return group?.items[selectedItemIndex.value] ?? undefined;
|
||||
});
|
||||
|
||||
const isSelected = (groupIndex: number, itemIndex: number): boolean => {
|
||||
return selectedGroupIndex.value === groupIndex && selectedItemIndex.value === itemIndex;
|
||||
};
|
||||
|
||||
const selectNext = (): void => {
|
||||
const currentGroup = groupedHistory.value[selectedGroupIndex.value];
|
||||
if (selectedItemIndex.value < currentGroup.items.length - 1) {
|
||||
selectedItemIndex.value++;
|
||||
} else if (selectedGroupIndex.value < groupedHistory.value.length - 1) {
|
||||
selectedGroupIndex.value++;
|
||||
selectedItemIndex.value = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const selectPrevious = (): void => {
|
||||
if (selectedItemIndex.value > 0) {
|
||||
selectedItemIndex.value--;
|
||||
} else if (selectedGroupIndex.value > 0) {
|
||||
selectedGroupIndex.value--;
|
||||
selectedItemIndex.value = groupedHistory.value[selectedGroupIndex.value].items.length - 1;
|
||||
}
|
||||
};
|
||||
|
||||
const selectItem = (groupIndex: number, itemIndex: number): void => {
|
||||
selectedGroupIndex.value = groupIndex;
|
||||
selectedItemIndex.value = itemIndex;
|
||||
};
|
||||
|
||||
return {
|
||||
selectedItem,
|
||||
isSelected,
|
||||
selectNext,
|
||||
selectPrevious,
|
||||
selectItem,
|
||||
selectedElement
|
||||
};
|
||||
};
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
return {
|
||||
provide: {
|
||||
selectedResult: {
|
||||
selectedGroupIndex,
|
||||
selectedItemIndex,
|
||||
selectedElement,
|
||||
useSelectedResult
|
||||
}
|
||||
}
|
||||
};
|
||||
import { ref, computed } from 'vue';
|
||||
import type { HistoryItem } from '~/types/types';
|
||||
|
||||
interface GroupedHistory {
|
||||
label: string;
|
||||
items: HistoryItem[];
|
||||
}
|
||||
|
||||
const selectedGroupIndex = ref(0);
|
||||
const selectedItemIndex = ref(0);
|
||||
const selectedElement = ref<HTMLElement | null>(null);
|
||||
|
||||
const useSelectedResult = (groupedHistory: Ref<GroupedHistory[]>) => {
|
||||
const selectedItem = computed<HistoryItem | undefined>(() => {
|
||||
const group = groupedHistory.value[selectedGroupIndex.value];
|
||||
return group?.items[selectedItemIndex.value] ?? undefined;
|
||||
});
|
||||
|
||||
const isSelected = (groupIndex: number, itemIndex: number): boolean => {
|
||||
return selectedGroupIndex.value === groupIndex && selectedItemIndex.value === itemIndex;
|
||||
};
|
||||
|
||||
const selectNext = (): void => {
|
||||
const currentGroup = groupedHistory.value[selectedGroupIndex.value];
|
||||
if (selectedItemIndex.value < currentGroup.items.length - 1) {
|
||||
selectedItemIndex.value++;
|
||||
} else if (selectedGroupIndex.value < groupedHistory.value.length - 1) {
|
||||
selectedGroupIndex.value++;
|
||||
selectedItemIndex.value = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const selectPrevious = (): void => {
|
||||
if (selectedItemIndex.value > 0) {
|
||||
selectedItemIndex.value--;
|
||||
} else if (selectedGroupIndex.value > 0) {
|
||||
selectedGroupIndex.value--;
|
||||
selectedItemIndex.value = groupedHistory.value[selectedGroupIndex.value].items.length - 1;
|
||||
}
|
||||
};
|
||||
|
||||
const selectItem = (groupIndex: number, itemIndex: number): void => {
|
||||
selectedGroupIndex.value = groupIndex;
|
||||
selectedItemIndex.value = itemIndex;
|
||||
};
|
||||
|
||||
return {
|
||||
selectedItem,
|
||||
isSelected,
|
||||
selectNext,
|
||||
selectPrevious,
|
||||
selectItem,
|
||||
selectedElement
|
||||
};
|
||||
};
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
return {
|
||||
provide: {
|
||||
selectedResult: {
|
||||
selectedGroupIndex,
|
||||
selectedItemIndex,
|
||||
selectedElement,
|
||||
useSelectedResult
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,17 +1,17 @@
|
|||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
return {
|
||||
provide: {
|
||||
settings: {
|
||||
async getSetting(key: string): Promise<string> {
|
||||
return await invoke<string>("get_setting", { key });
|
||||
},
|
||||
|
||||
async saveSetting(key: string, value: string): Promise<void> {
|
||||
await invoke<void>("save_setting", { key, value });
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
return {
|
||||
provide: {
|
||||
settings: {
|
||||
async getSetting(key: string): Promise<string> {
|
||||
return await invoke<string>("get_setting", { key });
|
||||
},
|
||||
|
||||
async saveSetting(key: string, value: string): Promise<void> {
|
||||
await invoke<void>("save_setting", { key, value });
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="12" height="8" viewBox="0 0 12 8" fill="none" stroke="none" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Arrow" transform="translate(0 0)">
|
||||
<rect id="Rectangle" width="12" height="8" style="mix-blend-mode:normal;" transform="translate(0 0)" />
|
||||
<path id="Path" d="M3.10928 8.76192C3.10928 8.76192 3.10928 0.952386 3.10928 0.952386C3.10928 0.682544 3.1892 0.456512 3.34903 0.274291C3.50887 0.0920672 3.70643 0.000639439 3.94173 3.33786e-06C4.17706 -0.000631809 4.37489 0.0907969 4.53526 0.274291C4.69566 0.457782 4.77529 0.683815 4.77418 0.952386C4.77418 0.952386 4.77418 8.76192 4.77418 8.76192C4.77418 8.76192 6.60566 7 6.60566 7C6.75827 6.8254 6.95253 6.73811 7.18839 6.73811C7.42426 6.73811 7.61849 6.8254 7.77111 7C7.92373 7.1746 8 7.39683 8 7.66667C8 7.93651 7.92373 8.15874 7.77111 8.33334C7.77111 8.33334 4.52444 11.7143 4.52444 11.7143C4.35797 11.9048 4.16372 12 3.94173 12C3.71975 12 3.52551 11.9048 3.35902 11.7143C3.35902 11.7143 0.228926 8.33334 0.228926 8.33334C0.076309 8.15874 0 7.93652 0 7.66668C0 7.39683 0.076309 7.1746 0.228926 7C0.381543 6.8254 0.575782 6.73811 0.811648 6.73811C1.04751 6.73811 1.24175 6.8254 1.39437 7C1.39437 7 3.10928 8.76192 3.10928 8.76192Z" style="fill:#ADA9A1;fill-rule:evenodd;mix-blend-mode:normal;" transform="matrix(0 1 -1 0 12 0)" />
|
||||
</g>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="12" height="8" viewBox="0 0 12 8" fill="none" stroke="none" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Arrow" transform="translate(0 0)">
|
||||
<rect id="Rectangle" width="12" height="8" style="mix-blend-mode:normal;" transform="translate(0 0)" />
|
||||
<path id="Path" d="M3.10928 8.76192C3.10928 8.76192 3.10928 0.952386 3.10928 0.952386C3.10928 0.682544 3.1892 0.456512 3.34903 0.274291C3.50887 0.0920672 3.70643 0.000639439 3.94173 3.33786e-06C4.17706 -0.000631809 4.37489 0.0907969 4.53526 0.274291C4.69566 0.457782 4.77529 0.683815 4.77418 0.952386C4.77418 0.952386 4.77418 8.76192 4.77418 8.76192C4.77418 8.76192 6.60566 7 6.60566 7C6.75827 6.8254 6.95253 6.73811 7.18839 6.73811C7.42426 6.73811 7.61849 6.8254 7.77111 7C7.92373 7.1746 8 7.39683 8 7.66667C8 7.93651 7.92373 8.15874 7.77111 8.33334C7.77111 8.33334 4.52444 11.7143 4.52444 11.7143C4.35797 11.9048 4.16372 12 3.94173 12C3.71975 12 3.52551 11.9048 3.35902 11.7143C3.35902 11.7143 0.228926 8.33334 0.228926 8.33334C0.076309 8.15874 0 7.93652 0 7.66668C0 7.39683 0.076309 7.1746 0.228926 7C0.381543 6.8254 0.575782 6.73811 0.811648 6.73811C1.04751 6.73811 1.24175 6.8254 1.39437 7C1.39437 7 3.10928 8.76192 3.10928 8.76192Z" style="fill:#ADA9A1;fill-rule:evenodd;mix-blend-mode:normal;" transform="matrix(0 1 -1 0 12 0)" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
8
src-tauri/.gitignore
vendored
8
src-tauri/.gitignore
vendored
|
@ -1,4 +1,4 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
/gen/schemas
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
/gen/schemas
|
||||
|
|
2469
src-tauri/Cargo.lock
generated
2469
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -7,52 +7,52 @@ edition = "2021"
|
|||
rust-version = "1.70"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.6", features = [] }
|
||||
tauri-build = { version = "2.2.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.3.1", features = [
|
||||
tauri = { version = "2.5.1", features = [
|
||||
"macos-private-api",
|
||||
"tray-icon",
|
||||
"image-png",
|
||||
] }
|
||||
tauri-plugin-sql = { version = "2.2.0", features = ["sqlite"] }
|
||||
tauri-plugin-autostart = "2.2.0"
|
||||
tauri-plugin-autostart = "2.3.0"
|
||||
tauri-plugin-os = "2.2.1"
|
||||
tauri-plugin-updater = "2.6.0"
|
||||
tauri-plugin-dialog = "2.2.0"
|
||||
tauri-plugin-fs = "2.2.0"
|
||||
tauri-plugin-updater = "2.7.1"
|
||||
tauri-plugin-dialog = "2.2.2"
|
||||
tauri-plugin-fs = "2.3.0"
|
||||
tauri-plugin-clipboard = "2.1.11"
|
||||
tauri-plugin-prevent-default = "1.2.1"
|
||||
tauri-plugin-global-shortcut = "2.2.0"
|
||||
tauri-plugin-aptabase = { git = "https://github.com/aptabase/tauri-plugin-aptabase", branch = "v2" }
|
||||
sqlx = { version = "0.8.3", features = [
|
||||
tauri-plugin-prevent-default = "2.1.1"
|
||||
tauri-plugin-global-shortcut = "2.2.1"
|
||||
tauri-plugin-aptabase = "1.0.0"
|
||||
sqlx = { version = "0.8.6", features = [
|
||||
"runtime-tokio-native-tls",
|
||||
"sqlite",
|
||||
"chrono",
|
||||
] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
tokio = { version = "1.44.1", features = ["full"] }
|
||||
tokio = { version = "1.45.1", features = ["full"] }
|
||||
serde_json = "1.0.140"
|
||||
rdev = "0.5.3"
|
||||
rand = "0.9.0"
|
||||
rand = "0.9.1"
|
||||
base64 = "0.22.1"
|
||||
image = "0.25.5"
|
||||
reqwest = { version = "0.12.14", features = ["json", "blocking"] }
|
||||
image = "0.25.6"
|
||||
reqwest = { version = "0.12.19", features = ["json", "blocking"] }
|
||||
url = "2.5.4"
|
||||
regex = "1.11.1"
|
||||
sha2 = "0.10.8"
|
||||
sha2 = "0.10.9"
|
||||
lazy_static = "1.5.0"
|
||||
time = "0.3.39"
|
||||
global-hotkey = "0.6.4"
|
||||
chrono = { version = "0.4.40", features = ["serde"] }
|
||||
log = { version = "0.4.26", features = ["std"] }
|
||||
uuid = "1.16.0"
|
||||
time = "0.3.41"
|
||||
global-hotkey = "0.7.0"
|
||||
chrono = { version = "0.4.41", features = ["serde"] }
|
||||
log = { version = "0.4.27", features = ["std"] }
|
||||
uuid = { version = "1.17.0", features = ["v4"]}
|
||||
include_dir = "0.7.4"
|
||||
# hyperpolyglot = { git = "https://github.com/0pandadev/hyperpolyglot" }
|
||||
applications = { git = "https://github.com/HuakunShen/applications-rs", branch = "fix/win-app-detection" }
|
||||
glob = "0.3.2"
|
||||
meta_fetcher = "0.1.1"
|
||||
parking_lot = "0.12.3"
|
||||
parking_lot = "0.12.4"
|
||||
|
||||
[features]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
|
|
|
@ -1,34 +1,34 @@
|
|||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:path:default",
|
||||
"core:event:default",
|
||||
"core:window:default",
|
||||
"core:webview:default",
|
||||
"core:app:default",
|
||||
"core:resources:default",
|
||||
"core:image:default",
|
||||
"core:menu:default",
|
||||
"core:tray:default",
|
||||
"sql:allow-load",
|
||||
"sql:allow-select",
|
||||
"sql:allow-execute",
|
||||
"autostart:allow-enable",
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-is-enabled",
|
||||
"os:allow-os-type",
|
||||
"core:app:allow-app-hide",
|
||||
"core:app:allow-app-show",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-is-focused",
|
||||
"core:window:allow-is-visible",
|
||||
"fs:allow-read"
|
||||
]
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:path:default",
|
||||
"core:event:default",
|
||||
"core:window:default",
|
||||
"core:webview:default",
|
||||
"core:app:default",
|
||||
"core:resources:default",
|
||||
"core:image:default",
|
||||
"core:menu:default",
|
||||
"core:tray:default",
|
||||
"sql:allow-load",
|
||||
"sql:allow-select",
|
||||
"sql:allow-execute",
|
||||
"autostart:allow-enable",
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-is-enabled",
|
||||
"os:allow-os-type",
|
||||
"core:app:allow-app-hide",
|
||||
"core:app:allow-app-show",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-is-focused",
|
||||
"core:window:allow-is-visible",
|
||||
"fs:allow-read"
|
||||
]
|
||||
}
|
|
@ -1,279 +1,279 @@
|
|||
use tauri_plugin_aptabase::EventTracker;
|
||||
use base64::{ engine::general_purpose::STANDARD, Engine };
|
||||
// use hyperpolyglot;
|
||||
use lazy_static::lazy_static;
|
||||
use rdev::{ simulate, EventType, Key };
|
||||
use regex::Regex;
|
||||
use sqlx::SqlitePool;
|
||||
use std::fs;
|
||||
use std::sync::atomic::{ AtomicBool, Ordering };
|
||||
use std::{ thread, time::Duration };
|
||||
use tauri::{ AppHandle, Emitter, Listener, Manager };
|
||||
use tauri_plugin_clipboard::Clipboard;
|
||||
use tokio::runtime::Runtime as TokioRuntime;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::db;
|
||||
use crate::utils::commands::get_app_info;
|
||||
use crate::utils::favicon::fetch_favicon_as_base64;
|
||||
use crate::utils::types::{ ContentType, HistoryItem };
|
||||
|
||||
lazy_static! {
|
||||
static ref IS_PROGRAMMATIC_PASTE: AtomicBool = AtomicBool::new(false);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn write_and_paste(
|
||||
app_handle: AppHandle,
|
||||
content: String,
|
||||
content_type: String
|
||||
) -> Result<(), String> {
|
||||
let clipboard = app_handle.state::<Clipboard>();
|
||||
|
||||
match content_type.as_str() {
|
||||
"text" => clipboard.write_text(content).map_err(|e| e.to_string())?,
|
||||
"link" => clipboard.write_text(content).map_err(|e| e.to_string())?,
|
||||
"color" => clipboard.write_text(content).map_err(|e| e.to_string())?,
|
||||
"image" => {
|
||||
clipboard.write_image_base64(content).map_err(|e| e.to_string())?;
|
||||
}
|
||||
"files" => {
|
||||
clipboard
|
||||
.write_files_uris(
|
||||
content
|
||||
.split(", ")
|
||||
.map(|file| file.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
_ => {
|
||||
return Err("Unsupported content type".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
IS_PROGRAMMATIC_PASTE.store(true, Ordering::SeqCst);
|
||||
|
||||
thread::spawn(|| {
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let modifier_key = Key::MetaLeft;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let modifier_key = Key::ControlLeft;
|
||||
|
||||
let events = vec![
|
||||
EventType::KeyPress(modifier_key),
|
||||
EventType::KeyPress(Key::KeyV),
|
||||
EventType::KeyRelease(Key::KeyV),
|
||||
EventType::KeyRelease(modifier_key)
|
||||
];
|
||||
|
||||
for event in events {
|
||||
if let Err(e) = simulate(&event) {
|
||||
println!("Simulation error: {:?}", e);
|
||||
}
|
||||
thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
});
|
||||
|
||||
tokio::spawn(async {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
IS_PROGRAMMATIC_PASTE.store(false, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"clipboard_paste",
|
||||
Some(serde_json::json!({
|
||||
"content_type": content_type
|
||||
}))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn setup(app: &AppHandle) {
|
||||
let app_handle = app.clone();
|
||||
let runtime = TokioRuntime::new().expect("Failed to create Tokio runtime");
|
||||
|
||||
app_handle.clone().listen("plugin:clipboard://clipboard-monitor/update", move |_event| {
|
||||
let app_handle = app_handle.clone();
|
||||
runtime.block_on(async move {
|
||||
if IS_PROGRAMMATIC_PASTE.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
let clipboard = app_handle.state::<Clipboard>();
|
||||
let available_types = clipboard.available_types().unwrap();
|
||||
|
||||
let (app_name, app_icon) = get_app_info();
|
||||
|
||||
match get_pool(&app_handle).await {
|
||||
Ok(pool) => {
|
||||
if available_types.image {
|
||||
println!("Handling image change");
|
||||
if let Ok(image_data) = clipboard.read_image_base64() {
|
||||
let file_path = save_image_to_file(&app_handle, &image_data).await
|
||||
.map_err(|e| e.to_string())
|
||||
.unwrap_or_else(|e| e);
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Image,
|
||||
file_path,
|
||||
None,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
}
|
||||
} else if available_types.files {
|
||||
println!("Handling files change");
|
||||
if let Ok(files) = clipboard.read_files() {
|
||||
for file in files {
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool.clone(),
|
||||
HistoryItem::new(
|
||||
app_name.clone(),
|
||||
ContentType::File,
|
||||
file,
|
||||
None,
|
||||
app_icon.clone(),
|
||||
None
|
||||
)
|
||||
).await;
|
||||
}
|
||||
}
|
||||
} else if available_types.text {
|
||||
println!("Handling text change");
|
||||
if let Ok(text) = clipboard.read_text() {
|
||||
let text = text.to_string();
|
||||
let url_regex = Regex::new(
|
||||
r"^https?://(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*)$"
|
||||
).unwrap();
|
||||
|
||||
if url_regex.is_match(&text) {
|
||||
if let Ok(url) = Url::parse(&text) {
|
||||
let favicon = match fetch_favicon_as_base64(url).await {
|
||||
Ok(Some(f)) => Some(f),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Link,
|
||||
text,
|
||||
favicon,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
}
|
||||
} else {
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Temporarily disabled code detection
|
||||
/*if let Some(detection) = hyperpolyglot::detect_from_text(&text) {
|
||||
let language = match detection {
|
||||
hyperpolyglot::Detection::Heuristics(lang) => lang.to_string(),
|
||||
_ => detection.language().to_string(),
|
||||
};
|
||||
|
||||
let _ = db::history::add_history_item(
|
||||
pool,
|
||||
HistoryItem::new(app_name, ContentType::Code, text, None, app_icon, Some(language))
|
||||
).await;
|
||||
} else*/ if crate::utils::commands::detect_color(&text) {
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Color,
|
||||
text,
|
||||
None,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
} else {
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Text,
|
||||
text.clone(),
|
||||
None,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Unknown clipboard content type");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to get database pool: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"clipboard_copied",
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"content_type": if available_types.image { "image" }
|
||||
else if available_types.files { "files" }
|
||||
else if available_types.text { "text" }
|
||||
else { "unknown" }
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async fn get_pool(
|
||||
app_handle: &AppHandle
|
||||
) -> Result<tauri::State<'_, SqlitePool>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
Ok(app_handle.state::<SqlitePool>())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn start_monitor(app_handle: AppHandle) -> Result<(), String> {
|
||||
let clipboard = app_handle.state::<Clipboard>();
|
||||
clipboard.start_monitor(app_handle.clone()).map_err(|e| e.to_string())?;
|
||||
app_handle
|
||||
.emit("plugin:clipboard://clipboard-monitor/status", true)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_image_to_file(
|
||||
app_handle: &AppHandle,
|
||||
base64_data: &str
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let app_data_dir = app_handle.path().app_data_dir().unwrap();
|
||||
let images_dir = app_data_dir.join("images");
|
||||
fs::create_dir_all(&images_dir)?;
|
||||
|
||||
let file_name = format!("{}.png", Uuid::new_v4());
|
||||
let file_path = images_dir.join(&file_name);
|
||||
|
||||
let bytes = STANDARD.decode(base64_data)?;
|
||||
fs::write(&file_path, bytes)?;
|
||||
|
||||
Ok(file_path.to_string_lossy().into_owned())
|
||||
}
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
use base64::{ engine::general_purpose::STANDARD, Engine };
|
||||
// use hyperpolyglot;
|
||||
use lazy_static::lazy_static;
|
||||
use rdev::{ simulate, EventType, Key };
|
||||
use regex::Regex;
|
||||
use sqlx::SqlitePool;
|
||||
use std::fs;
|
||||
use std::sync::atomic::{ AtomicBool, Ordering };
|
||||
use std::{ thread, time::Duration };
|
||||
use tauri::{ AppHandle, Emitter, Listener, Manager };
|
||||
use tauri_plugin_clipboard::Clipboard;
|
||||
use tokio::runtime::Runtime as TokioRuntime;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::db;
|
||||
use crate::utils::commands::get_app_info;
|
||||
use crate::utils::favicon::fetch_favicon_as_base64;
|
||||
use crate::utils::types::{ ContentType, HistoryItem };
|
||||
|
||||
lazy_static! {
|
||||
static ref IS_PROGRAMMATIC_PASTE: AtomicBool = AtomicBool::new(false);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn write_and_paste(
|
||||
app_handle: AppHandle,
|
||||
content: String,
|
||||
content_type: String
|
||||
) -> Result<(), String> {
|
||||
let clipboard = app_handle.state::<Clipboard>();
|
||||
|
||||
match content_type.as_str() {
|
||||
"text" => clipboard.write_text(content).map_err(|e| e.to_string())?,
|
||||
"link" => clipboard.write_text(content).map_err(|e| e.to_string())?,
|
||||
"color" => clipboard.write_text(content).map_err(|e| e.to_string())?,
|
||||
"image" => {
|
||||
clipboard.write_image_base64(content).map_err(|e| e.to_string())?;
|
||||
}
|
||||
"files" => {
|
||||
clipboard
|
||||
.write_files_uris(
|
||||
content
|
||||
.split(", ")
|
||||
.map(|file| file.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
_ => {
|
||||
return Err("Unsupported content type".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
IS_PROGRAMMATIC_PASTE.store(true, Ordering::SeqCst);
|
||||
|
||||
thread::spawn(|| {
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let modifier_key = Key::MetaLeft;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let modifier_key = Key::ControlLeft;
|
||||
|
||||
let events = vec![
|
||||
EventType::KeyPress(modifier_key),
|
||||
EventType::KeyPress(Key::KeyV),
|
||||
EventType::KeyRelease(Key::KeyV),
|
||||
EventType::KeyRelease(modifier_key)
|
||||
];
|
||||
|
||||
for event in events {
|
||||
if let Err(e) = simulate(&event) {
|
||||
println!("Simulation error: {:?}", e);
|
||||
}
|
||||
thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
});
|
||||
|
||||
tokio::spawn(async {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
IS_PROGRAMMATIC_PASTE.store(false, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"clipboard_paste",
|
||||
Some(serde_json::json!({
|
||||
"content_type": content_type
|
||||
}))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn setup(app: &AppHandle) {
|
||||
let app_handle = app.clone();
|
||||
let runtime = TokioRuntime::new().expect("Failed to create Tokio runtime");
|
||||
|
||||
app_handle.clone().listen("plugin:clipboard://clipboard-monitor/update", move |_event| {
|
||||
let app_handle = app_handle.clone();
|
||||
runtime.block_on(async move {
|
||||
if IS_PROGRAMMATIC_PASTE.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
let clipboard = app_handle.state::<Clipboard>();
|
||||
let available_types = clipboard.available_types().unwrap();
|
||||
|
||||
let (app_name, app_icon) = get_app_info();
|
||||
|
||||
match get_pool(&app_handle).await {
|
||||
Ok(pool) => {
|
||||
if available_types.image {
|
||||
println!("Handling image change");
|
||||
if let Ok(image_data) = clipboard.read_image_base64() {
|
||||
let file_path = save_image_to_file(&app_handle, &image_data).await
|
||||
.map_err(|e| e.to_string())
|
||||
.unwrap_or_else(|e| e);
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Image,
|
||||
file_path,
|
||||
None,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
}
|
||||
} else if available_types.files {
|
||||
println!("Handling files change");
|
||||
if let Ok(files) = clipboard.read_files() {
|
||||
for file in files {
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool.clone(),
|
||||
HistoryItem::new(
|
||||
app_name.clone(),
|
||||
ContentType::File,
|
||||
file,
|
||||
None,
|
||||
app_icon.clone(),
|
||||
None
|
||||
)
|
||||
).await;
|
||||
}
|
||||
}
|
||||
} else if available_types.text {
|
||||
println!("Handling text change");
|
||||
if let Ok(text) = clipboard.read_text() {
|
||||
let text = text.to_string();
|
||||
let url_regex = Regex::new(
|
||||
r"^https?://(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*)$"
|
||||
).unwrap();
|
||||
|
||||
if url_regex.is_match(&text) {
|
||||
if let Ok(url) = Url::parse(&text) {
|
||||
let favicon = match fetch_favicon_as_base64(url).await {
|
||||
Ok(Some(f)) => Some(f),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Link,
|
||||
text,
|
||||
favicon,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
}
|
||||
} else {
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Temporarily disabled code detection
|
||||
/*if let Some(detection) = hyperpolyglot::detect_from_text(&text) {
|
||||
let language = match detection {
|
||||
hyperpolyglot::Detection::Heuristics(lang) => lang.to_string(),
|
||||
_ => detection.language().to_string(),
|
||||
};
|
||||
|
||||
let _ = db::history::add_history_item(
|
||||
pool,
|
||||
HistoryItem::new(app_name, ContentType::Code, text, None, app_icon, Some(language))
|
||||
).await;
|
||||
} else*/ if crate::utils::commands::detect_color(&text) {
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Color,
|
||||
text,
|
||||
None,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
} else {
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Text,
|
||||
text.clone(),
|
||||
None,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Unknown clipboard content type");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to get database pool: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"clipboard_copied",
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"content_type": if available_types.image { "image" }
|
||||
else if available_types.files { "files" }
|
||||
else if available_types.text { "text" }
|
||||
else { "unknown" }
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async fn get_pool(
|
||||
app_handle: &AppHandle
|
||||
) -> Result<tauri::State<'_, SqlitePool>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
Ok(app_handle.state::<SqlitePool>())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn start_monitor(app_handle: AppHandle) -> Result<(), String> {
|
||||
let clipboard = app_handle.state::<Clipboard>();
|
||||
clipboard.start_monitor(app_handle.clone()).map_err(|e| e.to_string())?;
|
||||
app_handle
|
||||
.emit("plugin:clipboard://clipboard-monitor/status", true)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_image_to_file(
|
||||
app_handle: &AppHandle,
|
||||
base64_data: &str
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let app_data_dir = app_handle.path().app_data_dir().unwrap();
|
||||
let images_dir = app_data_dir.join("images");
|
||||
fs::create_dir_all(&images_dir)?;
|
||||
|
||||
let file_name = format!("{}.png", Uuid::new_v4());
|
||||
let file_path = images_dir.join(&file_name);
|
||||
|
||||
let bytes = STANDARD.decode(base64_data)?;
|
||||
fs::write(&file_path, bytes)?;
|
||||
|
||||
Ok(file_path.to_string_lossy().into_owned())
|
||||
}
|
||||
|
|
|
@ -1,155 +1,155 @@
|
|||
use crate::utils::commands::center_window_on_current_monitor;
|
||||
use crate::utils::keys::KeyCode;
|
||||
use global_hotkey::{
|
||||
hotkey::{ Code, HotKey, Modifiers },
|
||||
GlobalHotKeyEvent,
|
||||
GlobalHotKeyManager,
|
||||
HotKeyState,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tauri::{ AppHandle, Manager, Listener };
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
|
||||
#[derive(Default)]
|
||||
struct HotkeyState {
|
||||
manager: Option<GlobalHotKeyManager>,
|
||||
registered_hotkey: Option<HotKey>,
|
||||
}
|
||||
|
||||
unsafe impl Send for HotkeyState {}
|
||||
|
||||
pub fn setup(app_handle: tauri::AppHandle) {
|
||||
let state = Arc::new(Mutex::new(HotkeyState::default()));
|
||||
let manager = match GlobalHotKeyManager::new() {
|
||||
Ok(manager) => manager,
|
||||
Err(err) => {
|
||||
eprintln!("Failed to initialize hotkey manager: {:?}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
let mut hotkey_state = state.lock();
|
||||
hotkey_state.manager = Some(manager);
|
||||
}
|
||||
|
||||
let rt = app_handle.state::<tokio::runtime::Runtime>();
|
||||
let initial_keybind = rt
|
||||
.block_on(crate::db::settings::get_keybind(app_handle.clone()))
|
||||
.expect("Failed to get initial keybind");
|
||||
|
||||
if let Err(e) = register_shortcut(&state, &initial_keybind) {
|
||||
eprintln!("Error registering initial shortcut: {:?}", e);
|
||||
}
|
||||
|
||||
let state_clone = Arc::clone(&state);
|
||||
app_handle.listen("update-shortcut", move |event| {
|
||||
let payload_str = event.payload().replace("\\\"", "\"");
|
||||
let trimmed_str = payload_str.trim_matches('"');
|
||||
unregister_current_hotkey(&state_clone);
|
||||
|
||||
let payload: Vec<String> = serde_json::from_str(trimmed_str).unwrap_or_default();
|
||||
if let Err(e) = register_shortcut(&state_clone, &payload) {
|
||||
eprintln!("Error re-registering shortcut: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let state_clone = Arc::clone(&state);
|
||||
app_handle.listen("save_keybind", move |event| {
|
||||
let payload_str = event.payload().to_string();
|
||||
unregister_current_hotkey(&state_clone);
|
||||
|
||||
let payload: Vec<String> = serde_json::from_str(&payload_str).unwrap_or_default();
|
||||
if let Err(e) = register_shortcut(&state_clone, &payload) {
|
||||
eprintln!("Error registering saved shortcut: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
setup_hotkey_receiver(app_handle);
|
||||
}
|
||||
|
||||
fn setup_hotkey_receiver(app_handle: AppHandle) {
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
match GlobalHotKeyEvent::receiver().recv() {
|
||||
Ok(event) => {
|
||||
if event.state == HotKeyState::Released {
|
||||
continue;
|
||||
}
|
||||
handle_hotkey_event(&app_handle);
|
||||
}
|
||||
Err(e) => eprintln!("Error receiving hotkey event: {:?}", e),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn unregister_current_hotkey(state: &Arc<Mutex<HotkeyState>>) {
|
||||
let mut hotkey_state = state.lock();
|
||||
if let Some(old_hotkey) = hotkey_state.registered_hotkey.take() {
|
||||
if let Some(manager) = &hotkey_state.manager {
|
||||
let _ = manager.unregister(old_hotkey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn register_shortcut(state: &Arc<Mutex<HotkeyState>>, shortcut: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let hotkey = parse_hotkey(shortcut)?;
|
||||
let mut hotkey_state = state.lock();
|
||||
|
||||
if let Some(manager) = &hotkey_state.manager {
|
||||
manager.register(hotkey.clone())?;
|
||||
hotkey_state.registered_hotkey = Some(hotkey);
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Hotkey manager not initialized".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hotkey(shortcut: &[String]) -> Result<HotKey, Box<dyn std::error::Error>> {
|
||||
let mut modifiers = Modifiers::empty();
|
||||
let mut code = None;
|
||||
|
||||
for part in shortcut {
|
||||
match part.as_str() {
|
||||
"ControlLeft" => modifiers |= Modifiers::CONTROL,
|
||||
"AltLeft" => modifiers |= Modifiers::ALT,
|
||||
"ShiftLeft" => modifiers |= Modifiers::SHIFT,
|
||||
"MetaLeft" => modifiers |= Modifiers::META,
|
||||
key => code = Some(Code::from(KeyCode::from_str(key)?)),
|
||||
}
|
||||
}
|
||||
|
||||
let key_code = code.ok_or_else(|| "No valid key code found".to_string())?;
|
||||
Ok(HotKey::new(Some(modifiers), key_code))
|
||||
}
|
||||
|
||||
fn handle_hotkey_event(app_handle: &AppHandle) {
|
||||
let window = app_handle.get_webview_window("main").unwrap();
|
||||
if window.is_visible().unwrap() {
|
||||
window.hide().unwrap();
|
||||
} else {
|
||||
window.set_always_on_top(true).unwrap();
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
|
||||
let window_clone = window.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
window_clone.set_always_on_top(false).unwrap();
|
||||
});
|
||||
|
||||
center_window_on_current_monitor(&window);
|
||||
}
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"hotkey_triggered",
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"action": if window.is_visible().unwrap() { "hide" } else { "show" }
|
||||
})
|
||||
)
|
||||
);
|
||||
use crate::utils::commands::center_window_on_current_monitor;
|
||||
use crate::utils::keys::KeyCode;
|
||||
use global_hotkey::{
|
||||
hotkey::{ Code, HotKey, Modifiers },
|
||||
GlobalHotKeyEvent,
|
||||
GlobalHotKeyManager,
|
||||
HotKeyState,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tauri::{ AppHandle, Manager, Listener };
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
|
||||
#[derive(Default)]
|
||||
struct HotkeyState {
|
||||
manager: Option<GlobalHotKeyManager>,
|
||||
registered_hotkey: Option<HotKey>,
|
||||
}
|
||||
|
||||
unsafe impl Send for HotkeyState {}
|
||||
|
||||
pub fn setup(app_handle: tauri::AppHandle) {
|
||||
let state = Arc::new(Mutex::new(HotkeyState::default()));
|
||||
let manager = match GlobalHotKeyManager::new() {
|
||||
Ok(manager) => manager,
|
||||
Err(err) => {
|
||||
eprintln!("Failed to initialize hotkey manager: {:?}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
let mut hotkey_state = state.lock();
|
||||
hotkey_state.manager = Some(manager);
|
||||
}
|
||||
|
||||
let rt = app_handle.state::<tokio::runtime::Runtime>();
|
||||
let initial_keybind = rt
|
||||
.block_on(crate::db::settings::get_keybind(app_handle.clone()))
|
||||
.expect("Failed to get initial keybind");
|
||||
|
||||
if let Err(e) = register_shortcut(&state, &initial_keybind) {
|
||||
eprintln!("Error registering initial shortcut: {:?}", e);
|
||||
}
|
||||
|
||||
let state_clone = Arc::clone(&state);
|
||||
app_handle.listen("update-shortcut", move |event| {
|
||||
let payload_str = event.payload().replace("\\\"", "\"");
|
||||
let trimmed_str = payload_str.trim_matches('"');
|
||||
unregister_current_hotkey(&state_clone);
|
||||
|
||||
let payload: Vec<String> = serde_json::from_str(trimmed_str).unwrap_or_default();
|
||||
if let Err(e) = register_shortcut(&state_clone, &payload) {
|
||||
eprintln!("Error re-registering shortcut: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let state_clone = Arc::clone(&state);
|
||||
app_handle.listen("save_keybind", move |event| {
|
||||
let payload_str = event.payload().to_string();
|
||||
unregister_current_hotkey(&state_clone);
|
||||
|
||||
let payload: Vec<String> = serde_json::from_str(&payload_str).unwrap_or_default();
|
||||
if let Err(e) = register_shortcut(&state_clone, &payload) {
|
||||
eprintln!("Error registering saved shortcut: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
setup_hotkey_receiver(app_handle);
|
||||
}
|
||||
|
||||
fn setup_hotkey_receiver(app_handle: AppHandle) {
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
match GlobalHotKeyEvent::receiver().recv() {
|
||||
Ok(event) => {
|
||||
if event.state == HotKeyState::Released {
|
||||
continue;
|
||||
}
|
||||
handle_hotkey_event(&app_handle);
|
||||
}
|
||||
Err(e) => eprintln!("Error receiving hotkey event: {:?}", e),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn unregister_current_hotkey(state: &Arc<Mutex<HotkeyState>>) {
|
||||
let mut hotkey_state = state.lock();
|
||||
if let Some(old_hotkey) = hotkey_state.registered_hotkey.take() {
|
||||
if let Some(manager) = &hotkey_state.manager {
|
||||
let _ = manager.unregister(old_hotkey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn register_shortcut(state: &Arc<Mutex<HotkeyState>>, shortcut: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let hotkey = parse_hotkey(shortcut)?;
|
||||
let mut hotkey_state = state.lock();
|
||||
|
||||
if let Some(manager) = &hotkey_state.manager {
|
||||
manager.register(hotkey.clone())?;
|
||||
hotkey_state.registered_hotkey = Some(hotkey);
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Hotkey manager not initialized".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hotkey(shortcut: &[String]) -> Result<HotKey, Box<dyn std::error::Error>> {
|
||||
let mut modifiers = Modifiers::empty();
|
||||
let mut code = None;
|
||||
|
||||
for part in shortcut {
|
||||
match part.as_str() {
|
||||
"ControlLeft" => modifiers |= Modifiers::CONTROL,
|
||||
"AltLeft" => modifiers |= Modifiers::ALT,
|
||||
"ShiftLeft" => modifiers |= Modifiers::SHIFT,
|
||||
"MetaLeft" => modifiers |= Modifiers::META,
|
||||
key => code = Some(Code::from(KeyCode::from_str(key)?)),
|
||||
}
|
||||
}
|
||||
|
||||
let key_code = code.ok_or_else(|| "No valid key code found".to_string())?;
|
||||
Ok(HotKey::new(Some(modifiers), key_code))
|
||||
}
|
||||
|
||||
fn handle_hotkey_event(app_handle: &AppHandle) {
|
||||
let window = app_handle.get_webview_window("main").unwrap();
|
||||
if window.is_visible().unwrap() {
|
||||
window.hide().unwrap();
|
||||
} else {
|
||||
window.set_always_on_top(true).unwrap();
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
|
||||
let window_clone = window.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
window_clone.set_always_on_top(false).unwrap();
|
||||
});
|
||||
|
||||
center_window_on_current_monitor(&window);
|
||||
}
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"hotkey_triggered",
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"action": if window.is_visible().unwrap() { "hide" } else { "show" }
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
pub mod clipboard;
|
||||
pub mod hotkeys;
|
||||
pub mod tray;
|
||||
pub mod updater;
|
||||
pub mod clipboard;
|
||||
pub mod hotkeys;
|
||||
pub mod tray;
|
||||
pub mod updater;
|
||||
|
|
|
@ -1,61 +1,61 @@
|
|||
use tauri::{ menu::{ MenuBuilder, MenuItemBuilder }, tray::TrayIconBuilder, Emitter, Manager };
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
|
||||
pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
let is_visible = window.is_visible().unwrap();
|
||||
let _ = app.track_event(
|
||||
"tray_toggle",
|
||||
Some(serde_json::json!({
|
||||
"action": if is_visible { "hide" } else { "show" }
|
||||
}))
|
||||
);
|
||||
|
||||
let icon_bytes = include_bytes!("../../icons/Square71x71Logo.png");
|
||||
let icon = tauri::image::Image::from_bytes(icon_bytes).unwrap();
|
||||
|
||||
let _tray = TrayIconBuilder::new()
|
||||
.menu(
|
||||
&MenuBuilder::new(app)
|
||||
.items(&[&MenuItemBuilder::with_id("app_name", "Qopy").enabled(false).build(app)?])
|
||||
.items(&[&MenuItemBuilder::with_id("show", "Show/Hide").build(app)?])
|
||||
.items(&[&MenuItemBuilder::with_id("settings", "Settings").build(app)?])
|
||||
.items(&[&MenuItemBuilder::with_id("quit", "Quit").build(app)?])
|
||||
.build()?
|
||||
)
|
||||
.on_menu_event(move |_app, event| {
|
||||
match event.id().as_ref() {
|
||||
"quit" => {
|
||||
let _ = _app.track_event("app_quit", None);
|
||||
std::process::exit(0);
|
||||
}
|
||||
"show" => {
|
||||
let _ = _app.track_event(
|
||||
"tray_toggle",
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"action": if is_visible { "hide" } else { "show" }
|
||||
})
|
||||
)
|
||||
);
|
||||
let is_visible = window.is_visible().unwrap();
|
||||
if is_visible {
|
||||
window.hide().unwrap();
|
||||
} else {
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
}
|
||||
window.emit("main_route", ()).unwrap();
|
||||
}
|
||||
"settings" => {
|
||||
let _ = _app.track_event("tray_settings", None);
|
||||
window.emit("settings", ()).unwrap();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
})
|
||||
.icon(icon)
|
||||
.build(app)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
use tauri::{ menu::{ MenuBuilder, MenuItemBuilder }, tray::TrayIconBuilder, Emitter, Manager };
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
|
||||
pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
let is_visible = window.is_visible().unwrap();
|
||||
let _ = app.track_event(
|
||||
"tray_toggle",
|
||||
Some(serde_json::json!({
|
||||
"action": if is_visible { "hide" } else { "show" }
|
||||
}))
|
||||
);
|
||||
|
||||
let icon_bytes = include_bytes!("../../icons/Square71x71Logo.png");
|
||||
let icon = tauri::image::Image::from_bytes(icon_bytes).unwrap();
|
||||
|
||||
let _tray = TrayIconBuilder::new()
|
||||
.menu(
|
||||
&MenuBuilder::new(app)
|
||||
.items(&[&MenuItemBuilder::with_id("app_name", "Qopy").enabled(false).build(app)?])
|
||||
.items(&[&MenuItemBuilder::with_id("show", "Show/Hide").build(app)?])
|
||||
.items(&[&MenuItemBuilder::with_id("settings", "Settings").build(app)?])
|
||||
.items(&[&MenuItemBuilder::with_id("quit", "Quit").build(app)?])
|
||||
.build()?
|
||||
)
|
||||
.on_menu_event(move |_app, event| {
|
||||
match event.id().as_ref() {
|
||||
"quit" => {
|
||||
let _ = _app.track_event("app_quit", None);
|
||||
std::process::exit(0);
|
||||
}
|
||||
"show" => {
|
||||
let _ = _app.track_event(
|
||||
"tray_toggle",
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"action": if is_visible { "hide" } else { "show" }
|
||||
})
|
||||
)
|
||||
);
|
||||
let is_visible = window.is_visible().unwrap();
|
||||
if is_visible {
|
||||
window.hide().unwrap();
|
||||
} else {
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
}
|
||||
window.emit("main_route", ()).unwrap();
|
||||
}
|
||||
"settings" => {
|
||||
let _ = _app.track_event("tray_settings", None);
|
||||
window.emit("settings", ()).unwrap();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
})
|
||||
.icon(icon)
|
||||
.build(app)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,94 +1,94 @@
|
|||
use tauri::{ async_runtime, AppHandle, Manager };
|
||||
use tauri_plugin_dialog::{ DialogExt, MessageDialogButtons, MessageDialogKind };
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
pub async fn check_for_updates(app: AppHandle, prompted: bool) {
|
||||
println!("Checking for updates...");
|
||||
|
||||
let updater = app.updater().unwrap();
|
||||
let response = updater.check().await;
|
||||
|
||||
match response {
|
||||
Ok(Some(update)) => {
|
||||
let cur_ver = &update.current_version;
|
||||
let new_ver = &update.version;
|
||||
let mut msg = String::new();
|
||||
msg.extend([
|
||||
&format!("{cur_ver} -> {new_ver}\n\n"),
|
||||
"Would you like to install it now?",
|
||||
]);
|
||||
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
|
||||
app.dialog()
|
||||
.message(msg)
|
||||
.title("Qopy Update Available")
|
||||
.buttons(
|
||||
MessageDialogButtons::OkCancelCustom(
|
||||
String::from("Install"),
|
||||
String::from("Cancel")
|
||||
)
|
||||
)
|
||||
.show(move |response| {
|
||||
if !response {
|
||||
return;
|
||||
}
|
||||
async_runtime::spawn(async move {
|
||||
match
|
||||
update.download_and_install(
|
||||
|_, _| {},
|
||||
|| {}
|
||||
).await
|
||||
{
|
||||
Ok(_) => {
|
||||
app.dialog()
|
||||
.message(
|
||||
"Update installed successfully. The application needs to restart to apply the changes."
|
||||
)
|
||||
.title("Qopy Update Installed")
|
||||
.buttons(
|
||||
MessageDialogButtons::OkCancelCustom(
|
||||
String::from("Restart"),
|
||||
String::from("Cancel")
|
||||
)
|
||||
)
|
||||
.show(move |response| {
|
||||
if response {
|
||||
app.restart();
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error installing new update: {:?}", e);
|
||||
app.dialog()
|
||||
.message(
|
||||
"Failed to install new update. The new update can be downloaded from Github"
|
||||
)
|
||||
.kind(MessageDialogKind::Error)
|
||||
.show(|_| {});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("No updates available.");
|
||||
}
|
||||
Err(e) => {
|
||||
if prompted {
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
|
||||
app.dialog()
|
||||
.message("No updates available.")
|
||||
.title("Qopy Update Check")
|
||||
.show(|_| {});
|
||||
}
|
||||
|
||||
println!("No updates available. {}", e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
use tauri::{ async_runtime, AppHandle, Manager };
|
||||
use tauri_plugin_dialog::{ DialogExt, MessageDialogButtons, MessageDialogKind };
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
pub async fn check_for_updates(app: AppHandle, prompted: bool) {
|
||||
println!("Checking for updates...");
|
||||
|
||||
let updater = app.updater().unwrap();
|
||||
let response = updater.check().await;
|
||||
|
||||
match response {
|
||||
Ok(Some(update)) => {
|
||||
let cur_ver = &update.current_version;
|
||||
let new_ver = &update.version;
|
||||
let mut msg = String::new();
|
||||
msg.extend([
|
||||
&format!("{cur_ver} -> {new_ver}\n\n"),
|
||||
"Would you like to install it now?",
|
||||
]);
|
||||
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
|
||||
app.dialog()
|
||||
.message(msg)
|
||||
.title("Qopy Update Available")
|
||||
.buttons(
|
||||
MessageDialogButtons::OkCancelCustom(
|
||||
String::from("Install"),
|
||||
String::from("Cancel")
|
||||
)
|
||||
)
|
||||
.show(move |response| {
|
||||
if !response {
|
||||
return;
|
||||
}
|
||||
async_runtime::spawn(async move {
|
||||
match
|
||||
update.download_and_install(
|
||||
|_, _| {},
|
||||
|| {}
|
||||
).await
|
||||
{
|
||||
Ok(_) => {
|
||||
app.dialog()
|
||||
.message(
|
||||
"Update installed successfully. The application needs to restart to apply the changes."
|
||||
)
|
||||
.title("Qopy Update Installed")
|
||||
.buttons(
|
||||
MessageDialogButtons::OkCancelCustom(
|
||||
String::from("Restart"),
|
||||
String::from("Cancel")
|
||||
)
|
||||
)
|
||||
.show(move |response| {
|
||||
if response {
|
||||
app.restart();
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error installing new update: {:?}", e);
|
||||
app.dialog()
|
||||
.message(
|
||||
"Failed to install new update. The new update can be downloaded from Github"
|
||||
)
|
||||
.kind(MessageDialogKind::Error)
|
||||
.show(|_| {});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("No updates available.");
|
||||
}
|
||||
Err(e) => {
|
||||
if prompted {
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
|
||||
app.dialog()
|
||||
.message("No updates available.")
|
||||
.title("Qopy Update Check")
|
||||
.show(|_| {});
|
||||
}
|
||||
|
||||
println!("No updates available. {}", e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,107 +1,107 @@
|
|||
use include_dir::{ include_dir, Dir };
|
||||
use sqlx::sqlite::{ SqlitePool, SqlitePoolOptions };
|
||||
use std::fs;
|
||||
use tauri::Manager;
|
||||
use tokio::runtime::Runtime as TokioRuntime;
|
||||
|
||||
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/db/migrations");
|
||||
|
||||
pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let rt = TokioRuntime::new().expect("Failed to create Tokio runtime");
|
||||
app.manage(rt);
|
||||
|
||||
let rt = app.state::<TokioRuntime>();
|
||||
|
||||
let app_data_dir = app.path().app_data_dir().unwrap();
|
||||
fs::create_dir_all(&app_data_dir).expect("Failed to create app data directory");
|
||||
|
||||
let db_path = app_data_dir.join("data.db");
|
||||
let is_new_db = !db_path.exists();
|
||||
if is_new_db {
|
||||
fs::File::create(&db_path).expect("Failed to create database file");
|
||||
}
|
||||
|
||||
let db_url = format!("sqlite:{}", db_path.to_str().unwrap());
|
||||
let pool = rt.block_on(async {
|
||||
SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&db_url).await
|
||||
.expect("Failed to create pool")
|
||||
});
|
||||
|
||||
app.manage(pool.clone());
|
||||
|
||||
rt.block_on(async {
|
||||
apply_migrations(&pool).await?;
|
||||
if is_new_db {
|
||||
if let Err(e) = super::history::initialize_history(&pool).await {
|
||||
eprintln!("Failed to initialize history: {}", e);
|
||||
}
|
||||
if let Err(e) = super::settings::initialize_settings(&pool).await {
|
||||
eprintln!("Failed to initialize settings: {}", e);
|
||||
}
|
||||
}
|
||||
Ok::<(), Box<dyn std::error::Error>>(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
sqlx
|
||||
::query(
|
||||
"CREATE TABLE IF NOT EXISTS schema_version (
|
||||
version INTEGER PRIMARY KEY,
|
||||
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);"
|
||||
)
|
||||
.execute(pool).await?;
|
||||
|
||||
let current_version: Option<i64> = sqlx
|
||||
::query_scalar("SELECT MAX(version) FROM schema_version")
|
||||
.fetch_one(pool).await?;
|
||||
|
||||
let current_version = current_version.unwrap_or(0);
|
||||
|
||||
let mut migration_files: Vec<(i64, &str)> = MIGRATIONS_DIR.files()
|
||||
.filter_map(|file| {
|
||||
let file_name = file.path().file_name()?.to_str()?;
|
||||
if file_name.ends_with(".sql") && file_name.starts_with("v") {
|
||||
let version: i64 = file_name
|
||||
.trim_start_matches("v")
|
||||
.trim_end_matches(".sql")
|
||||
.parse()
|
||||
.ok()?;
|
||||
Some((version, file.contents_utf8()?))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
migration_files.sort_by_key(|(version, _)| *version);
|
||||
|
||||
for (version, content) in migration_files {
|
||||
if version > current_version {
|
||||
let statements: Vec<&str> = content
|
||||
.split(';')
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
|
||||
for statement in statements {
|
||||
sqlx
|
||||
::query(statement)
|
||||
.execute(pool).await
|
||||
.map_err(|e| format!("Failed to execute migration {}: {}", version, e))?;
|
||||
}
|
||||
|
||||
sqlx
|
||||
::query("INSERT INTO schema_version (version) VALUES (?)")
|
||||
.bind(version)
|
||||
.execute(pool).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
use include_dir::{ include_dir, Dir };
|
||||
use sqlx::sqlite::{ SqlitePool, SqlitePoolOptions };
|
||||
use std::fs;
|
||||
use tauri::Manager;
|
||||
use tokio::runtime::Runtime as TokioRuntime;
|
||||
|
||||
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/db/migrations");
|
||||
|
||||
pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let rt = TokioRuntime::new().expect("Failed to create Tokio runtime");
|
||||
app.manage(rt);
|
||||
|
||||
let rt = app.state::<TokioRuntime>();
|
||||
|
||||
let app_data_dir = app.path().app_data_dir().unwrap();
|
||||
fs::create_dir_all(&app_data_dir).expect("Failed to create app data directory");
|
||||
|
||||
let db_path = app_data_dir.join("data.db");
|
||||
let is_new_db = !db_path.exists();
|
||||
if is_new_db {
|
||||
fs::File::create(&db_path).expect("Failed to create database file");
|
||||
}
|
||||
|
||||
let db_url = format!("sqlite:{}", db_path.to_str().unwrap());
|
||||
let pool = rt.block_on(async {
|
||||
SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&db_url).await
|
||||
.expect("Failed to create pool")
|
||||
});
|
||||
|
||||
app.manage(pool.clone());
|
||||
|
||||
rt.block_on(async {
|
||||
apply_migrations(&pool).await?;
|
||||
if is_new_db {
|
||||
if let Err(e) = super::history::initialize_history(&pool).await {
|
||||
eprintln!("Failed to initialize history: {}", e);
|
||||
}
|
||||
if let Err(e) = super::settings::initialize_settings(&pool).await {
|
||||
eprintln!("Failed to initialize settings: {}", e);
|
||||
}
|
||||
}
|
||||
Ok::<(), Box<dyn std::error::Error>>(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
sqlx
|
||||
::query(
|
||||
"CREATE TABLE IF NOT EXISTS schema_version (
|
||||
version INTEGER PRIMARY KEY,
|
||||
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);"
|
||||
)
|
||||
.execute(pool).await?;
|
||||
|
||||
let current_version: Option<i64> = sqlx
|
||||
::query_scalar("SELECT MAX(version) FROM schema_version")
|
||||
.fetch_one(pool).await?;
|
||||
|
||||
let current_version = current_version.unwrap_or(0);
|
||||
|
||||
let mut migration_files: Vec<(i64, &str)> = MIGRATIONS_DIR.files()
|
||||
.filter_map(|file| {
|
||||
let file_name = file.path().file_name()?.to_str()?;
|
||||
if file_name.ends_with(".sql") && file_name.starts_with("v") {
|
||||
let version: i64 = file_name
|
||||
.trim_start_matches("v")
|
||||
.trim_end_matches(".sql")
|
||||
.parse()
|
||||
.ok()?;
|
||||
Some((version, file.contents_utf8()?))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
migration_files.sort_by_key(|(version, _)| *version);
|
||||
|
||||
for (version, content) in migration_files {
|
||||
if version > current_version {
|
||||
let statements: Vec<&str> = content
|
||||
.split(';')
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
|
||||
for statement in statements {
|
||||
sqlx
|
||||
::query(statement)
|
||||
.execute(pool).await
|
||||
.map_err(|e| format!("Failed to execute migration {}: {}", version, e))?;
|
||||
}
|
||||
|
||||
sqlx
|
||||
::query("INSERT INTO schema_version (version) VALUES (?)")
|
||||
.bind(version)
|
||||
.execute(pool).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,226 +1,226 @@
|
|||
use crate::utils::types::{ ContentType, HistoryItem };
|
||||
use base64::{ engine::general_purpose::STANDARD, Engine };
|
||||
use rand::{ rng, Rng };
|
||||
use rand::distr::Alphanumeric;
|
||||
use sqlx::{ Row, SqlitePool };
|
||||
use std::fs;
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
use tauri::Emitter;
|
||||
|
||||
pub async fn initialize_history(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let id: String = rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(16)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO history (id, source, content_type, content, timestamp) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)"
|
||||
)
|
||||
.bind(id)
|
||||
.bind("System")
|
||||
.bind("text")
|
||||
.bind("Welcome to your clipboard history!")
|
||||
.execute(pool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result<Vec<HistoryItem>, String> {
|
||||
let rows = sqlx
|
||||
::query(
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC"
|
||||
)
|
||||
.fetch_all(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(|row| HistoryItem {
|
||||
id: row.get("id"),
|
||||
source: row.get("source"),
|
||||
source_icon: row.get("source_icon"),
|
||||
content_type: ContentType::from(row.get::<String, _>("content_type")),
|
||||
content: row.get("content"),
|
||||
favicon: row.get("favicon"),
|
||||
timestamp: row.get("timestamp"),
|
||||
language: row.get("language"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn add_history_item(
|
||||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
item: HistoryItem
|
||||
) -> Result<(), String> {
|
||||
let (id, source, source_icon, content_type, content, favicon, timestamp, language) =
|
||||
item.to_row();
|
||||
|
||||
let existing = sqlx
|
||||
::query("SELECT id FROM history WHERE content = ? AND content_type = ?")
|
||||
.bind(&content)
|
||||
.bind(&content_type)
|
||||
.fetch_optional(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
match existing {
|
||||
Some(_) => {
|
||||
sqlx
|
||||
::query(
|
||||
"UPDATE history SET source = ?, source_icon = ?, timestamp = strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now'), favicon = ?, language = ? WHERE content = ? AND content_type = ?"
|
||||
)
|
||||
.bind(&source)
|
||||
.bind(&source_icon)
|
||||
.bind(&favicon)
|
||||
.bind(&language)
|
||||
.bind(&content)
|
||||
.bind(&content_type)
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
None => {
|
||||
sqlx
|
||||
::query(
|
||||
"INSERT INTO history (id, source, source_icon, content_type, content, favicon, timestamp, language) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
)
|
||||
.bind(id)
|
||||
.bind(source)
|
||||
.bind(source_icon)
|
||||
.bind(content_type)
|
||||
.bind(content)
|
||||
.bind(favicon)
|
||||
.bind(timestamp)
|
||||
.bind(language)
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"history_item_added",
|
||||
Some(serde_json::json!({
|
||||
"content_type": item.content_type.to_string()
|
||||
}))
|
||||
);
|
||||
|
||||
let _ = app_handle.emit("clipboard-content-updated", ());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn search_history(
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
query: String
|
||||
) -> Result<Vec<HistoryItem>, String> {
|
||||
if query.trim().is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let query = format!("%{}%", query);
|
||||
|
||||
let rows = sqlx
|
||||
::query(
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language
|
||||
FROM history
|
||||
WHERE content LIKE ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 100"
|
||||
)
|
||||
.bind(query)
|
||||
.fetch_all(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut items = Vec::with_capacity(rows.len());
|
||||
for row in rows.iter() {
|
||||
items.push(HistoryItem {
|
||||
id: row.get("id"),
|
||||
source: row.get("source"),
|
||||
source_icon: row.get("source_icon"),
|
||||
content_type: ContentType::from(row.get::<String, _>("content_type")),
|
||||
content: row.get("content"),
|
||||
favicon: row.get("favicon"),
|
||||
timestamp: row.get("timestamp"),
|
||||
language: row.get("language"),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_history_chunk(
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
offset: i64,
|
||||
limit: i64
|
||||
) -> Result<Vec<HistoryItem>, String> {
|
||||
let rows = sqlx
|
||||
::query(
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC LIMIT ? OFFSET ?"
|
||||
)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(|row| HistoryItem {
|
||||
id: row.get("id"),
|
||||
source: row.get("source"),
|
||||
source_icon: row.get("source_icon"),
|
||||
content_type: ContentType::from(row.get::<String, _>("content_type")),
|
||||
content: row.get("content"),
|
||||
favicon: row.get("favicon"),
|
||||
timestamp: row.get("timestamp"),
|
||||
language: row.get("language"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_history_item(
|
||||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
id: String
|
||||
) -> Result<(), String> {
|
||||
sqlx
|
||||
::query("DELETE FROM history WHERE id = ?")
|
||||
.bind(id)
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let _ = app_handle.track_event("history_item_deleted", None);
|
||||
let _ = app_handle.emit("clipboard-content-updated", ());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn clear_history(
|
||||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>
|
||||
) -> Result<(), String> {
|
||||
sqlx
|
||||
::query("DELETE FROM history")
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let _ = app_handle.track_event("history_cleared", None);
|
||||
let _ = app_handle.emit("clipboard-content-updated", ());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn read_image(filename: String) -> Result<String, String> {
|
||||
let bytes = fs::read(filename).map_err(|e| e.to_string())?;
|
||||
Ok(STANDARD.encode(bytes))
|
||||
}
|
||||
use crate::utils::types::{ ContentType, HistoryItem };
|
||||
use base64::{ engine::general_purpose::STANDARD, Engine };
|
||||
use rand::{ rng, Rng };
|
||||
use rand::distr::Alphanumeric;
|
||||
use sqlx::{ Row, SqlitePool };
|
||||
use std::fs;
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
use tauri::Emitter;
|
||||
|
||||
pub async fn initialize_history(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let id: String = rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(16)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO history (id, source, content_type, content, timestamp) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)"
|
||||
)
|
||||
.bind(id)
|
||||
.bind("System")
|
||||
.bind("text")
|
||||
.bind("Welcome to your clipboard history!")
|
||||
.execute(pool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result<Vec<HistoryItem>, String> {
|
||||
let rows = sqlx
|
||||
::query(
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC"
|
||||
)
|
||||
.fetch_all(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(|row| HistoryItem {
|
||||
id: row.get("id"),
|
||||
source: row.get("source"),
|
||||
source_icon: row.get("source_icon"),
|
||||
content_type: ContentType::from(row.get::<String, _>("content_type")),
|
||||
content: row.get("content"),
|
||||
favicon: row.get("favicon"),
|
||||
timestamp: row.get("timestamp"),
|
||||
language: row.get("language"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn add_history_item(
|
||||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
item: HistoryItem
|
||||
) -> Result<(), String> {
|
||||
let (id, source, source_icon, content_type, content, favicon, timestamp, language) =
|
||||
item.to_row();
|
||||
|
||||
let existing = sqlx
|
||||
::query("SELECT id FROM history WHERE content = ? AND content_type = ?")
|
||||
.bind(&content)
|
||||
.bind(&content_type)
|
||||
.fetch_optional(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
match existing {
|
||||
Some(_) => {
|
||||
sqlx
|
||||
::query(
|
||||
"UPDATE history SET source = ?, source_icon = ?, timestamp = strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now'), favicon = ?, language = ? WHERE content = ? AND content_type = ?"
|
||||
)
|
||||
.bind(&source)
|
||||
.bind(&source_icon)
|
||||
.bind(&favicon)
|
||||
.bind(&language)
|
||||
.bind(&content)
|
||||
.bind(&content_type)
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
None => {
|
||||
sqlx
|
||||
::query(
|
||||
"INSERT INTO history (id, source, source_icon, content_type, content, favicon, timestamp, language) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
)
|
||||
.bind(id)
|
||||
.bind(source)
|
||||
.bind(source_icon)
|
||||
.bind(content_type)
|
||||
.bind(content)
|
||||
.bind(favicon)
|
||||
.bind(timestamp)
|
||||
.bind(language)
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"history_item_added",
|
||||
Some(serde_json::json!({
|
||||
"content_type": item.content_type.to_string()
|
||||
}))
|
||||
);
|
||||
|
||||
let _ = app_handle.emit("clipboard-content-updated", ());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn search_history(
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
query: String
|
||||
) -> Result<Vec<HistoryItem>, String> {
|
||||
if query.trim().is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let query = format!("%{}%", query);
|
||||
|
||||
let rows = sqlx
|
||||
::query(
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language
|
||||
FROM history
|
||||
WHERE content LIKE ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 100"
|
||||
)
|
||||
.bind(query)
|
||||
.fetch_all(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut items = Vec::with_capacity(rows.len());
|
||||
for row in rows.iter() {
|
||||
items.push(HistoryItem {
|
||||
id: row.get("id"),
|
||||
source: row.get("source"),
|
||||
source_icon: row.get("source_icon"),
|
||||
content_type: ContentType::from(row.get::<String, _>("content_type")),
|
||||
content: row.get("content"),
|
||||
favicon: row.get("favicon"),
|
||||
timestamp: row.get("timestamp"),
|
||||
language: row.get("language"),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_history_chunk(
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
offset: i64,
|
||||
limit: i64
|
||||
) -> Result<Vec<HistoryItem>, String> {
|
||||
let rows = sqlx
|
||||
::query(
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC LIMIT ? OFFSET ?"
|
||||
)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(|row| HistoryItem {
|
||||
id: row.get("id"),
|
||||
source: row.get("source"),
|
||||
source_icon: row.get("source_icon"),
|
||||
content_type: ContentType::from(row.get::<String, _>("content_type")),
|
||||
content: row.get("content"),
|
||||
favicon: row.get("favicon"),
|
||||
timestamp: row.get("timestamp"),
|
||||
language: row.get("language"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_history_item(
|
||||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
id: String
|
||||
) -> Result<(), String> {
|
||||
sqlx
|
||||
::query("DELETE FROM history WHERE id = ?")
|
||||
.bind(id)
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let _ = app_handle.track_event("history_item_deleted", None);
|
||||
let _ = app_handle.emit("clipboard-content-updated", ());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn clear_history(
|
||||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>
|
||||
) -> Result<(), String> {
|
||||
sqlx
|
||||
::query("DELETE FROM history")
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let _ = app_handle.track_event("history_cleared", None);
|
||||
let _ = app_handle.emit("clipboard-content-updated", ());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn read_image(filename: String) -> Result<String, String> {
|
||||
let bytes = fs::read(filename).map_err(|e| e.to_string())?;
|
||||
Ok(STANDARD.encode(bytes))
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS history (
|
||||
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
||||
content_type TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
favicon TEXT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS history (
|
||||
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
||||
content_type TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
favicon TEXT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
|
@ -1,3 +1,3 @@
|
|||
ALTER TABLE history ADD COLUMN source TEXT DEFAULT 'System' NOT NULL;
|
||||
ALTER TABLE history ADD COLUMN source_icon TEXT;
|
||||
ALTER TABLE history ADD COLUMN language TEXT;
|
||||
ALTER TABLE history ADD COLUMN source TEXT DEFAULT 'System' NOT NULL;
|
||||
ALTER TABLE history ADD COLUMN source_icon TEXT;
|
||||
ALTER TABLE history ADD COLUMN language TEXT;
|
||||
|
|
|
@ -1 +1 @@
|
|||
INSERT INTO settings (key, value) VALUES ('autostart', 'true');
|
||||
INSERT INTO settings (key, value) VALUES ('autostart', 'true');
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
pub mod database;
|
||||
pub mod history;
|
||||
pub mod settings;
|
||||
pub mod database;
|
||||
pub mod history;
|
||||
pub mod settings;
|
||||
|
|
|
@ -1,87 +1,87 @@
|
|||
use serde::{ Deserialize, Serialize };
|
||||
use serde_json;
|
||||
use sqlx::Row;
|
||||
use sqlx::SqlitePool;
|
||||
use tauri::{ Emitter, Manager };
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct KeybindSetting {
|
||||
keybind: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn initialize_settings(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let default_keybind = KeybindSetting {
|
||||
keybind: vec!["Meta".to_string(), "V".to_string()],
|
||||
};
|
||||
let json = serde_json::to_string(&default_keybind)?;
|
||||
|
||||
sqlx
|
||||
::query("INSERT INTO settings (key, value) VALUES ('keybind', ?)")
|
||||
.bind(json)
|
||||
.execute(pool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_setting(
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
key: String
|
||||
) -> Result<String, String> {
|
||||
let row = sqlx
|
||||
::query("SELECT value FROM settings WHERE key = ?")
|
||||
.bind(key)
|
||||
.fetch_optional(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(row.map(|r| r.get("value")).unwrap_or_default())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_setting(
|
||||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
key: String,
|
||||
value: String
|
||||
) -> Result<(), String> {
|
||||
sqlx
|
||||
::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
|
||||
.bind(key.clone())
|
||||
.bind(value.clone())
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"setting_saved",
|
||||
Some(serde_json::json!({
|
||||
"key": key
|
||||
}))
|
||||
);
|
||||
|
||||
if key == "keybind" {
|
||||
let _ = app_handle.emit("update-shortcut", &value).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_keybind(app_handle: tauri::AppHandle) -> Result<Vec<String>, String> {
|
||||
let pool = app_handle.state::<SqlitePool>();
|
||||
|
||||
let row = sqlx
|
||||
::query("SELECT value FROM settings WHERE key = 'keybind'")
|
||||
.fetch_optional(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let json = row
|
||||
.map(|r| r.get::<String, _>("value"))
|
||||
.unwrap_or_else(|| {
|
||||
serde_json
|
||||
::to_string(&vec!["MetaLeft".to_string(), "KeyV".to_string()])
|
||||
.expect("Failed to serialize default keybind")
|
||||
});
|
||||
|
||||
serde_json::from_str::<Vec<String>>(&json).map_err(|e| e.to_string())
|
||||
}
|
||||
use serde::{ Deserialize, Serialize };
|
||||
use serde_json;
|
||||
use sqlx::Row;
|
||||
use sqlx::SqlitePool;
|
||||
use tauri::{ Emitter, Manager };
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct KeybindSetting {
|
||||
keybind: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn initialize_settings(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let default_keybind = KeybindSetting {
|
||||
keybind: vec!["Meta".to_string(), "V".to_string()],
|
||||
};
|
||||
let json = serde_json::to_string(&default_keybind)?;
|
||||
|
||||
sqlx
|
||||
::query("INSERT INTO settings (key, value) VALUES ('keybind', ?)")
|
||||
.bind(json)
|
||||
.execute(pool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_setting(
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
key: String
|
||||
) -> Result<String, String> {
|
||||
let row = sqlx
|
||||
::query("SELECT value FROM settings WHERE key = ?")
|
||||
.bind(key)
|
||||
.fetch_optional(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(row.map(|r| r.get("value")).unwrap_or_default())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_setting(
|
||||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
key: String,
|
||||
value: String
|
||||
) -> Result<(), String> {
|
||||
sqlx
|
||||
::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
|
||||
.bind(key.clone())
|
||||
.bind(value.clone())
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let _ = app_handle.track_event(
|
||||
"setting_saved",
|
||||
Some(serde_json::json!({
|
||||
"key": key
|
||||
}))
|
||||
);
|
||||
|
||||
if key == "keybind" {
|
||||
let _ = app_handle.emit("update-shortcut", &value).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_keybind(app_handle: tauri::AppHandle) -> Result<Vec<String>, String> {
|
||||
let pool = app_handle.state::<SqlitePool>();
|
||||
|
||||
let row = sqlx
|
||||
::query("SELECT value FROM settings WHERE key = 'keybind'")
|
||||
.fetch_optional(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let json = row
|
||||
.map(|r| r.get::<String, _>("value"))
|
||||
.unwrap_or_else(|| {
|
||||
serde_json
|
||||
::to_string(&vec!["MetaLeft".to_string(), "KeyV".to_string()])
|
||||
.expect("Failed to serialize default keybind")
|
||||
});
|
||||
|
||||
serde_json::from_str::<Vec<String>>(&json).map_err(|e| e.to_string())
|
||||
}
|
||||
|
|
|
@ -1,136 +1,136 @@
|
|||
#![cfg_attr(all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows")]
|
||||
|
||||
mod api;
|
||||
mod db;
|
||||
mod utils;
|
||||
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
use std::fs;
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_aptabase::{ EventTracker, InitOptions };
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_prevent_default::Flags;
|
||||
|
||||
fn main() {
|
||||
let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
|
||||
let _guard = runtime.enter();
|
||||
|
||||
tauri::Builder
|
||||
::default()
|
||||
.plugin(tauri_plugin_clipboard::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_sql::Builder::default().build())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_updater::Builder::default().build())
|
||||
.plugin(
|
||||
tauri_plugin_aptabase::Builder
|
||||
::new("A-SH-8937252746")
|
||||
.with_options(InitOptions {
|
||||
host: Some("https://aptabase.pandadev.net".to_string()),
|
||||
flush_interval: None,
|
||||
})
|
||||
.with_panic_hook(
|
||||
Box::new(|client, info, msg| {
|
||||
let location = info
|
||||
.location()
|
||||
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
|
||||
.unwrap_or_else(|| "".to_string());
|
||||
|
||||
let _ = client.track_event(
|
||||
"panic",
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"info": format!("{} ({})", msg, location),
|
||||
})
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec![])))
|
||||
.plugin(
|
||||
tauri_plugin_prevent_default::Builder
|
||||
::new()
|
||||
.with_flags(Flags::all().difference(Flags::CONTEXT_MENU))
|
||||
.build()
|
||||
)
|
||||
.setup(|app| {
|
||||
#[cfg(target_os = "macos")]
|
||||
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
|
||||
let app_data_dir = app.path().app_data_dir().unwrap();
|
||||
utils::logger::init_logger(&app_data_dir).expect("Failed to initialize logger");
|
||||
|
||||
fs::create_dir_all(&app_data_dir).expect("Failed to create app data directory");
|
||||
|
||||
let db_path = app_data_dir.join("data.db");
|
||||
let is_new_db = !db_path.exists();
|
||||
if is_new_db {
|
||||
fs::File::create(&db_path).expect("Failed to create database file");
|
||||
}
|
||||
|
||||
let db_url = format!("sqlite:{}", db_path.to_str().unwrap());
|
||||
|
||||
let app_handle = app.handle().clone();
|
||||
|
||||
let app_handle_clone = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&db_url).await
|
||||
.expect("Failed to create pool");
|
||||
|
||||
app_handle_clone.manage(pool);
|
||||
});
|
||||
|
||||
let main_window = app.get_webview_window("main");
|
||||
|
||||
let _ = db::database::setup(app);
|
||||
api::hotkeys::setup(app_handle.clone());
|
||||
api::tray::setup(app)?;
|
||||
api::clipboard::setup(app.handle());
|
||||
let _ = api::clipboard::start_monitor(app_handle.clone());
|
||||
|
||||
utils::commands::center_window_on_current_monitor(main_window.as_ref().unwrap());
|
||||
main_window
|
||||
.as_ref()
|
||||
.map(|w| w.hide())
|
||||
.unwrap_or(Ok(()))?;
|
||||
|
||||
let _ = app.track_event("app_started", None);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
api::updater::check_for_updates(app_handle, false).await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|_app, _event| {
|
||||
#[cfg(not(dev))]
|
||||
if let tauri::WindowEvent::Focused(false) = _event {
|
||||
if let Some(window) = _app.get_webview_window("main") {
|
||||
let _ = window.hide();
|
||||
}
|
||||
}
|
||||
})
|
||||
.invoke_handler(
|
||||
tauri::generate_handler![
|
||||
api::clipboard::write_and_paste,
|
||||
db::history::get_history,
|
||||
db::history::add_history_item,
|
||||
db::history::search_history,
|
||||
db::history::load_history_chunk,
|
||||
db::history::delete_history_item,
|
||||
db::history::clear_history,
|
||||
db::history::read_image,
|
||||
db::settings::get_setting,
|
||||
db::settings::save_setting,
|
||||
utils::commands::fetch_page_meta,
|
||||
utils::commands::get_app_info
|
||||
]
|
||||
)
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
#![cfg_attr(all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows")]
|
||||
|
||||
mod api;
|
||||
mod db;
|
||||
mod utils;
|
||||
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
use std::fs;
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_aptabase::{ EventTracker, InitOptions };
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_prevent_default::Flags;
|
||||
|
||||
fn main() {
|
||||
let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
|
||||
let _guard = runtime.enter();
|
||||
|
||||
tauri::Builder
|
||||
::default()
|
||||
.plugin(tauri_plugin_clipboard::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_sql::Builder::default().build())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_updater::Builder::default().build())
|
||||
.plugin(
|
||||
tauri_plugin_aptabase::Builder
|
||||
::new("A-SH-8937252746")
|
||||
.with_options(InitOptions {
|
||||
host: Some("https://aptabase.pandadev.net".to_string()),
|
||||
flush_interval: None,
|
||||
})
|
||||
.with_panic_hook(
|
||||
Box::new(|client, info, msg| {
|
||||
let location = info
|
||||
.location()
|
||||
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
|
||||
.unwrap_or_else(|| "".to_string());
|
||||
|
||||
let _ = client.track_event(
|
||||
"panic",
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"info": format!("{} ({})", msg, location),
|
||||
})
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec![])))
|
||||
.plugin(
|
||||
tauri_plugin_prevent_default::Builder
|
||||
::new()
|
||||
.with_flags(Flags::all().difference(Flags::CONTEXT_MENU))
|
||||
.build()
|
||||
)
|
||||
.setup(|app| {
|
||||
#[cfg(target_os = "macos")]
|
||||
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
|
||||
let app_data_dir = app.path().app_data_dir().unwrap();
|
||||
utils::logger::init_logger(&app_data_dir).expect("Failed to initialize logger");
|
||||
|
||||
fs::create_dir_all(&app_data_dir).expect("Failed to create app data directory");
|
||||
|
||||
let db_path = app_data_dir.join("data.db");
|
||||
let is_new_db = !db_path.exists();
|
||||
if is_new_db {
|
||||
fs::File::create(&db_path).expect("Failed to create database file");
|
||||
}
|
||||
|
||||
let db_url = format!("sqlite:{}", db_path.to_str().unwrap());
|
||||
|
||||
let app_handle = app.handle().clone();
|
||||
|
||||
let app_handle_clone = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&db_url).await
|
||||
.expect("Failed to create pool");
|
||||
|
||||
app_handle_clone.manage(pool);
|
||||
});
|
||||
|
||||
let main_window = app.get_webview_window("main");
|
||||
|
||||
let _ = db::database::setup(app);
|
||||
api::hotkeys::setup(app_handle.clone());
|
||||
api::tray::setup(app)?;
|
||||
api::clipboard::setup(app.handle());
|
||||
let _ = api::clipboard::start_monitor(app_handle.clone());
|
||||
|
||||
utils::commands::center_window_on_current_monitor(main_window.as_ref().unwrap());
|
||||
main_window
|
||||
.as_ref()
|
||||
.map(|w| w.hide())
|
||||
.unwrap_or(Ok(()))?;
|
||||
|
||||
let _ = app.track_event("app_started", None);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
api::updater::check_for_updates(app_handle, false).await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|_app, _event| {
|
||||
#[cfg(not(dev))]
|
||||
if let tauri::WindowEvent::Focused(false) = _event {
|
||||
if let Some(window) = _app.get_webview_window("main") {
|
||||
let _ = window.hide();
|
||||
}
|
||||
}
|
||||
})
|
||||
.invoke_handler(
|
||||
tauri::generate_handler![
|
||||
api::clipboard::write_and_paste,
|
||||
db::history::get_history,
|
||||
db::history::add_history_item,
|
||||
db::history::search_history,
|
||||
db::history::load_history_chunk,
|
||||
db::history::delete_history_item,
|
||||
db::history::clear_history,
|
||||
db::history::read_image,
|
||||
db::settings::get_setting,
|
||||
db::settings::save_setting,
|
||||
utils::commands::fetch_page_meta,
|
||||
utils::commands::get_app_info
|
||||
]
|
||||
)
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
|
@ -1,155 +1,155 @@
|
|||
use applications::{AppInfoContext, AppInfo, AppTrait, utils::image::RustImage};
|
||||
use base64::{ engine::general_purpose::STANDARD, Engine };
|
||||
use image::codecs::png::PngEncoder;
|
||||
use tauri::PhysicalPosition;
|
||||
use meta_fetcher;
|
||||
|
||||
pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
|
||||
if
|
||||
let Some(monitor) = window
|
||||
.available_monitors()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|m| {
|
||||
let primary_monitor = window
|
||||
.primary_monitor()
|
||||
.unwrap()
|
||||
.expect("Failed to get primary monitor");
|
||||
let mouse_position = primary_monitor.position();
|
||||
let monitor_position = m.position();
|
||||
let monitor_size = m.size();
|
||||
mouse_position.x >= monitor_position.x &&
|
||||
mouse_position.x < monitor_position.x + (monitor_size.width as i32) &&
|
||||
mouse_position.y >= monitor_position.y &&
|
||||
mouse_position.y < monitor_position.y + (monitor_size.height as i32)
|
||||
})
|
||||
{
|
||||
let monitor_size = monitor.size();
|
||||
let window_size = window.outer_size().unwrap();
|
||||
|
||||
let x = ((monitor_size.width as i32) - (window_size.width as i32)) / 2;
|
||||
let y = ((monitor_size.height as i32) - (window_size.height as i32)) / 2;
|
||||
|
||||
window
|
||||
.set_position(PhysicalPosition::new(monitor.position().x + x, monitor.position().y + y))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_app_info() -> (String, Option<String>) {
|
||||
println!("Getting app info");
|
||||
let mut ctx = AppInfoContext::new(vec![]);
|
||||
println!("Created AppInfoContext");
|
||||
|
||||
if let Err(e) = ctx.refresh_apps() {
|
||||
println!("Failed to refresh apps: {:?}", e);
|
||||
return ("System".to_string(), None);
|
||||
}
|
||||
|
||||
println!("Refreshed apps");
|
||||
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
match ctx.get_frontmost_application() {
|
||||
Ok(window) => {
|
||||
println!("Found frontmost application: {}", window.name);
|
||||
let name = window.name.clone();
|
||||
let icon = window
|
||||
.load_icon()
|
||||
.ok()
|
||||
.and_then(|i| {
|
||||
println!("Loading icon for {}", name);
|
||||
i.to_png().ok().map(|png| {
|
||||
let encoded = STANDARD.encode(png.get_bytes());
|
||||
println!("Icon encoded successfully");
|
||||
encoded
|
||||
})
|
||||
});
|
||||
println!("Returning app info: {} with icon: {}", name, icon.is_some());
|
||||
(name, icon)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to get frontmost application: {:?}", e);
|
||||
("System".to_string(), None)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(info) => info,
|
||||
Err(_) => {
|
||||
println!("Panic occurred while getting app info");
|
||||
("System".to_string(), None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn _process_icon_to_base64(path: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let img = image::open(path)?;
|
||||
let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
|
||||
let mut png_buffer = Vec::new();
|
||||
resized.write_with_encoder(PngEncoder::new(&mut png_buffer))?;
|
||||
Ok(STANDARD.encode(png_buffer))
|
||||
}
|
||||
|
||||
pub fn detect_color(color: &str) -> bool {
|
||||
let color = color.trim().to_lowercase();
|
||||
|
||||
// hex
|
||||
if color.starts_with('#') && color.len() == color.trim_end_matches(char::is_whitespace).len() {
|
||||
let hex = &color[1..];
|
||||
return match hex.len() {
|
||||
3 | 6 | 8 => hex.chars().all(|c| c.is_ascii_hexdigit()),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
// rgb/rgba
|
||||
if
|
||||
(color.starts_with("rgb(") || color.starts_with("rgba(")) &&
|
||||
color.ends_with(")") &&
|
||||
!color[..color.len() - 1].contains(")")
|
||||
{
|
||||
let values = color
|
||||
.trim_start_matches("rgba(")
|
||||
.trim_start_matches("rgb(")
|
||||
.trim_end_matches(')')
|
||||
.split(',')
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
return match values.len() {
|
||||
3 | 4 => values.iter().all(|v| v.trim().parse::<f32>().is_ok()),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
// hsl/hsla
|
||||
if
|
||||
(color.starts_with("hsl(") || color.starts_with("hsla(")) &&
|
||||
color.ends_with(")") &&
|
||||
!color[..color.len() - 1].contains(")")
|
||||
{
|
||||
let values = color
|
||||
.trim_start_matches("hsla(")
|
||||
.trim_start_matches("hsl(")
|
||||
.trim_end_matches(')')
|
||||
.split(',')
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
return match values.len() {
|
||||
3 | 4 => values.iter().all(|v| v.trim().parse::<f32>().is_ok()),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_page_meta(url: String) -> Result<(String, Option<String>), String> {
|
||||
let metadata = meta_fetcher
|
||||
::fetch_metadata(&url)
|
||||
.map_err(|e| format!("Failed to fetch metadata: {}", e))?;
|
||||
|
||||
Ok((metadata.title.unwrap_or_else(|| "No title found".to_string()), metadata.image))
|
||||
}
|
||||
use applications::{AppInfoContext, AppInfo, AppTrait, utils::image::RustImage};
|
||||
use base64::{ engine::general_purpose::STANDARD, Engine };
|
||||
use image::codecs::png::PngEncoder;
|
||||
use tauri::PhysicalPosition;
|
||||
use meta_fetcher;
|
||||
|
||||
pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
|
||||
if
|
||||
let Some(monitor) = window
|
||||
.available_monitors()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|m| {
|
||||
let primary_monitor = window
|
||||
.primary_monitor()
|
||||
.unwrap()
|
||||
.expect("Failed to get primary monitor");
|
||||
let mouse_position = primary_monitor.position();
|
||||
let monitor_position = m.position();
|
||||
let monitor_size = m.size();
|
||||
mouse_position.x >= monitor_position.x &&
|
||||
mouse_position.x < monitor_position.x + (monitor_size.width as i32) &&
|
||||
mouse_position.y >= monitor_position.y &&
|
||||
mouse_position.y < monitor_position.y + (monitor_size.height as i32)
|
||||
})
|
||||
{
|
||||
let monitor_size = monitor.size();
|
||||
let window_size = window.outer_size().unwrap();
|
||||
|
||||
let x = ((monitor_size.width as i32) - (window_size.width as i32)) / 2;
|
||||
let y = ((monitor_size.height as i32) - (window_size.height as i32)) / 2;
|
||||
|
||||
window
|
||||
.set_position(PhysicalPosition::new(monitor.position().x + x, monitor.position().y + y))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_app_info() -> (String, Option<String>) {
|
||||
println!("Getting app info");
|
||||
let mut ctx = AppInfoContext::new(vec![]);
|
||||
println!("Created AppInfoContext");
|
||||
|
||||
if let Err(e) = ctx.refresh_apps() {
|
||||
println!("Failed to refresh apps: {:?}", e);
|
||||
return ("System".to_string(), None);
|
||||
}
|
||||
|
||||
println!("Refreshed apps");
|
||||
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
match ctx.get_frontmost_application() {
|
||||
Ok(window) => {
|
||||
println!("Found frontmost application: {}", window.name);
|
||||
let name = window.name.clone();
|
||||
let icon = window
|
||||
.load_icon()
|
||||
.ok()
|
||||
.and_then(|i| {
|
||||
println!("Loading icon for {}", name);
|
||||
i.to_png().ok().map(|png| {
|
||||
let encoded = STANDARD.encode(png.get_bytes());
|
||||
println!("Icon encoded successfully");
|
||||
encoded
|
||||
})
|
||||
});
|
||||
println!("Returning app info: {} with icon: {}", name, icon.is_some());
|
||||
(name, icon)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to get frontmost application: {:?}", e);
|
||||
("System".to_string(), None)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(info) => info,
|
||||
Err(_) => {
|
||||
println!("Panic occurred while getting app info");
|
||||
("System".to_string(), None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn _process_icon_to_base64(path: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let img = image::open(path)?;
|
||||
let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
|
||||
let mut png_buffer = Vec::new();
|
||||
resized.write_with_encoder(PngEncoder::new(&mut png_buffer))?;
|
||||
Ok(STANDARD.encode(png_buffer))
|
||||
}
|
||||
|
||||
pub fn detect_color(color: &str) -> bool {
|
||||
let color = color.trim().to_lowercase();
|
||||
|
||||
// hex
|
||||
if color.starts_with('#') && color.len() == color.trim_end_matches(char::is_whitespace).len() {
|
||||
let hex = &color[1..];
|
||||
return match hex.len() {
|
||||
3 | 6 | 8 => hex.chars().all(|c| c.is_ascii_hexdigit()),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
// rgb/rgba
|
||||
if
|
||||
(color.starts_with("rgb(") || color.starts_with("rgba(")) &&
|
||||
color.ends_with(")") &&
|
||||
!color[..color.len() - 1].contains(")")
|
||||
{
|
||||
let values = color
|
||||
.trim_start_matches("rgba(")
|
||||
.trim_start_matches("rgb(")
|
||||
.trim_end_matches(')')
|
||||
.split(',')
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
return match values.len() {
|
||||
3 | 4 => values.iter().all(|v| v.trim().parse::<f32>().is_ok()),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
// hsl/hsla
|
||||
if
|
||||
(color.starts_with("hsl(") || color.starts_with("hsla(")) &&
|
||||
color.ends_with(")") &&
|
||||
!color[..color.len() - 1].contains(")")
|
||||
{
|
||||
let values = color
|
||||
.trim_start_matches("hsla(")
|
||||
.trim_start_matches("hsl(")
|
||||
.trim_end_matches(')')
|
||||
.split(',')
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
return match values.len() {
|
||||
3 | 4 => values.iter().all(|v| v.trim().parse::<f32>().is_ok()),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_page_meta(url: String) -> Result<(String, Option<String>), String> {
|
||||
let metadata = meta_fetcher
|
||||
::fetch_metadata(&url)
|
||||
.map_err(|e| format!("Failed to fetch metadata: {}", e))?;
|
||||
|
||||
Ok((metadata.title.unwrap_or_else(|| "No title found".to_string()), metadata.image))
|
||||
}
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine;
|
||||
use image::ImageFormat;
|
||||
use reqwest;
|
||||
use url::Url;
|
||||
|
||||
pub async fn fetch_favicon_as_base64(
|
||||
url: Url
|
||||
) -> Result<Option<String>, Box<dyn std::error::Error>> {
|
||||
let client = reqwest::Client::new();
|
||||
let favicon_url = format!("https://favicone.com/{}", url.host_str().unwrap());
|
||||
let response = client.get(&favicon_url).send().await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let bytes = response.bytes().await?;
|
||||
let img = image::load_from_memory(&bytes)?;
|
||||
let mut png_bytes: Vec<u8> = Vec::new();
|
||||
img.write_to(&mut std::io::Cursor::new(&mut png_bytes), ImageFormat::Png)?;
|
||||
Ok(Some(STANDARD.encode(&png_bytes)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine;
|
||||
use image::ImageFormat;
|
||||
use reqwest;
|
||||
use url::Url;
|
||||
|
||||
pub async fn fetch_favicon_as_base64(
|
||||
url: Url
|
||||
) -> Result<Option<String>, Box<dyn std::error::Error>> {
|
||||
let client = reqwest::Client::new();
|
||||
let favicon_url = format!("https://favicone.com/{}", url.host_str().unwrap());
|
||||
let response = client.get(&favicon_url).send().await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let bytes = response.bytes().await?;
|
||||
let img = image::load_from_memory(&bytes)?;
|
||||
let mut png_bytes: Vec<u8> = Vec::new();
|
||||
img.write_to(&mut std::io::Cursor::new(&mut png_bytes), ImageFormat::Png)?;
|
||||
Ok(Some(STANDARD.encode(&png_bytes)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,120 +1,120 @@
|
|||
use global_hotkey::hotkey::Code;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub struct KeyCode(Code);
|
||||
|
||||
impl FromStr for KeyCode {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let code = match s {
|
||||
"Backquote" => Code::Backquote,
|
||||
"Backslash" => Code::Backslash,
|
||||
"BracketLeft" => Code::BracketLeft,
|
||||
"BracketRight" => Code::BracketRight,
|
||||
"Comma" => Code::Comma,
|
||||
"Digit0" => Code::Digit0,
|
||||
"Digit1" => Code::Digit1,
|
||||
"Digit2" => Code::Digit2,
|
||||
"Digit3" => Code::Digit3,
|
||||
"Digit4" => Code::Digit4,
|
||||
"Digit5" => Code::Digit5,
|
||||
"Digit6" => Code::Digit6,
|
||||
"Digit7" => Code::Digit7,
|
||||
"Digit8" => Code::Digit8,
|
||||
"Digit9" => Code::Digit9,
|
||||
"Equal" => Code::Equal,
|
||||
"KeyA" => Code::KeyA,
|
||||
"KeyB" => Code::KeyB,
|
||||
"KeyC" => Code::KeyC,
|
||||
"KeyD" => Code::KeyD,
|
||||
"KeyE" => Code::KeyE,
|
||||
"KeyF" => Code::KeyF,
|
||||
"KeyG" => Code::KeyG,
|
||||
"KeyH" => Code::KeyH,
|
||||
"KeyI" => Code::KeyI,
|
||||
"KeyJ" => Code::KeyJ,
|
||||
"KeyK" => Code::KeyK,
|
||||
"KeyL" => Code::KeyL,
|
||||
"KeyM" => Code::KeyM,
|
||||
"KeyN" => Code::KeyN,
|
||||
"KeyO" => Code::KeyO,
|
||||
"KeyP" => Code::KeyP,
|
||||
"KeyQ" => Code::KeyQ,
|
||||
"KeyR" => Code::KeyR,
|
||||
"KeyS" => Code::KeyS,
|
||||
"KeyT" => Code::KeyT,
|
||||
"KeyU" => Code::KeyU,
|
||||
"KeyV" => Code::KeyV,
|
||||
"KeyW" => Code::KeyW,
|
||||
"KeyX" => Code::KeyX,
|
||||
"KeyY" => Code::KeyY,
|
||||
"KeyZ" => Code::KeyZ,
|
||||
"Minus" => Code::Minus,
|
||||
"Period" => Code::Period,
|
||||
"Quote" => Code::Quote,
|
||||
"Semicolon" => Code::Semicolon,
|
||||
"Slash" => Code::Slash,
|
||||
"Backspace" => Code::Backspace,
|
||||
"CapsLock" => Code::CapsLock,
|
||||
"Delete" => Code::Delete,
|
||||
"Enter" => Code::Enter,
|
||||
"Space" => Code::Space,
|
||||
"Tab" => Code::Tab,
|
||||
"End" => Code::End,
|
||||
"Home" => Code::Home,
|
||||
"Insert" => Code::Insert,
|
||||
"PageDown" => Code::PageDown,
|
||||
"PageUp" => Code::PageUp,
|
||||
"ArrowDown" => Code::ArrowDown,
|
||||
"ArrowLeft" => Code::ArrowLeft,
|
||||
"ArrowRight" => Code::ArrowRight,
|
||||
"ArrowUp" => Code::ArrowUp,
|
||||
"NumLock" => Code::NumLock,
|
||||
"Numpad0" => Code::Numpad0,
|
||||
"Numpad1" => Code::Numpad1,
|
||||
"Numpad2" => Code::Numpad2,
|
||||
"Numpad3" => Code::Numpad3,
|
||||
"Numpad4" => Code::Numpad4,
|
||||
"Numpad5" => Code::Numpad5,
|
||||
"Numpad6" => Code::Numpad6,
|
||||
"Numpad7" => Code::Numpad7,
|
||||
"Numpad8" => Code::Numpad8,
|
||||
"Numpad9" => Code::Numpad9,
|
||||
"NumpadAdd" => Code::NumpadAdd,
|
||||
"NumpadDecimal" => Code::NumpadDecimal,
|
||||
"NumpadDivide" => Code::NumpadDivide,
|
||||
"NumpadMultiply" => Code::NumpadMultiply,
|
||||
"NumpadSubtract" => Code::NumpadSubtract,
|
||||
"Escape" => Code::Escape,
|
||||
"PrintScreen" => Code::PrintScreen,
|
||||
"ScrollLock" => Code::ScrollLock,
|
||||
"Pause" => Code::Pause,
|
||||
"AudioVolumeDown" => Code::AudioVolumeDown,
|
||||
"AudioVolumeMute" => Code::AudioVolumeMute,
|
||||
"AudioVolumeUp" => Code::AudioVolumeUp,
|
||||
"F1" => Code::F1,
|
||||
"F2" => Code::F2,
|
||||
"F3" => Code::F3,
|
||||
"F4" => Code::F4,
|
||||
"F5" => Code::F5,
|
||||
"F6" => Code::F6,
|
||||
"F7" => Code::F7,
|
||||
"F8" => Code::F8,
|
||||
"F9" => Code::F9,
|
||||
"F10" => Code::F10,
|
||||
"F11" => Code::F11,
|
||||
"F12" => Code::F12,
|
||||
_ => {
|
||||
return Err(format!("Unknown key code: {}", s));
|
||||
}
|
||||
};
|
||||
Ok(KeyCode(code))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeyCode> for Code {
|
||||
fn from(key_code: KeyCode) -> Self {
|
||||
key_code.0
|
||||
}
|
||||
}
|
||||
use global_hotkey::hotkey::Code;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub struct KeyCode(Code);
|
||||
|
||||
impl FromStr for KeyCode {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let code = match s {
|
||||
"Backquote" => Code::Backquote,
|
||||
"Backslash" => Code::Backslash,
|
||||
"BracketLeft" => Code::BracketLeft,
|
||||
"BracketRight" => Code::BracketRight,
|
||||
"Comma" => Code::Comma,
|
||||
"Digit0" => Code::Digit0,
|
||||
"Digit1" => Code::Digit1,
|
||||
"Digit2" => Code::Digit2,
|
||||
"Digit3" => Code::Digit3,
|
||||
"Digit4" => Code::Digit4,
|
||||
"Digit5" => Code::Digit5,
|
||||
"Digit6" => Code::Digit6,
|
||||
"Digit7" => Code::Digit7,
|
||||
"Digit8" => Code::Digit8,
|
||||
"Digit9" => Code::Digit9,
|
||||
"Equal" => Code::Equal,
|
||||
"KeyA" => Code::KeyA,
|
||||
"KeyB" => Code::KeyB,
|
||||
"KeyC" => Code::KeyC,
|
||||
"KeyD" => Code::KeyD,
|
||||
"KeyE" => Code::KeyE,
|
||||
"KeyF" => Code::KeyF,
|
||||
"KeyG" => Code::KeyG,
|
||||
"KeyH" => Code::KeyH,
|
||||
"KeyI" => Code::KeyI,
|
||||
"KeyJ" => Code::KeyJ,
|
||||
"KeyK" => Code::KeyK,
|
||||
"KeyL" => Code::KeyL,
|
||||
"KeyM" => Code::KeyM,
|
||||
"KeyN" => Code::KeyN,
|
||||
"KeyO" => Code::KeyO,
|
||||
"KeyP" => Code::KeyP,
|
||||
"KeyQ" => Code::KeyQ,
|
||||
"KeyR" => Code::KeyR,
|
||||
"KeyS" => Code::KeyS,
|
||||
"KeyT" => Code::KeyT,
|
||||
"KeyU" => Code::KeyU,
|
||||
"KeyV" => Code::KeyV,
|
||||
"KeyW" => Code::KeyW,
|
||||
"KeyX" => Code::KeyX,
|
||||
"KeyY" => Code::KeyY,
|
||||
"KeyZ" => Code::KeyZ,
|
||||
"Minus" => Code::Minus,
|
||||
"Period" => Code::Period,
|
||||
"Quote" => Code::Quote,
|
||||
"Semicolon" => Code::Semicolon,
|
||||
"Slash" => Code::Slash,
|
||||
"Backspace" => Code::Backspace,
|
||||
"CapsLock" => Code::CapsLock,
|
||||
"Delete" => Code::Delete,
|
||||
"Enter" => Code::Enter,
|
||||
"Space" => Code::Space,
|
||||
"Tab" => Code::Tab,
|
||||
"End" => Code::End,
|
||||
"Home" => Code::Home,
|
||||
"Insert" => Code::Insert,
|
||||
"PageDown" => Code::PageDown,
|
||||
"PageUp" => Code::PageUp,
|
||||
"ArrowDown" => Code::ArrowDown,
|
||||
"ArrowLeft" => Code::ArrowLeft,
|
||||
"ArrowRight" => Code::ArrowRight,
|
||||
"ArrowUp" => Code::ArrowUp,
|
||||
"NumLock" => Code::NumLock,
|
||||
"Numpad0" => Code::Numpad0,
|
||||
"Numpad1" => Code::Numpad1,
|
||||
"Numpad2" => Code::Numpad2,
|
||||
"Numpad3" => Code::Numpad3,
|
||||
"Numpad4" => Code::Numpad4,
|
||||
"Numpad5" => Code::Numpad5,
|
||||
"Numpad6" => Code::Numpad6,
|
||||
"Numpad7" => Code::Numpad7,
|
||||
"Numpad8" => Code::Numpad8,
|
||||
"Numpad9" => Code::Numpad9,
|
||||
"NumpadAdd" => Code::NumpadAdd,
|
||||
"NumpadDecimal" => Code::NumpadDecimal,
|
||||
"NumpadDivide" => Code::NumpadDivide,
|
||||
"NumpadMultiply" => Code::NumpadMultiply,
|
||||
"NumpadSubtract" => Code::NumpadSubtract,
|
||||
"Escape" => Code::Escape,
|
||||
"PrintScreen" => Code::PrintScreen,
|
||||
"ScrollLock" => Code::ScrollLock,
|
||||
"Pause" => Code::Pause,
|
||||
"AudioVolumeDown" => Code::AudioVolumeDown,
|
||||
"AudioVolumeMute" => Code::AudioVolumeMute,
|
||||
"AudioVolumeUp" => Code::AudioVolumeUp,
|
||||
"F1" => Code::F1,
|
||||
"F2" => Code::F2,
|
||||
"F3" => Code::F3,
|
||||
"F4" => Code::F4,
|
||||
"F5" => Code::F5,
|
||||
"F6" => Code::F6,
|
||||
"F7" => Code::F7,
|
||||
"F8" => Code::F8,
|
||||
"F9" => Code::F9,
|
||||
"F10" => Code::F10,
|
||||
"F11" => Code::F11,
|
||||
"F12" => Code::F12,
|
||||
_ => {
|
||||
return Err(format!("Unknown key code: {}", s));
|
||||
}
|
||||
};
|
||||
Ok(KeyCode(code))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeyCode> for Code {
|
||||
fn from(key_code: KeyCode) -> Self {
|
||||
key_code.0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,84 +1,84 @@
|
|||
use chrono;
|
||||
use log::{ LevelFilter, SetLoggerError };
|
||||
use std::fs::{ File, OpenOptions };
|
||||
use std::io::Write;
|
||||
use std::panic;
|
||||
|
||||
pub struct FileLogger {
|
||||
file: File,
|
||||
}
|
||||
|
||||
impl log::Log for FileLogger {
|
||||
fn enabled(&self, _metadata: &log::Metadata) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn log(&self, record: &log::Record) {
|
||||
if self.enabled(record.metadata()) {
|
||||
let mut file = self.file.try_clone().expect("Failed to clone file handle");
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
"{} [{:<5}] {}: {} ({}:{})",
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
|
||||
record.level(),
|
||||
record.target(),
|
||||
record.args(),
|
||||
record.file().unwrap_or("unknown"),
|
||||
record.line().unwrap_or(0)
|
||||
).expect("Failed to write to log file");
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {
|
||||
self.file.sync_all().expect("Failed to flush log file");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_logger(app_data_dir: &std::path::Path) -> Result<(), SetLoggerError> {
|
||||
let logs_dir = app_data_dir.join("logs");
|
||||
std::fs::create_dir_all(&logs_dir).expect("Failed to create logs directory");
|
||||
|
||||
let log_path = logs_dir.join("app.log");
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&log_path)
|
||||
.expect("Failed to open log file");
|
||||
|
||||
let panic_file = file.try_clone().expect("Failed to clone file handle");
|
||||
panic::set_hook(
|
||||
Box::new(move |panic_info| {
|
||||
let mut file = panic_file.try_clone().expect("Failed to clone file handle");
|
||||
|
||||
let location = panic_info
|
||||
.location()
|
||||
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
|
||||
.unwrap_or_else(|| "unknown location".to_string());
|
||||
|
||||
let message = match panic_info.payload().downcast_ref::<&str>() {
|
||||
Some(s) => *s,
|
||||
None =>
|
||||
match panic_info.payload().downcast_ref::<String>() {
|
||||
Some(s) => s.as_str(),
|
||||
None => "Unknown panic message",
|
||||
}
|
||||
};
|
||||
|
||||
let _ = writeln!(
|
||||
file,
|
||||
"{} [PANIC] rust_panic: {} ({})",
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
|
||||
message,
|
||||
location
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
let logger = Box::new(FileLogger { file });
|
||||
unsafe {
|
||||
log::set_logger_racy(Box::leak(logger))?;
|
||||
}
|
||||
log::set_max_level(LevelFilter::Debug);
|
||||
Ok(())
|
||||
}
|
||||
use chrono;
|
||||
use log::{ LevelFilter, SetLoggerError };
|
||||
use std::fs::{ File, OpenOptions };
|
||||
use std::io::Write;
|
||||
use std::panic;
|
||||
|
||||
pub struct FileLogger {
|
||||
file: File,
|
||||
}
|
||||
|
||||
impl log::Log for FileLogger {
|
||||
fn enabled(&self, _metadata: &log::Metadata) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn log(&self, record: &log::Record) {
|
||||
if self.enabled(record.metadata()) {
|
||||
let mut file = self.file.try_clone().expect("Failed to clone file handle");
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
"{} [{:<5}] {}: {} ({}:{})",
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
|
||||
record.level(),
|
||||
record.target(),
|
||||
record.args(),
|
||||
record.file().unwrap_or("unknown"),
|
||||
record.line().unwrap_or(0)
|
||||
).expect("Failed to write to log file");
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {
|
||||
self.file.sync_all().expect("Failed to flush log file");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_logger(app_data_dir: &std::path::Path) -> Result<(), SetLoggerError> {
|
||||
let logs_dir = app_data_dir.join("logs");
|
||||
std::fs::create_dir_all(&logs_dir).expect("Failed to create logs directory");
|
||||
|
||||
let log_path = logs_dir.join("app.log");
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&log_path)
|
||||
.expect("Failed to open log file");
|
||||
|
||||
let panic_file = file.try_clone().expect("Failed to clone file handle");
|
||||
panic::set_hook(
|
||||
Box::new(move |panic_info| {
|
||||
let mut file = panic_file.try_clone().expect("Failed to clone file handle");
|
||||
|
||||
let location = panic_info
|
||||
.location()
|
||||
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
|
||||
.unwrap_or_else(|| "unknown location".to_string());
|
||||
|
||||
let message = match panic_info.payload().downcast_ref::<&str>() {
|
||||
Some(s) => *s,
|
||||
None =>
|
||||
match panic_info.payload().downcast_ref::<String>() {
|
||||
Some(s) => s.as_str(),
|
||||
None => "Unknown panic message",
|
||||
}
|
||||
};
|
||||
|
||||
let _ = writeln!(
|
||||
file,
|
||||
"{} [PANIC] rust_panic: {} ({})",
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
|
||||
message,
|
||||
location
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
let logger = Box::new(FileLogger { file });
|
||||
unsafe {
|
||||
log::set_logger_racy(Box::leak(logger))?;
|
||||
}
|
||||
log::set_max_level(LevelFilter::Debug);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
pub mod commands;
|
||||
pub mod favicon;
|
||||
pub mod types;
|
||||
pub mod logger;
|
||||
pub mod keys;
|
||||
pub mod commands;
|
||||
pub mod favicon;
|
||||
pub mod types;
|
||||
pub mod logger;
|
||||
pub mod keys;
|
||||
|
|
|
@ -1,155 +1,155 @@
|
|||
use chrono::{ DateTime, Utc };
|
||||
use serde::{ Deserialize, Serialize };
|
||||
use std::fmt;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||
pub struct HistoryItem {
|
||||
pub id: String,
|
||||
pub source: String,
|
||||
pub source_icon: Option<String>,
|
||||
pub content_type: ContentType,
|
||||
pub content: String,
|
||||
pub favicon: Option<String>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub language: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ContentType {
|
||||
Text,
|
||||
Image,
|
||||
File,
|
||||
Link,
|
||||
Color,
|
||||
Code,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoText {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub characters: i32,
|
||||
pub words: i32,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoImage {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub dimensions: String,
|
||||
pub size: i64,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoFile {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub path: String,
|
||||
pub filesize: i64,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoLink {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub title: Option<String>,
|
||||
pub url: String,
|
||||
pub characters: i32,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoColor {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub hex: String,
|
||||
pub rgb: String,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoCode {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub language: String,
|
||||
pub lines: i32,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl fmt::Display for ContentType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
ContentType::Text => write!(f, "text"),
|
||||
ContentType::Image => write!(f, "image"),
|
||||
ContentType::File => write!(f, "file"),
|
||||
ContentType::Link => write!(f, "link"),
|
||||
ContentType::Color => write!(f, "color"),
|
||||
ContentType::Code => write!(f, "code"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ContentType {
|
||||
fn from(s: String) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"text" => ContentType::Text,
|
||||
"image" => ContentType::Image,
|
||||
"file" => ContentType::File,
|
||||
"link" => ContentType::Link,
|
||||
"color" => ContentType::Color,
|
||||
"code" => ContentType::Code,
|
||||
_ => ContentType::Text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HistoryItem {
|
||||
pub fn new(
|
||||
source: String,
|
||||
content_type: ContentType,
|
||||
content: String,
|
||||
favicon: Option<String>,
|
||||
source_icon: Option<String>,
|
||||
language: Option<String>
|
||||
) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
source,
|
||||
source_icon,
|
||||
content_type,
|
||||
content,
|
||||
favicon,
|
||||
timestamp: Utc::now(),
|
||||
language,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_row(
|
||||
&self
|
||||
) -> (
|
||||
String,
|
||||
String,
|
||||
Option<String>,
|
||||
String,
|
||||
String,
|
||||
Option<String>,
|
||||
DateTime<Utc>,
|
||||
Option<String>,
|
||||
) {
|
||||
(
|
||||
self.id.clone(),
|
||||
self.source.clone(),
|
||||
self.source_icon.clone(),
|
||||
self.content_type.to_string(),
|
||||
self.content.clone(),
|
||||
self.favicon.clone(),
|
||||
self.timestamp,
|
||||
self.language.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
use chrono::{ DateTime, Utc };
|
||||
use serde::{ Deserialize, Serialize };
|
||||
use std::fmt;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||
pub struct HistoryItem {
|
||||
pub id: String,
|
||||
pub source: String,
|
||||
pub source_icon: Option<String>,
|
||||
pub content_type: ContentType,
|
||||
pub content: String,
|
||||
pub favicon: Option<String>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub language: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ContentType {
|
||||
Text,
|
||||
Image,
|
||||
File,
|
||||
Link,
|
||||
Color,
|
||||
Code,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoText {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub characters: i32,
|
||||
pub words: i32,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoImage {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub dimensions: String,
|
||||
pub size: i64,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoFile {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub path: String,
|
||||
pub filesize: i64,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoLink {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub title: Option<String>,
|
||||
pub url: String,
|
||||
pub characters: i32,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoColor {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub hex: String,
|
||||
pub rgb: String,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InfoCode {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub language: String,
|
||||
pub lines: i32,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl fmt::Display for ContentType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
ContentType::Text => write!(f, "text"),
|
||||
ContentType::Image => write!(f, "image"),
|
||||
ContentType::File => write!(f, "file"),
|
||||
ContentType::Link => write!(f, "link"),
|
||||
ContentType::Color => write!(f, "color"),
|
||||
ContentType::Code => write!(f, "code"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ContentType {
|
||||
fn from(s: String) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"text" => ContentType::Text,
|
||||
"image" => ContentType::Image,
|
||||
"file" => ContentType::File,
|
||||
"link" => ContentType::Link,
|
||||
"color" => ContentType::Color,
|
||||
"code" => ContentType::Code,
|
||||
_ => ContentType::Text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HistoryItem {
|
||||
pub fn new(
|
||||
source: String,
|
||||
content_type: ContentType,
|
||||
content: String,
|
||||
favicon: Option<String>,
|
||||
source_icon: Option<String>,
|
||||
language: Option<String>
|
||||
) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
source,
|
||||
source_icon,
|
||||
content_type,
|
||||
content,
|
||||
favicon,
|
||||
timestamp: Utc::now(),
|
||||
language,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_row(
|
||||
&self
|
||||
) -> (
|
||||
String,
|
||||
String,
|
||||
Option<String>,
|
||||
String,
|
||||
String,
|
||||
Option<String>,
|
||||
DateTime<Utc>,
|
||||
Option<String>,
|
||||
) {
|
||||
(
|
||||
self.id.clone(),
|
||||
self.source.clone(),
|
||||
self.source_icon.clone(),
|
||||
self.content_type.to_string(),
|
||||
self.content.clone(),
|
||||
self.favicon.clone(),
|
||||
self.timestamp,
|
||||
self.language.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,58 +1,58 @@
|
|||
{
|
||||
"productName": "Qopy",
|
||||
"version": "0.4.0",
|
||||
"identifier": "net.pandadev.qopy",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
"devUrl": "http://localhost:3000",
|
||||
"beforeDevCommand": "pnpm nuxt dev",
|
||||
"beforeBuildCommand": "pnpm nuxt generate"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Qopy",
|
||||
"titleBarStyle": "Overlay",
|
||||
"fullscreen": false,
|
||||
"resizable": false,
|
||||
"height": 474,
|
||||
"width": 750,
|
||||
"minHeight": 474,
|
||||
"maxHeight": 474,
|
||||
"minWidth": 750,
|
||||
"maxWidth": 750,
|
||||
"decorations": false,
|
||||
"center": true,
|
||||
"shadow": false,
|
||||
"transparent": true,
|
||||
"skipTaskbar": true,
|
||||
"alwaysOnTop": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"withGlobalTauri": true,
|
||||
"macOSPrivateApi": true
|
||||
},
|
||||
"bundle": {
|
||||
"createUpdaterArtifacts": true,
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"category": "DeveloperTool"
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNDIzNjA1QjE0NjU1OTkKUldTWlZVYXhCVFpDRWNvNmt0UE5lQmZkblEyZGZiZ2tHelJvT2YvNVpLU1RIM1RKZFQrb2tzWWwK",
|
||||
"endpoints": ["https://qopy.pandadev.net/"]
|
||||
}
|
||||
},
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json"
|
||||
}
|
||||
{
|
||||
"productName": "Qopy",
|
||||
"version": "0.4.0",
|
||||
"identifier": "net.pandadev.qopy",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
"devUrl": "http://localhost:3000",
|
||||
"beforeDevCommand": "bun nuxt dev",
|
||||
"beforeBuildCommand": "bun nuxt generate"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Qopy",
|
||||
"titleBarStyle": "Overlay",
|
||||
"fullscreen": false,
|
||||
"resizable": false,
|
||||
"height": 474,
|
||||
"width": 750,
|
||||
"minHeight": 474,
|
||||
"maxHeight": 474,
|
||||
"minWidth": 750,
|
||||
"maxWidth": 750,
|
||||
"decorations": false,
|
||||
"center": true,
|
||||
"shadow": false,
|
||||
"transparent": true,
|
||||
"skipTaskbar": true,
|
||||
"alwaysOnTop": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"withGlobalTauri": true,
|
||||
"macOSPrivateApi": true
|
||||
},
|
||||
"bundle": {
|
||||
"createUpdaterArtifacts": true,
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"category": "DeveloperTool"
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNDIzNjA1QjE0NjU1OTkKUldTWlZVYXhCVFpDRWNvNmt0UE5lQmZkblEyZGZiZ2tHelJvT2YvNVpLU1RIM1RKZFQrb2tzWWwK",
|
||||
"endpoints": ["https://qopy.pandadev.net/"]
|
||||
}
|
||||
},
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json"
|
||||
}
|
||||
|
|
|
@ -1,185 +1,185 @@
|
|||
$primary: #2e2d2b;
|
||||
$accent: #feb453;
|
||||
$divider: #ffffff0d;
|
||||
|
||||
$text: #e5dfd5;
|
||||
$text2: #ada9a1;
|
||||
$mutedtext: #78756f;
|
||||
|
||||
$search-height: 56px;
|
||||
$sidebar-width: 286px;
|
||||
$bottom-bar-height: 39px;
|
||||
$info-panel-height: 160px;
|
||||
$content-view-height: calc(
|
||||
100% - $search-height - $info-panel-height - $bottom-bar-height
|
||||
);
|
||||
|
||||
main {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: $primary;
|
||||
border: 1px solid $divider;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 12px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 376px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 14px 8px;
|
||||
gap: 8px;
|
||||
min-width: 286px;
|
||||
border-right: 1px solid var(--border);
|
||||
|
||||
.time-separator {
|
||||
font-size: 12px;
|
||||
color: $text2;
|
||||
font-family: SFRoundedSemiBold;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.group {
|
||||
& + .group {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.time-separator {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.results-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.favicon,
|
||||
.image,
|
||||
.icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 100%;
|
||||
font-family: CommitMono !important;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1;
|
||||
border-radius: 10px;
|
||||
width: 462px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
overflow: hidden;
|
||||
z-index: 2;
|
||||
color: $text;
|
||||
|
||||
&:not(:has(.image)) {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
span.content-text {
|
||||
font-family: CommitMono !important;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
.information {
|
||||
min-height: 160px;
|
||||
width: 462px;
|
||||
border-top: 1px solid $divider;
|
||||
padding: 14px;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
|
||||
.title {
|
||||
font-family: SFRoundedSemiBold;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.6px;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
flex-direction: column;
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid $divider;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: SFRoundedMedium;
|
||||
color: $text2;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
font-family: CommitMono;
|
||||
color: $text;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
margin-left: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
img {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary);
|
||||
$primary: #2e2d2b;
|
||||
$accent: #feb453;
|
||||
$divider: #ffffff0d;
|
||||
|
||||
$text: #e5dfd5;
|
||||
$text2: #ada9a1;
|
||||
$mutedtext: #78756f;
|
||||
|
||||
$search-height: 56px;
|
||||
$sidebar-width: 286px;
|
||||
$bottom-bar-height: 39px;
|
||||
$info-panel-height: 160px;
|
||||
$content-view-height: calc(
|
||||
100% - $search-height - $info-panel-height - $bottom-bar-height
|
||||
);
|
||||
|
||||
main {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: $primary;
|
||||
border: 1px solid $divider;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 12px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 376px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 14px 8px;
|
||||
gap: 8px;
|
||||
min-width: 286px;
|
||||
border-right: 1px solid var(--border);
|
||||
|
||||
.time-separator {
|
||||
font-size: 12px;
|
||||
color: $text2;
|
||||
font-family: SFRoundedSemiBold;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.group {
|
||||
& + .group {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.time-separator {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.results-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.favicon,
|
||||
.image,
|
||||
.icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 100%;
|
||||
font-family: CommitMono !important;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1;
|
||||
border-radius: 10px;
|
||||
width: 462px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
overflow: hidden;
|
||||
z-index: 2;
|
||||
color: $text;
|
||||
|
||||
&:not(:has(.image)) {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
span.content-text {
|
||||
font-family: CommitMono !important;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
.information {
|
||||
min-height: 160px;
|
||||
width: 462px;
|
||||
border-top: 1px solid $divider;
|
||||
padding: 14px;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
|
||||
.title {
|
||||
font-family: SFRoundedSemiBold;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.6px;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
flex-direction: column;
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid $divider;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: SFRoundedMedium;
|
||||
color: $text2;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
font-family: CommitMono;
|
||||
color: $text;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
margin-left: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
img {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary);
|
||||
}
|
|
@ -1,235 +1,235 @@
|
|||
$primary: #2e2d2b;
|
||||
$accent: #feb453;
|
||||
$divider: #ffffff0d;
|
||||
|
||||
$text: #e5dfd5;
|
||||
$text2: #ada9a1;
|
||||
$mutedtext: #78756f;
|
||||
|
||||
main {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: $primary;
|
||||
border: 1px solid $divider;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 12px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.back {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
background-color: $divider;
|
||||
border-radius: 6px;
|
||||
padding: 8px 6px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $text2;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: SFRoundedMedium;
|
||||
}
|
||||
|
||||
.settings-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-top: 26px;
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
font-family: SFRoundedMedium;
|
||||
|
||||
.settings {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-left: -26px;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
|
||||
.names {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
p {
|
||||
font-family: SFRoundedSemiBold;
|
||||
color: $text2;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
color: $mutedtext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.launch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
input[type="checkbox"] {
|
||||
appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-color: transparent;
|
||||
border-radius: 5px;
|
||||
border: 1px solid $mutedtext;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:checked {
|
||||
~ .checkmark {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $text2;
|
||||
}
|
||||
}
|
||||
|
||||
.keybind-input {
|
||||
width: min-content;
|
||||
white-space: nowrap;
|
||||
padding: 6px;
|
||||
border: 1px solid $divider;
|
||||
color: $text2;
|
||||
display: flex;
|
||||
border-radius: 10px;
|
||||
outline: none;
|
||||
gap: 4px;
|
||||
|
||||
.key {
|
||||
color: $text2;
|
||||
font-family: SFRoundedMedium;
|
||||
background-color: $divider;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.keybind-input:focus {
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.empty-keybind {
|
||||
border-color: rgba(255, 82, 82, 0.298);
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
width: 100%;
|
||||
min-height: 56px;
|
||||
border-bottom: 1px solid $divider;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
height: 40px;
|
||||
width: calc(100vw - 2px);
|
||||
backdrop-filter: blur(18px);
|
||||
background-color: hsla(40, 3%, 16%, 0.8);
|
||||
position: fixed;
|
||||
bottom: 1px;
|
||||
left: 1px;
|
||||
z-index: 100;
|
||||
border-radius: 0 0 12px 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding-inline: 12px;
|
||||
padding-right: 6px;
|
||||
padding-top: 1px;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
border-top: 1px solid $divider;
|
||||
|
||||
p {
|
||||
color: $text2;
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.logo {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.actions div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 2px;
|
||||
height: 12px;
|
||||
background-color: $divider;
|
||||
margin-left: 8px;
|
||||
margin-right: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: 4px;
|
||||
padding-left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 7px;
|
||||
background-color: transparent;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
p {
|
||||
color: $text;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.actions:hover {
|
||||
background-color: $divider;
|
||||
}
|
||||
|
||||
&:hover .actions:hover ~ .divider {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
$primary: #2e2d2b;
|
||||
$accent: #feb453;
|
||||
$divider: #ffffff0d;
|
||||
|
||||
$text: #e5dfd5;
|
||||
$text2: #ada9a1;
|
||||
$mutedtext: #78756f;
|
||||
|
||||
main {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: $primary;
|
||||
border: 1px solid $divider;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 12px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.back {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
background-color: $divider;
|
||||
border-radius: 6px;
|
||||
padding: 8px 6px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $text2;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: SFRoundedMedium;
|
||||
}
|
||||
|
||||
.settings-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-top: 26px;
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
font-family: SFRoundedMedium;
|
||||
|
||||
.settings {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-left: -26px;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
|
||||
.names {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
p {
|
||||
font-family: SFRoundedSemiBold;
|
||||
color: $text2;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
color: $mutedtext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.launch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
input[type="checkbox"] {
|
||||
appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-color: transparent;
|
||||
border-radius: 5px;
|
||||
border: 1px solid $mutedtext;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:checked {
|
||||
~ .checkmark {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $text2;
|
||||
}
|
||||
}
|
||||
|
||||
.keybind-input {
|
||||
width: min-content;
|
||||
white-space: nowrap;
|
||||
padding: 6px;
|
||||
border: 1px solid $divider;
|
||||
color: $text2;
|
||||
display: flex;
|
||||
border-radius: 10px;
|
||||
outline: none;
|
||||
gap: 4px;
|
||||
|
||||
.key {
|
||||
color: $text2;
|
||||
font-family: SFRoundedMedium;
|
||||
background-color: $divider;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.keybind-input:focus {
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.empty-keybind {
|
||||
border-color: rgba(255, 82, 82, 0.298);
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
width: 100%;
|
||||
min-height: 56px;
|
||||
border-bottom: 1px solid $divider;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
height: 40px;
|
||||
width: calc(100vw - 2px);
|
||||
backdrop-filter: blur(18px);
|
||||
background-color: hsla(40, 3%, 16%, 0.8);
|
||||
position: fixed;
|
||||
bottom: 1px;
|
||||
left: 1px;
|
||||
z-index: 100;
|
||||
border-radius: 0 0 12px 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding-inline: 12px;
|
||||
padding-right: 6px;
|
||||
padding-top: 1px;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
border-top: 1px solid $divider;
|
||||
|
||||
p {
|
||||
color: $text2;
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.logo {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.actions div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 2px;
|
||||
height: 12px;
|
||||
background-color: $divider;
|
||||
margin-left: 8px;
|
||||
margin-right: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: 4px;
|
||||
padding-left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 7px;
|
||||
background-color: transparent;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
p {
|
||||
color: $text;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.actions:hover {
|
||||
background-color: $divider;
|
||||
}
|
||||
|
||||
&:hover .actions:hover ~ .divider {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
||||
|
|
52
types/keyboard.d.ts
vendored
52
types/keyboard.d.ts
vendored
|
@ -1,27 +1,27 @@
|
|||
import type { Key as WaraduKey, useKeyboard } from '@waradu/keyboard';
|
||||
|
||||
declare module '#app' {
|
||||
interface NuxtApp {
|
||||
$keyboard: {
|
||||
listen: ReturnType<typeof useKeyboard>['listen'];
|
||||
init: ReturnType<typeof useKeyboard>['init'];
|
||||
Key: typeof WaraduKey;
|
||||
currentOS: string;
|
||||
clearAll: () => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
$keyboard: {
|
||||
listen: ReturnType<typeof useKeyboard>['listen'];
|
||||
init: ReturnType<typeof useKeyboard>['init'];
|
||||
Key: typeof WaraduKey;
|
||||
currentOS: string;
|
||||
clearAll: () => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
import type { Key as WaraduKey, useKeyboard } from '@waradu/keyboard';
|
||||
|
||||
declare module '#app' {
|
||||
interface NuxtApp {
|
||||
$keyboard: {
|
||||
listen: ReturnType<typeof useKeyboard>['listen'];
|
||||
init: ReturnType<typeof useKeyboard>['init'];
|
||||
Key: typeof WaraduKey;
|
||||
currentOS: string;
|
||||
clearAll: () => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
$keyboard: {
|
||||
listen: ReturnType<typeof useKeyboard>['listen'];
|
||||
init: ReturnType<typeof useKeyboard>['init'];
|
||||
Key: typeof WaraduKey;
|
||||
currentOS: string;
|
||||
clearAll: () => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
432
types/keys.ts
432
types/keys.ts
|
@ -1,217 +1,217 @@
|
|||
export enum KeyValues {
|
||||
Backquote = 'Backquote',
|
||||
Backslash = 'Backslash',
|
||||
BracketLeft = 'BracketLeft',
|
||||
BracketRight = 'BracketRight',
|
||||
Comma = 'Comma',
|
||||
Digit0 = 'Digit0',
|
||||
Digit1 = 'Digit1',
|
||||
Digit2 = 'Digit2',
|
||||
Digit3 = 'Digit3',
|
||||
Digit4 = 'Digit4',
|
||||
Digit5 = 'Digit5',
|
||||
Digit6 = 'Digit6',
|
||||
Digit7 = 'Digit7',
|
||||
Digit8 = 'Digit8',
|
||||
Digit9 = 'Digit9',
|
||||
Equal = 'Equal',
|
||||
KeyA = 'KeyA',
|
||||
KeyB = 'KeyB',
|
||||
KeyC = 'KeyC',
|
||||
KeyD = 'KeyD',
|
||||
KeyE = 'KeyE',
|
||||
KeyF = 'KeyF',
|
||||
KeyG = 'KeyG',
|
||||
KeyH = 'KeyH',
|
||||
KeyI = 'KeyI',
|
||||
KeyJ = 'KeyJ',
|
||||
KeyK = 'KeyK',
|
||||
KeyL = 'KeyL',
|
||||
KeyM = 'KeyM',
|
||||
KeyN = 'KeyN',
|
||||
KeyO = 'KeyO',
|
||||
KeyP = 'KeyP',
|
||||
KeyQ = 'KeyQ',
|
||||
KeyR = 'KeyR',
|
||||
KeyS = 'KeyS',
|
||||
KeyT = 'KeyT',
|
||||
KeyU = 'KeyU',
|
||||
KeyV = 'KeyV',
|
||||
KeyW = 'KeyW',
|
||||
KeyX = 'KeyX',
|
||||
KeyY = 'KeyY',
|
||||
KeyZ = 'KeyZ',
|
||||
Minus = 'Minus',
|
||||
Period = 'Period',
|
||||
Quote = 'Quote',
|
||||
Semicolon = 'Semicolon',
|
||||
Slash = 'Slash',
|
||||
AltLeft = 'AltLeft',
|
||||
AltRight = 'AltRight',
|
||||
Backspace = 'Backspace',
|
||||
CapsLock = 'CapsLock',
|
||||
ContextMenu = 'ContextMenu',
|
||||
ControlLeft = 'ControlLeft',
|
||||
ControlRight = 'ControlRight',
|
||||
Enter = 'Enter',
|
||||
MetaLeft = 'MetaLeft',
|
||||
MetaRight = 'MetaRight',
|
||||
ShiftLeft = 'ShiftLeft',
|
||||
ShiftRight = 'ShiftRight',
|
||||
Space = 'Space',
|
||||
Tab = 'Tab',
|
||||
Delete = 'Delete',
|
||||
End = 'End',
|
||||
Home = 'Home',
|
||||
Insert = 'Insert',
|
||||
PageDown = 'PageDown',
|
||||
PageUp = 'PageUp',
|
||||
ArrowDown = 'ArrowDown',
|
||||
ArrowLeft = 'ArrowLeft',
|
||||
ArrowRight = 'ArrowRight',
|
||||
ArrowUp = 'ArrowUp',
|
||||
NumLock = 'NumLock',
|
||||
Numpad0 = 'Numpad0',
|
||||
Numpad1 = 'Numpad1',
|
||||
Numpad2 = 'Numpad2',
|
||||
Numpad3 = 'Numpad3',
|
||||
Numpad4 = 'Numpad4',
|
||||
Numpad5 = 'Numpad5',
|
||||
Numpad6 = 'Numpad6',
|
||||
Numpad7 = 'Numpad7',
|
||||
Numpad8 = 'Numpad8',
|
||||
Numpad9 = 'Numpad9',
|
||||
NumpadAdd = 'NumpadAdd',
|
||||
NumpadDecimal = 'NumpadDecimal',
|
||||
NumpadDivide = 'NumpadDivide',
|
||||
NumpadMultiply = 'NumpadMultiply',
|
||||
NumpadSubtract = 'NumpadSubtract',
|
||||
Escape = 'Escape',
|
||||
PrintScreen = 'PrintScreen',
|
||||
ScrollLock = 'ScrollLock',
|
||||
Pause = 'Pause',
|
||||
AudioVolumeDown = 'AudioVolumeDown',
|
||||
AudioVolumeMute = 'AudioVolumeMute',
|
||||
AudioVolumeUp = 'AudioVolumeUp',
|
||||
F1 = 'F1',
|
||||
F2 = 'F2',
|
||||
F3 = 'F3',
|
||||
F4 = 'F4',
|
||||
F5 = 'F5',
|
||||
F6 = 'F6',
|
||||
F7 = 'F7',
|
||||
F8 = 'F8',
|
||||
F9 = 'F9',
|
||||
F10 = 'F10',
|
||||
F11 = 'F11',
|
||||
F12 = 'F12',
|
||||
}
|
||||
|
||||
export enum KeyLabels {
|
||||
Backquote = '`',
|
||||
Backslash = '\\',
|
||||
BracketLeft = '[',
|
||||
BracketRight = ']',
|
||||
Comma = ',',
|
||||
Digit0 = '0',
|
||||
Digit1 = '1',
|
||||
Digit2 = '2',
|
||||
Digit3 = '3',
|
||||
Digit4 = '4',
|
||||
Digit5 = '5',
|
||||
Digit6 = '6',
|
||||
Digit7 = '7',
|
||||
Digit8 = '8',
|
||||
Digit9 = '9',
|
||||
Equal = '=',
|
||||
KeyA = 'A',
|
||||
KeyB = 'B',
|
||||
KeyC = 'C',
|
||||
KeyD = 'D',
|
||||
KeyE = 'E',
|
||||
KeyF = 'F',
|
||||
KeyG = 'G',
|
||||
KeyH = 'H',
|
||||
KeyI = 'I',
|
||||
KeyJ = 'J',
|
||||
KeyK = 'K',
|
||||
KeyL = 'L',
|
||||
KeyM = 'M',
|
||||
KeyN = 'N',
|
||||
KeyO = 'O',
|
||||
KeyP = 'P',
|
||||
KeyQ = 'Q',
|
||||
KeyR = 'R',
|
||||
KeyS = 'S',
|
||||
KeyT = 'T',
|
||||
KeyU = 'U',
|
||||
KeyV = 'V',
|
||||
KeyW = 'W',
|
||||
KeyX = 'X',
|
||||
KeyY = 'Y',
|
||||
KeyZ = 'Z',
|
||||
Minus = '-',
|
||||
Period = '.',
|
||||
Quote = "'",
|
||||
Semicolon = ';',
|
||||
Slash = '/',
|
||||
AltLeft = 'Alt',
|
||||
AltRight = 'Alt (Right)',
|
||||
Backspace = 'Backspace',
|
||||
CapsLock = 'Caps Lock',
|
||||
ContextMenu = 'Context Menu',
|
||||
ControlLeft = 'Ctrl',
|
||||
ControlRight = 'Ctrl (Right)',
|
||||
Enter = 'Enter',
|
||||
MetaLeft = 'Meta',
|
||||
MetaRight = 'Meta (Right)',
|
||||
ShiftLeft = 'Shift',
|
||||
ShiftRight = 'Shift (Right)',
|
||||
Space = 'Space',
|
||||
Tab = 'Tab',
|
||||
Delete = 'Delete',
|
||||
End = 'End',
|
||||
Home = 'Home',
|
||||
Insert = 'Insert',
|
||||
PageDown = 'Page Down',
|
||||
PageUp = 'Page Up',
|
||||
ArrowDown = '↓',
|
||||
ArrowLeft = '←',
|
||||
ArrowRight = '→',
|
||||
ArrowUp = '↑',
|
||||
NumLock = 'Num Lock',
|
||||
Numpad0 = 'Numpad 0',
|
||||
Numpad1 = 'Numpad 1',
|
||||
Numpad2 = 'Numpad 2',
|
||||
Numpad3 = 'Numpad 3',
|
||||
Numpad4 = 'Numpad 4',
|
||||
Numpad5 = 'Numpad 5',
|
||||
Numpad6 = 'Numpad 6',
|
||||
Numpad7 = 'Numpad 7',
|
||||
Numpad8 = 'Numpad 8',
|
||||
Numpad9 = 'Numpad 9',
|
||||
NumpadAdd = 'Numpad +',
|
||||
NumpadDecimal = 'Numpad .',
|
||||
NumpadDivide = 'Numpad /',
|
||||
NumpadMultiply = 'Numpad *',
|
||||
NumpadSubtract = 'Numpad -',
|
||||
Escape = 'Esc',
|
||||
PrintScreen = 'Print Screen',
|
||||
ScrollLock = 'Scroll Lock',
|
||||
Pause = 'Pause',
|
||||
AudioVolumeDown = 'Volume Down',
|
||||
AudioVolumeMute = 'Volume Mute',
|
||||
AudioVolumeUp = 'Volume Up',
|
||||
F1 = 'F1',
|
||||
F2 = 'F2',
|
||||
F3 = 'F3',
|
||||
F4 = 'F4',
|
||||
F5 = 'F5',
|
||||
F6 = 'F6',
|
||||
F7 = 'F7',
|
||||
F8 = 'F8',
|
||||
F9 = 'F9',
|
||||
F10 = 'F10',
|
||||
F11 = 'F11',
|
||||
F12 = 'F12',
|
||||
export enum KeyValues {
|
||||
Backquote = 'Backquote',
|
||||
Backslash = 'Backslash',
|
||||
BracketLeft = 'BracketLeft',
|
||||
BracketRight = 'BracketRight',
|
||||
Comma = 'Comma',
|
||||
Digit0 = 'Digit0',
|
||||
Digit1 = 'Digit1',
|
||||
Digit2 = 'Digit2',
|
||||
Digit3 = 'Digit3',
|
||||
Digit4 = 'Digit4',
|
||||
Digit5 = 'Digit5',
|
||||
Digit6 = 'Digit6',
|
||||
Digit7 = 'Digit7',
|
||||
Digit8 = 'Digit8',
|
||||
Digit9 = 'Digit9',
|
||||
Equal = 'Equal',
|
||||
KeyA = 'KeyA',
|
||||
KeyB = 'KeyB',
|
||||
KeyC = 'KeyC',
|
||||
KeyD = 'KeyD',
|
||||
KeyE = 'KeyE',
|
||||
KeyF = 'KeyF',
|
||||
KeyG = 'KeyG',
|
||||
KeyH = 'KeyH',
|
||||
KeyI = 'KeyI',
|
||||
KeyJ = 'KeyJ',
|
||||
KeyK = 'KeyK',
|
||||
KeyL = 'KeyL',
|
||||
KeyM = 'KeyM',
|
||||
KeyN = 'KeyN',
|
||||
KeyO = 'KeyO',
|
||||
KeyP = 'KeyP',
|
||||
KeyQ = 'KeyQ',
|
||||
KeyR = 'KeyR',
|
||||
KeyS = 'KeyS',
|
||||
KeyT = 'KeyT',
|
||||
KeyU = 'KeyU',
|
||||
KeyV = 'KeyV',
|
||||
KeyW = 'KeyW',
|
||||
KeyX = 'KeyX',
|
||||
KeyY = 'KeyY',
|
||||
KeyZ = 'KeyZ',
|
||||
Minus = 'Minus',
|
||||
Period = 'Period',
|
||||
Quote = 'Quote',
|
||||
Semicolon = 'Semicolon',
|
||||
Slash = 'Slash',
|
||||
AltLeft = 'AltLeft',
|
||||
AltRight = 'AltRight',
|
||||
Backspace = 'Backspace',
|
||||
CapsLock = 'CapsLock',
|
||||
ContextMenu = 'ContextMenu',
|
||||
ControlLeft = 'ControlLeft',
|
||||
ControlRight = 'ControlRight',
|
||||
Enter = 'Enter',
|
||||
MetaLeft = 'MetaLeft',
|
||||
MetaRight = 'MetaRight',
|
||||
ShiftLeft = 'ShiftLeft',
|
||||
ShiftRight = 'ShiftRight',
|
||||
Space = 'Space',
|
||||
Tab = 'Tab',
|
||||
Delete = 'Delete',
|
||||
End = 'End',
|
||||
Home = 'Home',
|
||||
Insert = 'Insert',
|
||||
PageDown = 'PageDown',
|
||||
PageUp = 'PageUp',
|
||||
ArrowDown = 'ArrowDown',
|
||||
ArrowLeft = 'ArrowLeft',
|
||||
ArrowRight = 'ArrowRight',
|
||||
ArrowUp = 'ArrowUp',
|
||||
NumLock = 'NumLock',
|
||||
Numpad0 = 'Numpad0',
|
||||
Numpad1 = 'Numpad1',
|
||||
Numpad2 = 'Numpad2',
|
||||
Numpad3 = 'Numpad3',
|
||||
Numpad4 = 'Numpad4',
|
||||
Numpad5 = 'Numpad5',
|
||||
Numpad6 = 'Numpad6',
|
||||
Numpad7 = 'Numpad7',
|
||||
Numpad8 = 'Numpad8',
|
||||
Numpad9 = 'Numpad9',
|
||||
NumpadAdd = 'NumpadAdd',
|
||||
NumpadDecimal = 'NumpadDecimal',
|
||||
NumpadDivide = 'NumpadDivide',
|
||||
NumpadMultiply = 'NumpadMultiply',
|
||||
NumpadSubtract = 'NumpadSubtract',
|
||||
Escape = 'Escape',
|
||||
PrintScreen = 'PrintScreen',
|
||||
ScrollLock = 'ScrollLock',
|
||||
Pause = 'Pause',
|
||||
AudioVolumeDown = 'AudioVolumeDown',
|
||||
AudioVolumeMute = 'AudioVolumeMute',
|
||||
AudioVolumeUp = 'AudioVolumeUp',
|
||||
F1 = 'F1',
|
||||
F2 = 'F2',
|
||||
F3 = 'F3',
|
||||
F4 = 'F4',
|
||||
F5 = 'F5',
|
||||
F6 = 'F6',
|
||||
F7 = 'F7',
|
||||
F8 = 'F8',
|
||||
F9 = 'F9',
|
||||
F10 = 'F10',
|
||||
F11 = 'F11',
|
||||
F12 = 'F12',
|
||||
}
|
||||
|
||||
export enum KeyLabels {
|
||||
Backquote = '`',
|
||||
Backslash = '\\',
|
||||
BracketLeft = '[',
|
||||
BracketRight = ']',
|
||||
Comma = ',',
|
||||
Digit0 = '0',
|
||||
Digit1 = '1',
|
||||
Digit2 = '2',
|
||||
Digit3 = '3',
|
||||
Digit4 = '4',
|
||||
Digit5 = '5',
|
||||
Digit6 = '6',
|
||||
Digit7 = '7',
|
||||
Digit8 = '8',
|
||||
Digit9 = '9',
|
||||
Equal = '=',
|
||||
KeyA = 'A',
|
||||
KeyB = 'B',
|
||||
KeyC = 'C',
|
||||
KeyD = 'D',
|
||||
KeyE = 'E',
|
||||
KeyF = 'F',
|
||||
KeyG = 'G',
|
||||
KeyH = 'H',
|
||||
KeyI = 'I',
|
||||
KeyJ = 'J',
|
||||
KeyK = 'K',
|
||||
KeyL = 'L',
|
||||
KeyM = 'M',
|
||||
KeyN = 'N',
|
||||
KeyO = 'O',
|
||||
KeyP = 'P',
|
||||
KeyQ = 'Q',
|
||||
KeyR = 'R',
|
||||
KeyS = 'S',
|
||||
KeyT = 'T',
|
||||
KeyU = 'U',
|
||||
KeyV = 'V',
|
||||
KeyW = 'W',
|
||||
KeyX = 'X',
|
||||
KeyY = 'Y',
|
||||
KeyZ = 'Z',
|
||||
Minus = '-',
|
||||
Period = '.',
|
||||
Quote = "'",
|
||||
Semicolon = ';',
|
||||
Slash = '/',
|
||||
AltLeft = 'Alt',
|
||||
AltRight = 'Alt (Right)',
|
||||
Backspace = 'Backspace',
|
||||
CapsLock = 'Caps Lock',
|
||||
ContextMenu = 'Context Menu',
|
||||
ControlLeft = 'Ctrl',
|
||||
ControlRight = 'Ctrl (Right)',
|
||||
Enter = 'Enter',
|
||||
MetaLeft = 'Meta',
|
||||
MetaRight = 'Meta (Right)',
|
||||
ShiftLeft = 'Shift',
|
||||
ShiftRight = 'Shift (Right)',
|
||||
Space = 'Space',
|
||||
Tab = 'Tab',
|
||||
Delete = 'Delete',
|
||||
End = 'End',
|
||||
Home = 'Home',
|
||||
Insert = 'Insert',
|
||||
PageDown = 'Page Down',
|
||||
PageUp = 'Page Up',
|
||||
ArrowDown = '↓',
|
||||
ArrowLeft = '←',
|
||||
ArrowRight = '→',
|
||||
ArrowUp = '↑',
|
||||
NumLock = 'Num Lock',
|
||||
Numpad0 = 'Numpad 0',
|
||||
Numpad1 = 'Numpad 1',
|
||||
Numpad2 = 'Numpad 2',
|
||||
Numpad3 = 'Numpad 3',
|
||||
Numpad4 = 'Numpad 4',
|
||||
Numpad5 = 'Numpad 5',
|
||||
Numpad6 = 'Numpad 6',
|
||||
Numpad7 = 'Numpad 7',
|
||||
Numpad8 = 'Numpad 8',
|
||||
Numpad9 = 'Numpad 9',
|
||||
NumpadAdd = 'Numpad +',
|
||||
NumpadDecimal = 'Numpad .',
|
||||
NumpadDivide = 'Numpad /',
|
||||
NumpadMultiply = 'Numpad *',
|
||||
NumpadSubtract = 'Numpad -',
|
||||
Escape = 'Esc',
|
||||
PrintScreen = 'Print Screen',
|
||||
ScrollLock = 'Scroll Lock',
|
||||
Pause = 'Pause',
|
||||
AudioVolumeDown = 'Volume Down',
|
||||
AudioVolumeMute = 'Volume Mute',
|
||||
AudioVolumeUp = 'Volume Up',
|
||||
F1 = 'F1',
|
||||
F2 = 'F2',
|
||||
F3 = 'F3',
|
||||
F4 = 'F4',
|
||||
F5 = 'F5',
|
||||
F6 = 'F6',
|
||||
F7 = 'F7',
|
||||
F8 = 'F8',
|
||||
F9 = 'F9',
|
||||
F10 = 'F10',
|
||||
F11 = 'F11',
|
||||
F12 = 'F12',
|
||||
}
|
232
types/types.ts
232
types/types.ts
|
@ -1,116 +1,116 @@
|
|||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export enum ContentType {
|
||||
Text = "text",
|
||||
Image = "image",
|
||||
File = "file",
|
||||
Link = "link",
|
||||
Color = "color",
|
||||
Code = "code",
|
||||
}
|
||||
|
||||
export class HistoryItem {
|
||||
id: string;
|
||||
source: string;
|
||||
source_icon?: string;
|
||||
content_type: ContentType;
|
||||
content: string;
|
||||
favicon?: string;
|
||||
timestamp: Date;
|
||||
language?: string;
|
||||
|
||||
constructor(
|
||||
source: string,
|
||||
content_type: ContentType,
|
||||
content: string,
|
||||
favicon?: string,
|
||||
source_icon?: string,
|
||||
language?: string
|
||||
) {
|
||||
this.id = uuidv4();
|
||||
this.source = source;
|
||||
this.source_icon = source_icon;
|
||||
this.content_type = content_type;
|
||||
this.content = content;
|
||||
this.favicon = favicon;
|
||||
this.timestamp = new Date();
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
toRow(): [
|
||||
string,
|
||||
string,
|
||||
string | undefined,
|
||||
string,
|
||||
string,
|
||||
string | undefined,
|
||||
Date,
|
||||
string | undefined
|
||||
] {
|
||||
return [
|
||||
this.id,
|
||||
this.source,
|
||||
this.source_icon,
|
||||
this.content_type,
|
||||
this.content,
|
||||
this.favicon,
|
||||
this.timestamp,
|
||||
this.language,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface InfoText {
|
||||
source: string;
|
||||
content_type: ContentType.Text;
|
||||
characters: number;
|
||||
words: number;
|
||||
copied: Date;
|
||||
}
|
||||
|
||||
export interface InfoImage {
|
||||
source: string;
|
||||
content_type: ContentType.Image;
|
||||
dimensions: string;
|
||||
size: number;
|
||||
copied: Date;
|
||||
}
|
||||
|
||||
export interface InfoFile {
|
||||
source: string;
|
||||
content_type: ContentType.File;
|
||||
path: string;
|
||||
filesize: number;
|
||||
copied: Date;
|
||||
}
|
||||
|
||||
export interface InfoLink {
|
||||
source: string;
|
||||
content_type: ContentType.Link;
|
||||
title?: string;
|
||||
url: string;
|
||||
characters: number;
|
||||
copied: Date;
|
||||
}
|
||||
|
||||
export interface InfoColor {
|
||||
source: string;
|
||||
content_type: ContentType.Color;
|
||||
hex: string;
|
||||
rgb: string;
|
||||
hsl: string;
|
||||
copied: Date;
|
||||
}
|
||||
|
||||
export interface InfoCode {
|
||||
source: string;
|
||||
content_type: ContentType.Code;
|
||||
language: string;
|
||||
lines: number;
|
||||
copied: Date;
|
||||
}
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export enum ContentType {
|
||||
Text = "text",
|
||||
Image = "image",
|
||||
File = "file",
|
||||
Link = "link",
|
||||
Color = "color",
|
||||
Code = "code",
|
||||
}
|
||||
|
||||
export class HistoryItem {
|
||||
id: string;
|
||||
source: string;
|
||||
source_icon?: string;
|
||||
content_type: ContentType;
|
||||
content: string;
|
||||
favicon?: string;
|
||||
timestamp: Date;
|
||||
language?: string;
|
||||
|
||||
constructor(
|
||||
source: string,
|
||||
content_type: ContentType,
|
||||
content: string,
|
||||
favicon?: string,
|
||||
source_icon?: string,
|
||||
language?: string
|
||||
) {
|
||||
this.id = uuidv4();
|
||||
this.source = source;
|
||||
this.source_icon = source_icon;
|
||||
this.content_type = content_type;
|
||||
this.content = content;
|
||||
this.favicon = favicon;
|
||||
this.timestamp = new Date();
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
toRow(): [
|
||||
string,
|
||||
string,
|
||||
string | undefined,
|
||||
string,
|
||||
string,
|
||||
string | undefined,
|
||||
Date,
|
||||
string | undefined
|
||||
] {
|
||||
return [
|
||||
this.id,
|
||||
this.source,
|
||||
this.source_icon,
|
||||
this.content_type,
|
||||
this.content,
|
||||
this.favicon,
|
||||
this.timestamp,
|
||||
this.language,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface InfoText {
|
||||
source: string;
|
||||
content_type: ContentType.Text;
|
||||
characters: number;
|
||||
words: number;
|
||||
copied: Date;
|
||||
}
|
||||
|
||||
export interface InfoImage {
|
||||
source: string;
|
||||
content_type: ContentType.Image;
|
||||
dimensions: string;
|
||||
size: number;
|
||||
copied: Date;
|
||||
}
|
||||
|
||||
export interface InfoFile {
|
||||
source: string;
|
||||
content_type: ContentType.File;
|
||||
path: string;
|
||||
filesize: number;
|
||||
copied: Date;
|
||||
}
|
||||
|
||||
export interface InfoLink {
|
||||
source: string;
|
||||
content_type: ContentType.Link;
|
||||
title?: string;
|
||||
url: string;
|
||||
characters: number;
|
||||
copied: Date;
|
||||
}
|
||||
|
||||
export interface InfoColor {
|
||||
source: string;
|
||||
content_type: ContentType.Color;
|
||||
hex: string;
|
||||
rgb: string;
|
||||
hsl: string;
|
||||
copied: Date;
|
||||
}
|
||||
|
||||
export interface InfoCode {
|
||||
source: string;
|
||||
content_type: ContentType.Code;
|
||||
language: string;
|
||||
lines: number;
|
||||
copied: Date;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue