diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5ace4600a1..b7f03146ca 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,3 +4,8 @@ updates: directory: "/" schedule: interval: "weekly" + + - package-ecosystem: "pip" + directory: "/tools/windows/requirements" + schedule: + interval: "weekly" diff --git a/.github/patch/CMakeLists.txt.patch b/.github/patch/CMakeLists.txt.patch deleted file mode 100644 index 801b2caf0e..0000000000 --- a/.github/patch/CMakeLists.txt.patch +++ /dev/null @@ -1,33 +0,0 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 1010923e..0b17700f 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -80,24 +80,24 @@ if(MSVC) - if(CMAKE_CL_64) - # Paths for 64-bit windows builds - set(OPENSSL_PATH "C:/dev/OpenSSL-Win64" CACHE PATH "OpenSSL Path") -- set(QT5_PATH "C:/dev/Qt/5.12.12/msvc2017_64" CACHE PATH "Qt5 Path") -+ set(QT5_PATH "C:/Qt/5.15.2/msvc2019_64" CACHE PATH "Qt5 Path") - - # Choose between SQLCipher or SQLite, depending whether - # -Dsqlcipher=on is passed on the command line - if(sqlcipher) -- set(SQLITE3_PATH "C:/git_repos/SQLCipher-Win64" CACHE PATH "SQLCipher Path") -+ set(SQLITE3_PATH "C:/dev/SQLCipher-Win64" CACHE PATH "SQLCipher Path") - else() - set(SQLITE3_PATH "C:/dev/SQLite-Win64" CACHE PATH "SQLite Path") - endif() - else() - # Paths for 32-bit windows builds - set(OPENSSL_PATH "C:/dev/OpenSSL-Win32" CACHE PATH "OpenSSL Path") -- set(QT5_PATH "C:/dev/Qt/5.12.12/msvc2017" CACHE PATH "Qt5 Path") -+ set(QT5_PATH "C:/Qt/5.15.2/msvc2019" CACHE PATH "Qt5 Path") - - # Choose between SQLCipher or SQLite, depending whether - # -Dsqlcipher=on is passed on the command line - if(sqlcipher) -- set(SQLITE3_PATH "C:/git_repos/SQLCipher-Win32" CACHE PATH "SQLCipher Path") -+ set(SQLITE3_PATH "C:/dev/SQLCipher-Win32" CACHE PATH "SQLCipher Path") - else() - set(SQLITE3_PATH "C:/dev/SQLite-Win32" CACHE PATH "SQLite Path") - endif() diff --git a/.github/patch/README.md b/.github/patch/README.md deleted file mode 100644 index bbd2117aca..0000000000 --- a/.github/patch/README.md +++ /dev/null @@ -1 +0,0 @@ -A collection of patch files used when a need change to a file in the source tree is required for GitHub Actions to work. \ No newline at end of file diff --git a/.github/patch/product.wxs.patch b/.github/patch/product.wxs.patch deleted file mode 100644 index abb0e62ad4..0000000000 --- a/.github/patch/product.wxs.patch +++ /dev/null @@ -1,58 +0,0 @@ -diff --git a/installer/windows/product.wxs b/installer/windows/product.wxs -index c040591a..46d57881 100644 ---- a/installer/windows/product.wxs -+++ b/installer/windows/product.wxs -@@ -63,7 +63,11 @@ - - - -- -+ -+ -+ -+ -+ - - - -@@ -84,6 +88,9 @@ - - - -+ -+ -+ - - - -@@ -149,8 +156,11 @@ - - - -- -+ -+ -+ - - - -@@ -169,6 +179,7 @@ - - - -+ - - - -@@ -186,6 +197,9 @@ - - - -+ -+ -+ - - - diff --git a/.github/patch/translations.wxs.patch b/.github/patch/translations.wxs.patch deleted file mode 100644 index 1360929417..0000000000 --- a/.github/patch/translations.wxs.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff --git a/installer/windows/translations.wxs b/installer/windows/translations.wxs -index f842e05b..0743a202 100644 ---- a/installer/windows/translations.wxs -+++ b/installer/windows/translations.wxs -@@ -97,7 +97,7 @@ - - - -- -+ - - - -@@ -197,7 +197,7 @@ - - - -- -+ - - - diff --git a/.github/patch/variables.wxi.patch b/.github/patch/variables.wxi.patch deleted file mode 100644 index 7077f15f43..0000000000 --- a/.github/patch/variables.wxi.patch +++ /dev/null @@ -1,43 +0,0 @@ -diff --git a/installer/windows/variables.wxi b/installer/windows/variables.wxi -index fbedf0c3..34a65831 100644 ---- a/installer/windows/variables.wxi -+++ b/installer/windows/variables.wxi -@@ -40,8 +40,8 @@ - Visual Studio 2017. The build "ARCH" will be set automatically. - --> - -- -- -+ -+ - - - -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -+ -+ -+ -+ -+ -+ -+ - - diff --git a/.github/workflows/build-appimage.yml b/.github/workflows/build-appimage.yml deleted file mode 100644 index ae845af474..0000000000 --- a/.github/workflows/build-appimage.yml +++ /dev/null @@ -1,80 +0,0 @@ -# KEEP THE RUNNER OS AS LOW AS POSSIBLE. -# If built on the latest OS, it may not run on previous OS versions. -# Related: https://docs.appimage.org/reference/best-practices.html#binaries-compiled-on-old-enough-base-system - -name: Build - AppImage - -on: - workflow_call: - -jobs: - build: - name: ${{ matrix.os }} - SQLCipher ${{ matrix.sqlcipher }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-20.04] - sqlcipher: ["0", "1"] - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - sudo apt update - sudo apt install libqcustomplot-dev libqscintilla2-qt5-dev libqt5svg5 qttools5-dev - - - if: matrix.sqlcipher == 0 - name: Build SQLite - run: | - TARBALL=$(curl -s https://sqlite.org/download.html | awk '// {print}' | grep 'sqlite-autoconf' | cut -d ',' -f 3) - SHA3=$(curl -s https://sqlite.org/download.html | awk '// {print}' | grep 'sqlite-autoconf' | cut -d ',' -f 5) - curl -LsS -o sqlite.tar.gz https://sqlite.org/${TARBALL} - VERIFY=$(openssl dgst -sha3-256 sqlite.tar.gz | cut -d ' ' -f 2) - if [ "$SHA3" != "$VERIFY" ]; then exit 1 ; fi - tar -xzf sqlite.tar.gz && cd sqlite-autoconf-* - - CPPFLAGS="-DSQLITE_ENABLE_COLUMN_METADATA=1 -DSQLITE_MAX_VARIABLE_NUMBER=250000 -DSQLITE_ENABLE_RTREE=1 -DSQLITE_ENABLE_GEOPOLY=1 -DSQLITE_ENABLE_FTS3=1 -DSQLITE_ENABLE_FTS3_PARENTHESIS=1 -DSQLITE_ENABLE_FTS5=1 -DSQLITE_ENABLE_STAT4=1 -DSQLITE_ENABLE_JSON1=1 -DSQLITE_SOUNDEX=1 -DSQLITE_ENABLE_MATH_FUNCTIONS=1 -DSQLITE_MAX_ATTACHED=125 -DSQLITE_ENABLE_MEMORY_MANAGEMENT=1 -DSQLITE_ENABLE_SNAPSHOT=1" ./configure --enable-shared=no - make -j2 && sudo make install -j2 - - - if: matrix.sqlcipher == 1 - name: Build SQLCipher - run: | - sudo apt install libssl-dev tcl - git clone https://github.com/sqlcipher/sqlcipher && cd sqlcipher && git checkout v4.6.1 - ./configure --enable-tempstore=yes --with-crypto-lib=openssl --enable-load-extension --disable-tcl CFLAGS="-DSQLCIPHER_CRYPTO_OPENSSL -DSQLITE_ENABLE_COLUMN_METADATA -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_GEOPOLY -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_MEMORY_MANAGEMENT=1 -DSQLITE_ENABLE_RTREE -DSQLITE_ENABLE_SNAPSHOT=1 -DSQLITE_ENABLE_STAT4 -DSQLITE_HAS_CODEC -DSQLITE_SOUNDEX" - make -j2 && sudo make install -j2 - - - name: Configure build - run: | - mkdir appbuild && mkdir appdir && cd appbuild - cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX:PATH=../appdir/usr -Wno-dev -DFORCE_INTERNAL_QSCINTILLA=ON -Dsqlcipher=${{ matrix.sqlcipher }} .. - - - name: Build - working-directory: ./appbuild - run: make install -j2 - - - name: Build AppImage - run: | - wget -c -nv "https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20240109-1/linuxdeploy-x86_64.AppImage" - wget -c -nv "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/1-alpha-20240109-1/linuxdeploy-plugin-qt-x86_64.AppImage" - chmod a+x linuxdeploy-x86_64.AppImage linuxdeploy-plugin-qt-x86_64.AppImage - export VERSION=$(printf "dev-`git -C . rev-parse --short HEAD`") - LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib" ./linuxdeploy-x86_64.AppImage --appdir=appdir --desktop-file=appdir/usr/share/applications/sqlitebrowser.desktop --plugin qt --output appimage - - - name: Rename a file - run: | - for i in DB_Browser_for_SQLite*; do mv "$i" "${i//_/.}"; done - if [ "${{ matrix.sqlcipher }}" = "1" ]; then - export FILE=$(ls DB.Browser.for.SQLite*.AppImage) - export FILE=${FILE/SQLite/SQLCipher} - mv DB.Browser.for.SQLite*.AppImage $FILE - fi - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: build-artifacts-${{ matrix.os }}-${{ matrix.sqlcipher }} - path: DB.Browser.for.*.AppImage - retention-days: 1 diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml deleted file mode 100644 index 7fc0d552f5..0000000000 --- a/.github/workflows/build-macos.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: Build - macOS - -on: - workflow_call: - inputs: - NIGHTLY: - default: false - type: boolean - -jobs: - build: - name: ${{ matrix.os }} - SQLCipher ${{ matrix.sqlcipher }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [macos-14] - sqlcipher: ["0", "1"] - env: - MACOSX_DEPLOYMENT_TARGET: 10.13 - steps: - - name: Checkout - uses: actions/checkout@v4 - - # Uninstall Mono, which is included by default in GitHub-hosted macOS runners, - # as it interferes with referencing our own compiled SQLite libraries. - - name: Uninstall Mono - run: | - sudo rm -rfv /Library/Frameworks/Mono.framework - sudo pkgutil --forget com.xamarin.mono-MDK.pkg - sudo rm /etc/paths.d/mono-commands - - - name: Install dependencies - run: | - brew update - brew tap sqlitebrowser/tap - brew unlink openssl@3 - brew install sqlb-openssl@3 - brew install sqlb-qt@5 sqlb-sqlcipher sqlb-sqlite ninja - npm install -g appdmg - - - name: Configure build - run: | - if [ "${{ inputs.NIGHTLY }}" = "true" ]; then - if [ "${{ matrix.sqlcipher }}" = "1" ]; then - sed -i "" 's/"DB Browser for SQLite"/"DB Browser for SQLCipher Nightly"/' config/platform_apple.cmake - else - sed -i "" 's/"DB Browser for SQLite"/"DB Browser for SQLite Nightly"/' config/platform_apple.cmake - fi - else - if [ "${{ matrix.sqlcipher }}" = "1" ]; then - sed -i "" 's/"DB Browser for SQLite"/"DB Browser for SQLCipher-dev-'$(git rev-parse --short --verify HEAD)'"/' config/platform_apple.cmake - else - sed -i "" 's/"DB Browser for SQLite"/"DB Browser for SQLite-dev-'$(git rev-parse --short --verify HEAD)'"/' config/platform_apple.cmake - fi - fi - - mkdir build && cd build - cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_STANDARD=14 -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" -DcustomTap=1 -DENABLE_TESTING=ON -Dsqlcipher=${{ matrix.sqlcipher }} .. - - - name: Build - working-directory: ./build - run: ninja - - - name: Tests - working-directory: ./build - run: ninja test - - - name: Build Extension - run: clang -I /opt/homebrew/opt/sqlb-sqlite/include -L /opt/homebrew/opt/sqlb-sqlite/lib -fno-common -dynamiclib src/extensions/extension-formats.c - - - if: github.event_name != 'pull_request' - name: Notarization - id: notarization - run: chmod +x ./installer/macos/notarize.sh && ./installer/macos/notarize.sh - env: - APPLE_ID: ${{ secrets.MACOS_CODESIGN_APPLE_ID }} - APPLE_PW: ${{ secrets.MACOS_CODESIGN_APPLE_PW }} - DEV_ID: ${{ secrets.MACOS_CODESIGN_DEV_ID }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - KEYCHAIN_PW: ${{ secrets.MACOS_CODESIGN_KEYCHAIN_PW }} - P12: ${{ secrets.MACOS_CODESIGN_P12 }} - P12_PW: ${{ secrets.MACOS_CODESIGN_P12_PW }} - NIGHTLY: ${{ inputs.NIGHTLY }} - SQLCIPHER: ${{ matrix.sqlcipher }} - TEAM_ID: ${{ secrets.MACOS_CODESIGN_TEAM_ID }} - - - if: steps.notarization.conclusion != 'skipped' - name: Clear Keychain - run: security delete-keychain $RUNNER_TEMP/app-signing.keychain-db - continue-on-error: true - - - if: github.event_name != 'pull_request' - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: build-artifacts-${{ matrix.os }}-${{ matrix.sqlcipher }} - path: DB.Browser.for.*.dmg diff --git a/.github/workflows/build-ubuntu.yml b/.github/workflows/build-ubuntu.yml deleted file mode 100644 index 5cdaec181e..0000000000 --- a/.github/workflows/build-ubuntu.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Build - Ubuntu - -on: - release: - types: [created] - workflow_call: - -jobs: - build: - name: ${{ matrix.os }} - SQLCipher ${{ matrix.sqlcipher }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-20.04] - sqlcipher: ["0", "1"] - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install and cache dependencies - uses: awalsh128/cache-apt-pkgs-action@v1.4.2 - with: - packages: libqcustomplot-dev libqscintilla2-qt5-dev libqt5svg5 libsqlcipher-dev libsqlite3-dev qttools5-dev - - - name: Configure CMake - run: | - cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX=${PWD}/install \ - -DCPACK_PACKAGE_DIRECTORY=${PWD}/package \ - -DENABLE_TESTING=ON \ - -Dsqlcipher=${{ matrix.sqlcipher }} - - - name: Run make - run: cmake --build build --config Release -j --target install - - - name: Run tests - run: ctest -V -C Release --test-dir build - - - if: github.event_name == 'release' - name: Package - run: | - cmake --build build --config Release -j --target package - cmake -E remove_directory package/_CPack_Packages - - - if: github.event_name == 'release' - name: Upload package - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ github.event.release.tag_name }} - UPLOAD_URL: ${{ github.event.release.upload_url }} - run: | - set the env var TAG: - $GITHUB_API_URL/repos/$GITHUB_REPOSITORY/releases/tags/$TAG | jq -r .upload_url) - UPLOAD_URL=${UPLOAD_URL%\{*} # remove "{name,label}" suffix - for pkg in package/*.*; do - NAME=$(basename $pkg) - MIME=$(file --mime-type $pkg|cut -d ' ' -f2) - curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token $GITHUB_TOKEN" -H "Content-Type: $MIME" --data-binary @$pkg $UPLOAD_URL?name=$NAME - done diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml deleted file mode 100644 index ed676446b9..0000000000 --- a/.github/workflows/build-windows.yml +++ /dev/null @@ -1,218 +0,0 @@ -name: Build - Windows - -on: - workflow_call: - inputs: - NIGHTLY: - default: false - type: boolean - -jobs: - build: - name: Build - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [windows-2019] - arch: ["Win32", "Win64"] - env: - GH_TOKEN: ${{ github.token }} - OPENSSL_VERSION: 1.1.1.2100 - QT_VERSION: 5.15.2 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install OpenSSL - run: | - if ("${{ matrix.arch }}" -eq "Win32") { - choco install openssl --x86 --version=${{ env.OPENSSL_VERSION}} - } else { - choco install openssl --version=${{ env.OPENSSL_VERSION}} - } - - - name: Install Ninja (Windows) - run: choco install -y --no-progress ninja - - # When building SQLCipher, if we specify a path to OpenSSL and - # there are spaces in the path, an error will occur, so to - # avoid this, create the symlink. - - name: Create OpenSSL symlink - run: | - mkdir C:\dev - if ("${{ matrix.arch }}" -eq "Win32") { - New-Item -Path "C:\dev\OpenSSL-${{ matrix.arch }}" -ItemType SymbolicLink -Value "C:\Program Files (x86)\OpenSSL-Win32\" - } else { - New-Item -Path "C:\dev\OpenSSL-${{ matrix.arch }}" -ItemType SymbolicLink -Value "C:\Program Files\OpenSSL" - } - - - name: Install Qt - uses: jurplel/install-qt-action@v4 - with: - arch: ${{ matrix.arch == 'Win32' && 'win32_msvc2019' || matrix.arch == 'Win64' && 'win64_msvc2019_64'}} - cache: true - cache-key-prefix: "cache" - version: ${{ env.QT_VERSION }} - - - name: Download 'nalgeon/sqlean' - run: | - if ("${{ matrix.arch }}" -eq "Win32") { - gh release download --pattern "sqlean-win-x86.zip" --repo "nalgeon/sqlean" - Expand-Archive -Path sqlean-win-x86.zip -DestinationPath .\sqlean - } else { - gh release download --pattern "sqlean-win-x64.zip" --repo "nalgeon/sqlean" - Expand-Archive -Path sqlean-win-x64.zip -DestinationPath .\sqlean - } - - - name: Setup MSVC - uses: ilammy/msvc-dev-cmd@v1 - with: - arch: ${{ matrix.arch == 'Win32' && 'amd64_x86' || matrix.arch == 'Win64' && 'amd64'}} - - - name: Install SQLite - run: | - $htmlContent = Invoke-WebRequest -Uri "https://sqlite.org/download.html" | Select-Object -ExpandProperty Content - $regex = [regex]::new('PRODUCT,(\d+\.\d+\.\d+),(\d+/sqlite-amalgamation-\d+\.zip),\d+,(.+)') - $match = $regex.Match($htmlContent) - $relativeUrl = $match.Groups[2].Value - $downloadLink = "https://sqlite.org/$relativeUrl" - Invoke-WebRequest -Uri $downloadLink -OutFile 'sqlite.zip' - Expand-Archive -Path sqlite.zip -DestinationPath C:\dev\ - Move-Item -Path C:\dev\sqlite-amalgamation-* C:\dev\SQLite-${{ matrix.arch }} - cd C:\dev\SQLite-${{ matrix.arch }} - cl sqlite3.c -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_STAT4 -DSQLITE_SOUNDEX -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_GEOPOLY -DSQLITE_ENABLE_RTREE -DSQLITE_ENABLE_MATH_FUNCTIONS -DSQLITE_MAX_ATTACHED=125 -DSQLITE_API="__declspec(dllexport)" -link -dll -out:sqlite3.dll - - - name: Install SQLite Extensions - run: | - cp .\src\extensions\extension-formats.c C:\dev\SQLite-${{ matrix.arch}}\ - cp .\src\extensions\extension-formats.def C:\dev\SQLite-${{ matrix.arch}}\ - cp .\src\extensions\extension-functions.c C:\dev\SQLite-${{ matrix.arch}}\ - cp .\src\extensions\extension-functions.def C:\dev\SQLite-${{ matrix.arch}}\ - cd C:\dev\SQLite-${{ matrix.arch}}\ - cl /MD extension-formats.c -link -dll -def:extension-formats.def -out:formats.dll - cl /MD extension-functions.c -link -dll -def:extension-functions.def -out:math.dll - # FIXME: Disable building the 'fileio' extension for now (#3488) - # If this issue is resolved, be sure to delete the related patch for WiX - # curl -L -o fileio.c "https://sqlite.org/src/raw?filename=ext/misc/fileio.c&ci=trunk" - # curl -L -o test_windirent.c "https://sqlite.org/src/raw?filename=src/test_windirent.c&ci=trunk" - # curl -L -o test_windirent.h "https://sqlite.org/src/raw?filename=src/test_windirent.h&ci=trunk" - # cl /MD fileio.c test_windirent.c -link sqlite3.lib -dll -out:fileio.dll - - - name: Install SQLCipher - run: | - cd C:\dev - git clone https://github.com/sqlcipher/sqlcipher - mv sqlcipher SQLCipher-${{ matrix.arch }} - cd SQLCipher-${{ matrix.arch }} - git switch $(git describe --tags --abbrev=0) - nmake /f Makefile.msc sqlcipher.dll USE_AMALGAMATION=1 NO_TCL=1 SQLITE3DLL=sqlcipher.dll SQLITE3LIB=sqlcipher.lib SQLITE3EXE=sqlcipher.exe LTLINKOPTS="C:\dev\OpenSSL-${{ matrix.arch }}\lib\libcrypto.lib" OPT_FEATURE_FLAGS="-DSQLITE_TEMP_STORE=2 -DSQLITE_HAS_CODEC=1 -DSQLITE_ENABLE_FTS3=1 -DSQLITE_ENABLE_FTS5=1 -DSQLITE_ENABLE_FTS3_PARENTHESIS=1 -DSQLITE_ENABLE_STAT4=1 -DSQLITE_SOUNDEX=1 -DSQLITE_ENABLE_JSON1=1 -DSQLITE_ENABLE_GEOPOLY=1 -DSQLITE_ENABLE_RTREE=1 -DSQLCIPHER_CRYPTO_OPENSSL=1 -DSQLITE_MAX_ATTACHED=125 -IC:\dev\OpenSSL-${{ matrix.arch }}\include" - mkdir sqlcipher - copy sqlite3.h sqlcipher - - - name: Patch WiX Toolset Variables - run: | - git apply .github\patch\product.wxs.patch - git apply .github\patch\translations.wxs.patch - git apply .github\patch\variables.wxi.patch - - - name: Configure build (SQLite) - run: | - mkdir release-sqlite && cd release-sqlite - if ("${{ matrix.arch }}" -eq "Win32") { - cmake -G"Ninja Multi-Config" -DCMAKE_PREFIX_PATH="C:/dev/SQLite-Win32;C:/dev/OpenSSL-Win32" ..\ - } else { - cmake -G"Ninja Multi-Config" -DCMAKE_PREFIX_PATH="C:/dev/SQLite-Win64;C:/dev/OpenSSL-Win64" ..\ - } - - - name: Build (SQLite) - run: | - cd release-sqlite - cmake --build . --config Release - - - name: Configure build (SQLCipher) - run: | - mkdir release-sqlcipher && cd release-sqlcipher - if ("${{ matrix.arch }}" -eq "Win32") { - cmake -G"Ninja Multi-Config" -Dsqlcipher=1 -DCMAKE_PREFIX_PATH="C:/dev/SQLCipher-Win32;C:/dev/OpenSSL-Win32" ..\ - } else { - cmake -G"Ninja Multi-Config" -Dsqlcipher=1 -DCMAKE_PREFIX_PATH="C:/dev/SQLCipher-Win64;C:/dev/OpenSSL-Win64" ..\ - } - - - name: Build (SQLCipher) - run: | - cd release-sqlcipher - cmake --build . --config Release - - - if: github.event_name != 'pull_request' - name: Create MSI - env: - ExePath: ${{ github.workspace }} - OpenSSLPath: C:\dev\OpenSSL-${{ matrix.arch }} - SQLCipherPath: C:\dev\SQLCipher-${{ matrix.arch }} - SqleanPath: ${{ github.workspace }}\sqlean - SQLitePath: C:\dev\SQLite-${{ matrix.arch }} - run: | - cd installer/windows - ./build.cmd "${{ matrix.arch }}".ToLower() - $ARCH="${{ matrix.arch }}".ToLower() - $DATE=$(Get-Date -Format "yyyy-MM-dd") - if ("${{ inputs.NIGHTLY }}" -eq "true") { - mv DB.Browser.for.SQLite-*.msi "DB.Browser.for.SQLite-$DATE-$ARCH.msi" - } else { - mv DB.Browser.for.SQLite-*.msi "DB.Browser.for.SQLite-dev-$(git rev-parse --short HEAD)-$ARCH.msi" - } - - - if: github.event_name != 'pull_request' - name: Upload artifacts for code signing with SignPath - id: unsigned-artifacts - uses: actions/upload-artifact@v4 - with: - name: build-artifacts-${{ matrix.os}}-${{ matrix.arch }}-unsigned - path: installer\windows\DB.Browser.for.SQLite-*.msi - - # Change the signing-policy-slug when you release an RC, RTM or stable release. - - if: github.event_name != 'pull_request' - name: Code signing with SignPath - uses: signpath/github-action-submit-signing-request@v1 - with: - api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' - github-artifact-id: '${{ steps.unsigned-artifacts.outputs.artifact-id }}' - organization-id: '${{ secrets.SIGNPATH_ORGANIZATION_ID }}' - output-artifact-directory: .\installer\windows - project-slug: 'sqlitebrowser' - signing-policy-slug: 'test-signing' - wait-for-completion: true - - - if: github.event_name != 'pull_request' - name: Create ZIP - run: | - $ARCH="${{ matrix.arch }}".ToLower() - $DATE=$(Get-Date -Format "yyyy-MM-dd") - if ("${{ inputs.NIGHTLY }}" -eq "true") { - $FILENAME_FORMAT="DB.Browser.for.SQLite-$DATE-$ARCH.zip" - } else { - $FILENAME_FORMAT="DB.Browser.for.SQLite-dev-$(git rev-parse --short HEAD)-$ARCH.zip" - } - Start-Process msiexec.exe -ArgumentList "/a $(dir installer\windows\DB.Browser.for.SQLite-*.msi) /q TARGETDIR=$PWD\target\" -Wait - if ("${{ matrix.arch }}" -eq "Win32") { - move target\System\* "target\DB Browser for SQLite\" - } else { - move target\System64\* "target\DB Browser for SQLite\" - } - Compress-Archive -Path "target\DB Browser for SQLite\*" -DestinationPath $FILENAME_FORMAT - - - if: github.event_name != 'pull_request' - name: Prepare artifacts - run: | - mkdir build-artifacts - move installer\windows\DB.Browser.for.SQLite-*.msi build-artifacts\ - move DB.Browser.for.SQLite-*.zip build-artifacts\ - Compress-Archive -Path build-artifacts\* -DestinationPath build-artifacts-${{ matrix.arch }}.zip - - - if: github.event_name != 'pull_request' - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: build-artifacts-${{ matrix.os }}-${{ matrix.arch }} - path: build-artifacts-${{ matrix.arch }}.zip diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d776b78cd8..5694ae2eb1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -30,7 +30,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - if: matrix.language == 'cpp' name: Install dependencies @@ -40,7 +40,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -62,7 +62,7 @@ jobs: # If this step fails, then you should remove it and run the build manually (see below) - if: matrix.language != 'cpp' name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@v4 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -75,6 +75,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml deleted file mode 100644 index 00fdb93eeb..0000000000 --- a/.github/workflows/coverity.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Coverity - -on: - push: - branches: [coverity_scan] - -defaults: - run: - shell: bash - -jobs: - build: - name: Coverity Scan - runs-on: ubuntu-20.04 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install qttools5-dev libqt5scintilla2-dev libqcustomplot-dev libsqlite3-dev libqt5svg5 libsqlcipher-dev qt5-default - - - name: Configure cmake - run: | - cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr -DENABLE_TESTING=ON -Dsqlcipher=1 . - - - name: Build and scan - uses: vapier/coverity-scan-action@v1 - with: - project: sqlitebrowser%2Fsqlitebrowser - token: ${{ secrets.COVERITY_TOKEN }} - email: "github@mkleusberg.de" - command: make diff --git a/.github/workflows/cppcmake-macos.yml b/.github/workflows/cppcmake-macos.yml new file mode 100644 index 0000000000..0f0f01faba --- /dev/null +++ b/.github/workflows/cppcmake-macos.yml @@ -0,0 +1,175 @@ +name: Build (macOS) + +on: + workflow_call: + inputs: + NIGHTLY: + default: false + type: boolean + CONSOLIDATE_DEPENDENCY_SUMMARY: + default: false + type: boolean + workflow_dispatch: + +jobs: + build: + name: ${{ matrix.os }} (${{ matrix.bundle }}) + runs-on: ${{ matrix.os }} + permissions: + contents: read + strategy: + fail-fast: false + matrix: + bundle: [SQLCipher, SQLite] + os: [macos-14] + env: + MACOSX_DEPLOYMENT_TARGET: 10.13 + steps: + - name: Checkout + uses: actions/checkout@v6 + + # Uninstall Mono, which is included by default in GitHub-hosted macOS runners, + # as it interferes with referencing our own compiled SQLite libraries. + - name: Uninstall Mono + run: | + sudo rm -rfv /Library/Frameworks/Mono.framework + sudo pkgutil --forget com.xamarin.mono-MDK.pkg + sudo rm -v /etc/paths.d/mono-commands + + - name: Install dependencies + run: | + brew tap sqlitebrowser/tap + brew install sqlb-qt@5 sqlb-sqlcipher sqlb-sqlite ninja + npm install -g appdmg + + - name: Configure build + run: | + if [ "${{ inputs.NIGHTLY }}" = "true" ]; then + if [ "${{ matrix.bundle }}" = "SQLCipher" ]; then + sed -i "" 's/"DB Browser for SQLite"/"DB Browser for SQLCipher Nightly"/' config/platform_apple.cmake + else + sed -i "" 's/"DB Browser for SQLite"/"DB Browser for SQLite Nightly"/' config/platform_apple.cmake + fi + else + if [ "${{ matrix.bundle }}" = "SQLCipher" ]; then + sed -i "" 's/"DB Browser for SQLite"/"DB Browser for SQLCipher-dev-'$(git rev-parse --short --verify HEAD)'"/' config/platform_apple.cmake + else + sed -i "" 's/"DB Browser for SQLite"/"DB Browser for SQLite-dev-'$(git rev-parse --short --verify HEAD)'"/' config/platform_apple.cmake + fi + fi + + mkdir -v build && cd build + cmake -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_STANDARD=14 \ + -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \ + -DcustomTap=1 \ + -DENABLE_TESTING=ON \ + -Dsqlcipher=${{ matrix.bundle == 'SQLCipher' }} .. + + - name: Build + working-directory: ./build + run: ninja + + - name: Tests + working-directory: ./build + run: ninja test + + - name: Build Extension + run: clang -I /opt/homebrew/opt/sqlb-sqlite/include -L /opt/homebrew/opt/sqlb-sqlite/lib -fno-common -dynamiclib src/extensions/extension-formats.c + + - if: github.event_name != 'pull_request' + name: Notarization + id: notarization + run: chmod +x ./installer/macos/notarize.sh && ./installer/macos/notarize.sh + env: + APPLE_ID: ${{ secrets.MACOS_CODESIGN_APPLE_ID }} + APPLE_PW: ${{ secrets.MACOS_CODESIGN_APPLE_PW }} + DEV_ID: ${{ secrets.MACOS_CODESIGN_DEV_ID }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + KEYCHAIN_PW: ${{ secrets.MACOS_CODESIGN_KEYCHAIN_PW }} + P12: ${{ secrets.MACOS_CODESIGN_P12 }} + P12_PW: ${{ secrets.MACOS_CODESIGN_P12_PW }} + NIGHTLY: ${{ inputs.NIGHTLY || false }} + SQLCIPHER: ${{ matrix.bundle == 'SQLCipher'}} + TEAM_ID: ${{ secrets.MACOS_CODESIGN_TEAM_ID }} + + - if: always() && steps.notarization.conclusion != 'skipped' + name: Clear Keychain + run: security delete-keychain $RUNNER_TEMP/app-signing.keychain-db + continue-on-error: true + + - if: github.event_name != 'pull_request' + name: Upload artifacts + uses: actions/upload-artifact@v7 + with: + name: build-artifacts-${{ matrix.os }}-${{ matrix.bundle }} + path: DB.Browser.for.*.dmg + retention-days: 1 + + - name: Summary + run: | + QT_VERSION=$($(brew --prefix sqlb-qt@5)/bin/qmake --version | awk '/Using Qt version/ {print $4}') + if [ "${{ matrix.bundle }}" = "SQLCipher" ]; then + OPENSSL_VERSION=$($(brew --prefix sqlb-openssl@3)/bin/openssl version | awk '{print $2}') + SQLCIPHER_VERSION=$($(brew --prefix sqlb-sqlcipher)/bin/sqlcipher ":memory:" "PRAGMA cipher_version;" | awk '{print $1}') + SQLITE_VERSION="Not applicable" + else + OPENSSL_VERSION="Not applicable" + SQLCIPHER_VERSION="Not applicable" + SQLITE_VERSION=$($(brew --prefix sqlb-sqlite)/bin/sqlite3 --version | awk '{print $1}') + fi + + mkdir -p dependency-summary + SUMMARY_ROW="| Build (macOS) | ${{ matrix.os }} / ${{ matrix.bundle }} | $OPENSSL_VERSION | $QT_VERSION | $SQLCIPHER_VERSION | $SQLITE_VERSION |" + echo "$SUMMARY_ROW" > "dependency-summary/dependencies-macos-${{ matrix.os }}-${{ matrix.bundle }}.md" + + - name: Upload dependency summary + uses: actions/upload-artifact@v7 + with: + name: dependency-summary-macos-${{ matrix.os }}-${{ matrix.bundle }} + path: dependency-summary/*.md + retention-days: 1 + + summarize-dependencies: + if: always() && inputs.CONSOLIDATE_DEPENDENCY_SUMMARY != true && !cancelled() + needs: build + name: Summarize Dependencies + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: read + steps: + - name: Download dependency summaries + uses: actions/download-artifact@v8 + continue-on-error: true + with: + pattern: dependency-summary-* + path: dependency-summary + merge-multiple: true + + - name: Summary + run: | + { + echo "## Dependencies used" + echo "" + echo "| Workflow | Target | OpenSSL | Qt | SQLCipher | SQLite |" + echo "| :---: | :---: | :---: | :---: | :---: | :---: |" + } >> "$GITHUB_STEP_SUMMARY" + + if [ -d dependency-summary ] && find dependency-summary -type f -name '*.md' | grep -q .; then + find dependency-summary -type f -name '*.md' -print | sort | while IFS= read -r file; do + cat "$file" >> "$GITHUB_STEP_SUMMARY" + done + else + echo "| Not available | No dependency summaries were uploaded. | - | - | - | - |" >> "$GITHUB_STEP_SUMMARY" + fi + + release: + if: github.event_name == 'workflow_dispatch' && github.workflow == 'Build (macOS)' + needs: build + name: Release + permissions: + actions: read + contents: write + uses: ./.github/workflows/release.yml diff --git a/.github/workflows/cppcmake-nightly.yml b/.github/workflows/cppcmake-nightly.yml deleted file mode 100644 index f824c74064..0000000000 --- a/.github/workflows/cppcmake-nightly.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Build and Deploy Nightly Builds - -on: - schedule: - - cron: '0 0 * * *' # Every day at midnight UTC - workflow_dispatch: - -jobs: - build-macos: - uses: ./.github/workflows/build-macos.yml - secrets: inherit - with: - NIGHTLY: true - - build-windows: - uses: ./.github/workflows/build-windows.yml - secrets: inherit - with: - NIGHTLY: true - - release: - needs: [build-macos, build-windows] - uses: ./.github/workflows/release.yml - secrets: inherit - with: - NIGHTLY: true diff --git a/.github/workflows/cppcmake-ubuntu.yml b/.github/workflows/cppcmake-ubuntu.yml new file mode 100644 index 0000000000..0cb1442513 --- /dev/null +++ b/.github/workflows/cppcmake-ubuntu.yml @@ -0,0 +1,178 @@ +# KEEP THE RUNNER OS AS LOW AS POSSIBLE. +# If built on the latest OS, it may not run on previous OS versions. +# Related: https://docs.appimage.org/reference/best-practices.html#binaries-compiled-on-old-enough-base-system + +name: Build (Ubuntu) + +on: + workflow_call: + inputs: + NIGHTLY: + default: false + type: boolean + CONSOLIDATE_DEPENDENCY_SUMMARY: + default: false + type: boolean + workflow_dispatch: + +jobs: + build: + name: ${{ matrix.os }} (${{ matrix.bundle }}) + runs-on: ${{ matrix.os }} + permissions: + contents: read + strategy: + fail-fast: false + matrix: + bundle: [SQLCipher, SQLite] + os: [ubuntu-22.04, ubuntu-22.04-arm] + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install and cache dependencies + uses: awalsh128/cache-apt-pkgs-action@v1.6.0 + with: + packages: libfuse2 libqcustomplot-dev libqscintilla2-qt5-dev libqt5svg5 ninja-build qttools5-dev + version: ${{ matrix.os }} + + - if: matrix.bundle == 'SQLCipher' + name: Build SQLCipher + working-directory: ${{ runner.temp }} + run: | + git clone https://github.com/sqlcipher/sqlcipher && cd sqlcipher && git checkout $(git describe --tags --abbrev=0) + LDFLAGS="-lcrypto -Wl,-soname,libsqlcipher.so.0" ./configure --prefix=./prefix --with-tempstore=yes --enable-load-extension --disable-tcl CFLAGS="-DSQLITE_EXTRA_INIT=sqlcipher_extra_init -DSQLITE_EXTRA_SHUTDOWN=sqlcipher_extra_shutdown -DSQLCIPHER_CRYPTO_OPENSSL -DSQLITE_ENABLE_COLUMN_METADATA -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_GEOPOLY -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_MEMORY_MANAGEMENT=1 -DSQLITE_ENABLE_RTREE -DSQLITE_ENABLE_SNAPSHOT=1 -DSQLITE_ENABLE_STAT4 -DSQLITE_HAS_CODEC -DSQLITE_SOUNDEX" + make -j2 && sudo make install -j2 + cp ${GITHUB_WORKSPACE}/tools/linux/sqlcipher_rename.sh . + chmod +x sqlcipher_rename.sh && sudo ./sqlcipher_rename.sh + + - if: matrix.bundle == 'SQLite' + name: Build SQLite + run: | + TARBALL=$(curl -s https://sqlite.org/download.html | awk '// {print}' | grep 'sqlite-autoconf' | cut -d ',' -f 3) + SHA3=$(curl -s https://sqlite.org/download.html | awk '// {print}' | grep 'sqlite-autoconf' | cut -d ',' -f 5) + curl -LsS -o sqlite.tar.gz https://sqlite.org/${TARBALL} + VERIFY=$(openssl dgst -sha3-256 sqlite.tar.gz | cut -d ' ' -f 2) + if [ "$SHA3" != "$VERIFY" ]; then + echo "::error::SQLite tarball checksum mismatch." + exit 1 + fi + tar -xzf sqlite.tar.gz && cd sqlite-autoconf-* + + CPPFLAGS="-DSQLITE_ENABLE_COLUMN_METADATA=1 -DSQLITE_MAX_VARIABLE_NUMBER=250000 -DSQLITE_ENABLE_RTREE=1 -DSQLITE_ENABLE_GEOPOLY=1 -DSQLITE_ENABLE_FTS3=1 -DSQLITE_ENABLE_FTS3_PARENTHESIS=1 -DSQLITE_ENABLE_FTS5=1 -DSQLITE_ENABLE_STAT4=1 -DSQLITE_ENABLE_JSON1=1 -DSQLITE_SOUNDEX=1 -DSQLITE_ENABLE_MATH_FUNCTIONS=1 -DSQLITE_MAX_ATTACHED=125 -DSQLITE_ENABLE_MEMORY_MANAGEMENT=1 -DSQLITE_ENABLE_SNAPSHOT=1" ./configure --disable-shared + make -j2 && sudo make install -j2 + + - name: Configure build + run: | + mkdir -v appbuild appdir && cd appbuild + cmake -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH=$RUNNER_TEMP/sqlcipher/prefix \ + -DCMAKE_INSTALL_PREFIX:PATH=../appdir/usr \ + -DENABLE_TESTING=ON \ + -DFORCE_INTERNAL_QSCINTILLA=ON \ + -Dsqlcipher=${{ matrix.bundle == 'SQLCipher' }} .. + + - name: Build + working-directory: ./appbuild + run: ninja install + + - name: Tests + working-directory: ./appbuild + run: ninja test + + - name: Build AppImage + run: | + wget -c -nv "https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20240109-1/linuxdeploy-$(uname -m).AppImage" + wget -c -nv "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/1-alpha-20240109-1/linuxdeploy-plugin-qt-$(uname -m).AppImage" + chmod a+x "linuxdeploy-$(uname -m).AppImage" "linuxdeploy-plugin-qt-$(uname -m).AppImage" + if [ "${{ inputs.NIGHTLY }}" = "true" ]; then + export VERSION=$(date +%Y%m%d) + else + export VERSION=$(printf "dev-`git -C . rev-parse --short HEAD`") + fi + LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib:$RUNNER_TEMP/sqlcipher/prefix/lib" "./linuxdeploy-$(uname -m).AppImage" --appdir=appdir --desktop-file=appdir/usr/share/applications/sqlitebrowser.desktop --plugin qt --output appimage + + - name: Rename a file + run: | + for i in DB_Browser_for_SQLite*; do mv "$i" "${i//_/.}"; done + if [ "${{ matrix.bundle }}" = "SQLCipher" ]; then + export FILE=$(ls DB.Browser.for.SQLite*.AppImage) + export FILE=${FILE/SQLite/SQLCipher} + mv -v DB.Browser.for.SQLite*.AppImage $FILE + fi + + - if: github.event_name != 'pull_request' + name: Upload artifacts + uses: actions/upload-artifact@v7 + with: + name: build-artifacts-${{ matrix.os }}-${{ matrix.bundle }} + path: DB.Browser.for.*.AppImage + retention-days: 1 + + - name: Summary + run: | + QT_VERSION=$(qmake --version | awk '/Using Qt version/ {print $4}') + if [ "${{ matrix.bundle }}" = "SQLCipher" ]; then + OPENSSL_VERSION=$(openssl version | awk '{print $2}') + SQLCIPHER_VERSION=$($RUNNER_TEMP/sqlcipher/prefix/bin/sqlcipher ":memory:" "PRAGMA cipher_version;" | awk '{print $1}') + SQLITE_VERSION="Not applicable" + else + OPENSSL_VERSION="Not applicable" + SQLCIPHER_VERSION="Not applicable" + SQLITE_VERSION=$(/usr/local/bin/sqlite3 --version | awk '{print $1}') + fi + + mkdir -p dependency-summary + SUMMARY_ROW="| Build (Ubuntu) | ${{ matrix.os }} / ${{ matrix.bundle }} | $OPENSSL_VERSION | $QT_VERSION | $SQLCIPHER_VERSION | $SQLITE_VERSION |" + echo "$SUMMARY_ROW" > "dependency-summary/dependencies-ubuntu-${{ matrix.os }}-${{ matrix.bundle }}.md" + + - name: Upload dependency summary + uses: actions/upload-artifact@v7 + with: + name: dependency-summary-ubuntu-${{ matrix.os }}-${{ matrix.bundle }} + path: dependency-summary/*.md + retention-days: 1 + + summarize-dependencies: + if: always() && inputs.CONSOLIDATE_DEPENDENCY_SUMMARY != true && !cancelled() + needs: build + name: Summarize Dependencies + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: read + steps: + - name: Download dependency summaries + uses: actions/download-artifact@v8 + continue-on-error: true + with: + pattern: dependency-summary-* + path: dependency-summary + merge-multiple: true + + - name: Summary + run: | + { + echo "## Dependencies used" + echo "" + echo "| Workflow | Target | OpenSSL | Qt | SQLCipher | SQLite |" + echo "| :---: | :---: | :---: | :---: | :---: | :---: |" + } >> "$GITHUB_STEP_SUMMARY" + + if [ -d dependency-summary ] && find dependency-summary -type f -name '*.md' | grep -q .; then + find dependency-summary -type f -name '*.md' -print | sort | while IFS= read -r file; do + cat "$file" >> "$GITHUB_STEP_SUMMARY" + done + else + echo "| Not available | No dependency summaries were uploaded. | - | - | - | - |" >> "$GITHUB_STEP_SUMMARY" + fi + + release: + if: github.event_name == 'workflow_dispatch' && github.workflow == 'Build (Ubuntu)' + needs: build + name: Release + permissions: + actions: read + contents: write + uses: ./.github/workflows/release.yml diff --git a/.github/workflows/cppcmake-windows.yml b/.github/workflows/cppcmake-windows.yml new file mode 100644 index 0000000000..f2530fbea1 --- /dev/null +++ b/.github/workflows/cppcmake-windows.yml @@ -0,0 +1,266 @@ +name: Build (Windows) + +on: + workflow_call: + inputs: + NIGHTLY: + default: false + type: boolean + CONSOLIDATE_DEPENDENCY_SUMMARY: + default: false + type: boolean + workflow_dispatch: + +jobs: + build: + name: ${{ matrix.os }}-${{ matrix.arch }} + runs-on: ${{ matrix.os }} + permissions: + actions: read + contents: read + strategy: + fail-fast: false + matrix: + arch: [x86, x64] + os: [windows-2022] + env: + GH_TOKEN: ${{ github.token }} + OPENSSL_VERSION: 1.1.1.2100 + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install dependencies + run: | + choco install --no-progress ninja + if ("${{ matrix.arch }}" -eq "x86") { + choco install --no-progress openssl --x86 --version=${{ env.OPENSSL_VERSION}} + } else { + choco install --no-progress openssl --version=${{ env.OPENSSL_VERSION}} + } + + # When building SQLCipher, if we specify a path to OpenSSL and + # there are spaces in the path, an error will occur, so to + # avoid this, create the symlink. + New-Item -Path C:\dev -ItemType Directory + if ("${{ matrix.arch }}" -eq "x86") { + New-Item -Path "C:\dev\OpenSSL" -ItemType SymbolicLink -Value "C:\Program Files (x86)\OpenSSL-Win32\" + } else { + New-Item -Path "C:\dev\OpenSSL" -ItemType SymbolicLink -Value "C:\Program Files\OpenSSL" + } + + - name: Install Qt + uses: jurplel/install-qt-action@v4 + with: + arch: ${{ matrix.arch == 'x86' && 'win32_msvc2019' || matrix.arch == 'x64' && 'win64_msvc2019_64'}} + cache: true + cache-key-prefix: "cache" + version: 5.15.2 + + - name: Install VS2019 + run: choco install visualstudio2019community --package-parameters "--add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 --add Microsoft.VisualStudio.Component.Windows10SDK.19041 --add Microsoft.VisualStudio.Component.VC.Redist.MSM" + + - name: Setup MSVC + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: ${{ matrix.arch == 'x86' && 'amd64_x86' || matrix.arch == 'x64' && 'amd64'}} + vsversion: 2019 + + - name: Build SQLite + run: | + Set-Location "${{ github.workspace }}\tools\windows" + $SQLiteArchiveFileName = (python download_latest_sqlite.py).Trim() + Expand-Archive -Path $SQLiteArchiveFileName -DestinationPath C:\dev\ + Move-Item -Path C:\dev\sqlite-amalgamation-* C:\dev\SQLite\ + cd C:\dev\SQLite + cl sqlite3.c -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_STAT4 -DSQLITE_SOUNDEX -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_GEOPOLY -DSQLITE_ENABLE_RTREE -DSQLITE_ENABLE_MATH_FUNCTIONS -DSQLITE_MAX_ATTACHED=125 -DSQLITE_API="__declspec(dllexport)" -link -dll -out:sqlite3.dll + + - name: Download SQLean extension + run: | + if ("${{ matrix.arch }}" -eq "x86") { + # sqlean has discontinued x86 support starting from v0.28.0 + gh release download 0.27.4 --pattern "sqlean-win-x86.zip" --repo "nalgeon/sqlean" + Expand-Archive -Path sqlean-win-x86.zip -DestinationPath .\sqlean + } else { + gh release download --pattern "sqlean-win-x64.zip" --repo "nalgeon/sqlean" + Expand-Archive -Path sqlean-win-x64.zip -DestinationPath .\sqlean + } + + - name: Build 'formats' Extensions + run: | + cp .\src\extensions\extension-formats.c C:\dev\SQLite\ + cp .\src\extensions\extension-formats.def C:\dev\SQLite\ + cd C:\dev\SQLite + cl /MD extension-formats.c -link -dll -def:extension-formats.def -out:formats.dll + + - name: Build SQLCipher + run: | + cd C:\dev + Invoke-WebRequest -Uri https://github.com/sqlcipher/sqlcipher/archive/refs/tags/v4.6.1.zip -OutFile 'sqlcipher.zip' + Expand-Archive -Path sqlcipher.zip -DestinationPath C:\dev\ + Move-Item -Path C:\dev\sqlcipher-4.6.1 C:\dev\SQLCipher\ + cd SQLCipher + nmake /f Makefile.msc sqlcipher.dll USE_AMALGAMATION=1 NO_TCL=1 SQLITE3DLL=sqlcipher.dll SQLITE3LIB=sqlcipher.lib SQLITE3EXE=sqlcipher.exe LTLINKOPTS="C:\dev\OpenSSL\lib\libcrypto.lib" OPT_FEATURE_FLAGS="-DSQLITE_TEMP_STORE=2 -DSQLITE_HAS_CODEC=1 -DSQLITE_ENABLE_FTS3=1 -DSQLITE_ENABLE_FTS5=1 -DSQLITE_ENABLE_FTS3_PARENTHESIS=1 -DSQLITE_ENABLE_STAT4=1 -DSQLITE_SOUNDEX=1 -DSQLITE_ENABLE_JSON1=1 -DSQLITE_ENABLE_GEOPOLY=1 -DSQLITE_ENABLE_RTREE=1 -DSQLCIPHER_CRYPTO_OPENSSL=1 -DSQLITE_MAX_ATTACHED=125 -IC:\dev\OpenSSL\include" + mkdir sqlcipher + copy sqlite3.h sqlcipher + + - name: Configure build (SQLite) + run: | + mkdir release-sqlite && cd release-sqlite + cmake -G "Ninja Multi-Config" -DCMAKE_PREFIX_PATH="C:\dev\SQLite" -DENABLE_TESTING=ON ..\ + + - name: Build (SQLite) + run: | + cd release-sqlite + cmake --build . --config Release + + - name: Tests (SQLite) + run: | + cd release-sqlite + ctest -C Release --output-on-failure + + - name: Configure build (SQLCipher) + run: | + mkdir release-sqlcipher && cd release-sqlcipher + cmake -G "Ninja Multi-Config" -Dsqlcipher=1 -DCMAKE_PREFIX_PATH="C:\dev\OpenSSL;C:\dev\SQLCipher" -DENABLE_TESTING=ON ..\ + + - name: Build (SQLCipher) + run: | + cd release-sqlcipher + cmake --build . --config Release + + - name: Tests (SQLCipher) + run: | + cd release-sqlcipher + ctest -C Release --output-on-failure + + - if: github.event_name != 'pull_request' + name: Create MSI + env: + ExePath: ${{ github.workspace }} + OpenSSLPath: C:\dev\OpenSSL + SQLCipherPath: C:\dev\SQLCipher + SqleanPath: ${{ github.workspace }}\sqlean + SQLitePath: C:\dev\SQLite + run: | + cd installer/windows + ./build.cmd ${{ matrix.arch }} + $DATE=$(Get-Date -Format "yyyyMMdd") + if ("${{ inputs.NIGHTLY }}" -eq "true") { + mv DB.Browser.for.SQLite-*.msi "DB.Browser.for.SQLite-$DATE-win-${{ matrix.arch }}.msi" + } else { + mv DB.Browser.for.SQLite-*.msi "DB.Browser.for.SQLite-dev-$(git rev-parse --short HEAD)-win-${{ matrix.arch }}.msi" + } + + - if: github.event_name != 'pull_request' + name: Upload artifacts for code signing with SignPath + id: unsigned-artifacts + uses: actions/upload-artifact@v7 + with: + name: build-artifacts-${{ matrix.os }}-win-${{ matrix.arch }}-unsigned + path: installer\windows\DB.Browser.for.SQLite-*.msi + retention-days: 1 + + # Change the signing-policy-slug when you release an RC, RTM or stable release. + - if: github.event_name != 'pull_request' + name: Code signing with SignPath + uses: signpath/github-action-submit-signing-request@v2 + with: + api-token: "${{ secrets.SIGNPATH_API_TOKEN }}" + github-artifact-id: "${{ steps.unsigned-artifacts.outputs.artifact-id }}" + organization-id: "${{ secrets.SIGNPATH_ORGANIZATION_ID }}" + output-artifact-directory: .\installer\windows + project-slug: "sqlitebrowser" + signing-policy-slug: "test-signing" + wait-for-completion: true + + - if: github.event_name != 'pull_request' + name: Create ZIP + run: | + $DATE=$(Get-Date -Format "yyyyMMdd") + if ("${{ inputs.NIGHTLY }}" -eq "true") { + $FILENAME_FORMAT="DB.Browser.for.SQLite-$DATE-win-${{ matrix.arch }}.zip" + } else { + $FILENAME_FORMAT="DB.Browser.for.SQLite-dev-$(git rev-parse --short HEAD)-win-${{ matrix.arch }}.zip" + } + Start-Process msiexec.exe -ArgumentList "/a $(dir installer\windows\DB.Browser.for.SQLite-*.msi) /q TARGETDIR=$PWD\target\" -Wait + if ("${{ matrix.arch }}" -eq "x86") { + move target\System\* "target\DB Browser for SQLite\" + } else { + move target\System64\* "target\DB Browser for SQLite\" + } + Compress-Archive -Path "target\DB Browser for SQLite\*" -DestinationPath $FILENAME_FORMAT + + - if: github.event_name != 'pull_request' + name: Upload artifacts + uses: actions/upload-artifact@v7 + with: + name: build-artifacts-win-${{ matrix.arch }} + path: | + installer\windows\DB.Browser.for.SQLite-*.msi + DB.Browser.for.SQLite-*.zip + retention-days: 1 + + - name: Summary + run: | + $OPENSSL_VERSION=(C:\dev\OpenSSL\bin\openssl version) -replace "OpenSSL ([\d\.]+[a-z]+) .*", '$1' + $QT_VERSION = & "$env:QT_ROOT_DIR\bin\qmake.exe" --version | Select-String "Using Qt version" | ForEach-Object { $_.ToString().Split()[3] } + $SQLCIPHER_VERSION=(Get-Item "C:\dev\SQLCipher\sqlcipher.dll").VersionInfo.FileVersion + Select-String -Path "C:\dev\SQLite\sqlite3.h" -Pattern '#define SQLITE_VERSION\s+"([\d\.]+)"' | ForEach-Object { + ($_ -match '"([\d\.]+)"') | Out-Null + $SQLITE_VERSION=$matches[1] + } + + New-Item -ItemType Directory -Force -Path dependency-summary | Out-Null + $SUMMARY_ROW = "| Build (Windows) | ${{ matrix.os }} / ${{ matrix.arch }} | $OPENSSL_VERSION | $QT_VERSION | $SQLCIPHER_VERSION | $SQLITE_VERSION |" + Set-Content -Path "dependency-summary/dependencies-windows-${{ matrix.os }}-${{ matrix.arch }}.md" -Value $SUMMARY_ROW + + - name: Upload dependency summary + uses: actions/upload-artifact@v7 + with: + name: dependency-summary-windows-${{ matrix.os }}-${{ matrix.arch }} + path: dependency-summary/*.md + retention-days: 1 + + summarize-dependencies: + if: always() && inputs.CONSOLIDATE_DEPENDENCY_SUMMARY != true && !cancelled() + needs: build + name: Summarize Dependencies + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: read + steps: + - name: Download dependency summaries + uses: actions/download-artifact@v8 + continue-on-error: true + with: + pattern: dependency-summary-* + path: dependency-summary + merge-multiple: true + + - name: Summary + run: | + { + echo "## Dependencies used" + echo "" + echo "| Workflow | Target | OpenSSL | Qt | SQLCipher | SQLite |" + echo "| :---: | :---: | :---: | :---: | :---: | :---: |" + } >> "$GITHUB_STEP_SUMMARY" + + if [ -d dependency-summary ] && find dependency-summary -type f -name '*.md' | grep -q .; then + find dependency-summary -type f -name '*.md' -print | sort | while IFS= read -r file; do + cat "$file" >> "$GITHUB_STEP_SUMMARY" + done + else + echo "| Not available | No dependency summaries were uploaded. | - | - | - | - |" >> "$GITHUB_STEP_SUMMARY" + fi + + release: + if: github.event_name == 'workflow_dispatch' && github.workflow == 'Build (Windows)' + needs: build + name: Release + permissions: + actions: read + contents: write + uses: ./.github/workflows/release.yml diff --git a/.github/workflows/cppcmake-windows_on_arm.yml b/.github/workflows/cppcmake-windows_on_arm.yml new file mode 100644 index 0000000000..c7052bb9a2 --- /dev/null +++ b/.github/workflows/cppcmake-windows_on_arm.yml @@ -0,0 +1,284 @@ +name: Build (Windows on ARM) + +on: + workflow_call: + inputs: + NIGHTLY: + default: false + type: boolean + CONSOLIDATE_DEPENDENCY_SUMMARY: + default: false + type: boolean + workflow_dispatch: + +jobs: + build: + name: ${{ matrix.os }}-arm64 + runs-on: ${{ matrix.os }} + permissions: + actions: read + contents: read + strategy: + fail-fast: false + matrix: + os: [windows-11-arm] + env: + GH_TOKEN: ${{ github.token }} + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.14.4" + cache: "pip" + cache-dependency-path: tools/windows/requirements/download_latest_openssl3.txt + + - name: Install dependencies + run: | + choco install --no-progress ninja wixtoolset + + cd $env:GITHUB_WORKSPACE/tools/windows/ + python -m pip install --disable-pip-version-check -r requirements/download_latest_openssl3.txt + $OpenSSLInstallerFileName = (python download_latest_openssl3.py).Trim() + Start-Process -FilePath ".\$OpenSSLInstallerFileName" -ArgumentList '/ALLUSERS /verysilent /dir="C:\dev\OpenSSL"' -Wait + + - name: Install Qt + uses: jurplel/install-qt-action@v4 + with: + arch: "win64_msvc2022_arm64" + cache: true + cache-key-prefix: "cache" + modules: "debug_info qt5compat qtactiveqt qtcharts qtconnectivity qtgraphs qtgrpc qthttpserver qtimageformats qtlanguageserver qtlocation qtlottie qtmultimedia qtnetworkauth qtpositioning qtquick3d qtquick3dphysics qtquicktimeline qtremoteobjects qtscxml qtsensors qtserialbus qtserialport qtshadertools qtspeech qtvirtualkeyboard qtwebchannel qtwebsockets" + version: 6.8.3 + + - name: Setup MSVC for ARM64 + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: arm64 + vsversion: 2022 + + - name: Build SQLite + run: | + cd $env:GITHUB_WORKSPACE/tools/windows/ + $SQLiteArchiveFileName = (python download_latest_sqlite.py).Trim() + Expand-Archive -Path $SQLiteArchiveFileName -DestinationPath C:\dev\ + Move-Item -Path C:\dev\sqlite-amalgamation-* C:\dev\SQLite\ + cd C:\dev\SQLite + cl sqlite3.c -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_STAT4 -DSQLITE_SOUNDEX -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_GEOPOLY -DSQLITE_ENABLE_RTREE -DSQLITE_ENABLE_MATH_FUNCTIONS -DSQLITE_MAX_ATTACHED=125 -DSQLITE_API="__declspec(dllexport)" -link -dll -out:sqlite3.dll + + # MSYS2 is required to build the SQLean extension. + - name: Setup MSYS2 + uses: msys2/setup-msys2@v2 + with: + msystem: CLANGARM64 + update: true + install: >- + curl + git + make + mingw-w64-clang-aarch64-clang + unzip + + - name: Build SQLean extension + shell: msys2 {0} + run: | + git clone https://www.github.com/nalgeon/sqlean /c/dev/sqlean + cd /c/dev/sqlean + cp /c/a/sqlitebrowser/sqlitebrowser/tools/windows/add_sqlean_arm64_target.patch . + git apply --ignore-space-change --ignore-whitespace add_sqlean_arm64_target.patch + make prepare-dist + make download-sqlite + make download-external + make compile-windows-arm64 + + - name: Build Extension + run: | + cp $env:GITHUB_WORKSPACE/src/extensions/extension-formats.c C:\dev\SQLite\ + cp $env:GITHUB_WORKSPACE/src/extensions/extension-formats.def C:\dev\SQLite\ + cd C:\dev\SQLite + cl /MD extension-formats.c -link -dll -def:extension-formats.def -out:formats.dll + + - name: Setup MSVC to x64 to build required SQLCipher binaries + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + vsversion: 2022 + + - name: Build x64 binaries for build SQLCipher + run: | + git clone https://www.github.com/sqlcipher/sqlcipher C:\dev\SQLCipher --branch v4.6.1 + cd C:\dev\SQLCipher + nmake /f Makefile.msc lemon.exe mkkeywordhash.exe mksourceid.exe src-verify.exe USE_AMALGAMATION=1 NO_TCL=1 SQLITE3DLL=sqlcipher.dll SQLITE3LIB=sqlcipher.lib SQLITE3EXE=sqlcipher.exe LTLINKOPTS="C:\dev\OpenSSL\lib\VC\x64\MD\libcrypto.lib" OPT_FEATURE_FLAGS="-DSQLITE_TEMP_STORE=2 -DSQLITE_HAS_CODEC=1 -DSQLITE_ENABLE_FTS3=1 -DSQLITE_ENABLE_FTS5=1 -DSQLITE_ENABLE_FTS3_PARENTHESIS=1 -DSQLITE_ENABLE_STAT4=1 -DSQLITE_SOUNDEX=1 -DSQLITE_ENABLE_JSON1=1 -DSQLITE_ENABLE_GEOPOLY=1 -DSQLITE_ENABLE_RTREE=1 -DSQLCIPHER_CRYPTO_OPENSSL=1 -DSQLITE_MAX_ATTACHED=125 -IC:\dev\OpenSSL\include\x64" + + - name: Re-setup MSVC for ARM64 + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: arm64 + vsversion: 2022 + + - name: Build SQLCipher + run: | + cd C:\dev\SQLCipher + nmake /f Makefile.msc sqlcipher.dll USE_AMALGAMATION=1 NO_TCL=1 SQLITE3DLL=sqlcipher.dll SQLITE3LIB=sqlcipher.lib SQLITE3EXE=sqlcipher.exe LTLINKOPTS="C:\dev\OpenSSL\lib\VC\arm64\MD\libcrypto.lib" OPT_FEATURE_FLAGS="-DSQLITE_TEMP_STORE=2 -DSQLITE_HAS_CODEC=1 -DSQLITE_ENABLE_FTS3=1 -DSQLITE_ENABLE_FTS5=1 -DSQLITE_ENABLE_FTS3_PARENTHESIS=1 -DSQLITE_ENABLE_STAT4=1 -DSQLITE_SOUNDEX=1 -DSQLITE_ENABLE_JSON1=1 -DSQLITE_ENABLE_GEOPOLY=1 -DSQLITE_ENABLE_RTREE=1 -DSQLCIPHER_CRYPTO_OPENSSL=1 -DSQLITE_MAX_ATTACHED=125 -IC:\dev\OpenSSL\include\arm64" + mkdir sqlcipher + copy sqlite3.h sqlcipher + # Avoid the C2059 error. + mv VERSION VERSION.txt + + - name: Configure build (SQLite) + run: | + mkdir release-sqlite && cd release-sqlite + cmake -G "Ninja Multi-Config" -DCMAKE_PREFIX_PATH="C:\dev\SQLite" -DENABLE_TESTING=ON -DQT_MAJOR=Qt6 ..\ + + - name: Build (SQLite) + run: | + cd release-sqlite + cmake --build . --config Release + + - name: Tests (SQLite) + run: | + cd release-sqlite + ctest -C Release --output-on-failure + + - name: Configure build (SQLCipher) + run: | + mkdir release-sqlcipher && cd release-sqlcipher + cmake -G "Ninja Multi-Config" -Dsqlcipher=1 -DCMAKE_PREFIX_PATH="C:\dev\OpenSSL;C:\dev\SQLCipher" -DENABLE_TESTING=ON -DQT_MAJOR=Qt6 ..\ + + - name: Build (SQLCipher) + run: | + cd release-sqlcipher + cmake --build . --config Release + + - name: Tests (SQLCipher) + run: | + cd release-sqlcipher + ctest -C Release --output-on-failure + + - if: github.event_name != 'pull_request' + name: Create MSI + env: + ExePath: ${{ github.workspace }} + OpenSSLPath: C:\dev\OpenSSL + SQLCipherPath: C:\dev\SQLCipher + SqleanPath: C:\dev\sqlean + SQLitePath: C:\dev\SQLite + run: | + cd installer\windows_on_arm + ./build.cmd + $DATE=$(Get-Date -Format "yyyyMMdd") + if ("${{ inputs.NIGHTLY }}" -eq "true") { + mv DB.Browser.for.SQLite-*.msi "DB.Browser.for.SQLite-$DATE-win-arm64.msi" + } else { + mv DB.Browser.for.SQLite-*.msi "DB.Browser.for.SQLite-dev-$(git rev-parse --short HEAD)-win-arm64.msi" + } + + - if: github.event_name != 'pull_request' + name: Upload artifacts for code signing with SignPath + id: unsigned-artifacts + uses: actions/upload-artifact@v7 + with: + name: build-artifacts-${{ matrix.os }}-win-arm64-unsigned + path: installer\windows_on_arm\DB.Browser.for.SQLite-*.msi + retention-days: 1 + + # Change the signing-policy-slug when you release an RC, RTM or stable release. + - if: github.event_name != 'pull_request' + name: Code signing with SignPath + uses: signpath/github-action-submit-signing-request@v2 + with: + api-token: "${{ secrets.SIGNPATH_API_TOKEN }}" + github-artifact-id: "${{ steps.unsigned-artifacts.outputs.artifact-id }}" + organization-id: "${{ secrets.SIGNPATH_ORGANIZATION_ID }}" + output-artifact-directory: .\installer\windows_on_arm + project-slug: "sqlitebrowser" + signing-policy-slug: "test-signing" + wait-for-completion: true + + - if: github.event_name != 'pull_request' + name: Create ZIP + run: | + $DATE=$(Get-Date -Format "yyyyMMdd") + if ("${{ inputs.NIGHTLY }}" -eq "true") { + $FILENAME_FORMAT="DB.Browser.for.SQLite-$DATE-win-arm64.zip" + } else { + $FILENAME_FORMAT="DB.Browser.for.SQLite-dev-$(git rev-parse --short HEAD)-win-arm64.zip" + } + Start-Process msiexec.exe -ArgumentList "/a $(dir installer\windows_on_arm\DB.Browser.for.SQLite-*.msi) /q TARGETDIR=$PWD\target\" -Wait + Compress-Archive -Path "target\DB Browser for SQLite\*" -DestinationPath $FILENAME_FORMAT + + - if: github.event_name != 'pull_request' + name: Upload artifacts + uses: actions/upload-artifact@v7 + with: + name: build-artifacts-${{ matrix.os }}-win-arm64 + path: | + installer\windows_on_arm\DB.Browser.for.SQLite-*.msi + DB.Browser.for.SQLite-*.zip + retention-days: 1 + + - name: Summary + run: | + $OPENSSL_VERSION=(C:\dev\OpenSSL\bin\arm64\openssl version) -replace "OpenSSL ([\d\.]+[a-z]+) .*", '$1' + $QT_VERSION = & "$env:QT_ROOT_DIR\bin\qmake.exe" --version | Select-String "Using Qt version" | ForEach-Object { $_.ToString().Split()[3] } + $SQLCIPHER_VERSION=(Get-Item "C:\dev\SQLCipher\sqlcipher.dll").VersionInfo.FileVersion + Select-String -Path "C:\dev\SQLite\sqlite3.h" -Pattern '#define SQLITE_VERSION\s+"([\d\.]+)"' | ForEach-Object { + ($_ -match '"([\d\.]+)"') | Out-Null + $SQLITE_VERSION=$matches[1] + } + + New-Item -ItemType Directory -Force -Path dependency-summary | Out-Null + $SUMMARY_ROW = "| Build (Windows on ARM) | ${{ matrix.os }} / arm64 | $OPENSSL_VERSION | $QT_VERSION | $SQLCIPHER_VERSION | $SQLITE_VERSION |" + Set-Content -Path "dependency-summary/dependencies-windows-on-arm-${{ matrix.os }}-arm64.md" -Value $SUMMARY_ROW + + - name: Upload dependency summary + uses: actions/upload-artifact@v7 + with: + name: dependency-summary-windows-on-arm-${{ matrix.os }}-arm64 + path: dependency-summary/*.md + retention-days: 1 + + summarize-dependencies: + if: always() && inputs.CONSOLIDATE_DEPENDENCY_SUMMARY != true && !cancelled() + needs: build + name: Summarize Dependencies + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: read + steps: + - name: Download dependency summaries + uses: actions/download-artifact@v8 + continue-on-error: true + with: + pattern: dependency-summary-* + path: dependency-summary + merge-multiple: true + + - name: Summary + run: | + { + echo "## Dependencies used" + echo "" + echo "| Workflow | Target | OpenSSL | Qt | SQLCipher | SQLite |" + echo "| :---: | :---: | :---: | :---: | :---: | :---: |" + } >> "$GITHUB_STEP_SUMMARY" + + if [ -d dependency-summary ] && find dependency-summary -type f -name '*.md' | grep -q .; then + find dependency-summary -type f -name '*.md' -print | sort | while IFS= read -r file; do + cat "$file" >> "$GITHUB_STEP_SUMMARY" + done + else + echo "| Not available | No dependency summaries were uploaded. | - | - | - | - |" >> "$GITHUB_STEP_SUMMARY" + fi + + release: + if: github.event_name == 'workflow_dispatch' && github.workflow == 'Build (Windows on ARM)' + needs: build + name: Release + permissions: + actions: read + contents: write + uses: ./.github/workflows/release.yml diff --git a/.github/workflows/cppcmake.yml b/.github/workflows/cppcmake.yml index 9c66c0e723..3c75d112f4 100644 --- a/.github/workflows/cppcmake.yml +++ b/.github/workflows/cppcmake.yml @@ -4,30 +4,144 @@ on: push: branches: [master] pull_request: + schedule: + - cron: "0 0 * * *" # Every day at midnight UTC workflow_dispatch: + inputs: + NIGHTLY: + description: "Run as a nightly build" + default: false + required: true + type: boolean -permissions: - contents: write +run-name: >- + ${{ + (github.event_name == 'schedule' || inputs.NIGHTLY == true) + && 'Build and Deploy Nightly Builds' + || github.event.pull_request.title + || github.event.head_commit.message + || format('{0} on {1}', github.workflow, github.ref_name) + }} jobs: - build-appimage: - if: github.event_name != 'pull_request' - uses: ./.github/workflows/build-appimage.yml + check-skippable: + name: Check Skippable + runs-on: ubuntu-24.04 + permissions: + contents: read + outputs: + skip: ${{ steps.set-skippable.outputs.skippable || 'false' }} + steps: + - uses: actions/checkout@v6 + + - name: Check and set skippable + id: set-skippable + continue-on-error: true + run: | + if [ "${{ github.event_name }}" = "schedule" ]; then + git fetch origin tag nightly + LAST_COMMIT_HASH=$(git rev-list -n 1 nightly) + if [ "$(git rev-parse HEAD)" = "$LAST_COMMIT_HASH" ]; then + echo "::notice::No new commits since last nightly build, skipping this build." + echo "skippable=true" >> $GITHUB_OUTPUT + fi + else + echo "skippable=false" >> $GITHUB_OUTPUT + fi build-macos: - uses: ./.github/workflows/build-macos.yml + needs: check-skippable + if: needs.check-skippable.outputs.skip != 'true' + permissions: + actions: read + contents: write + uses: ./.github/workflows/cppcmake-macos.yml secrets: inherit + with: + NIGHTLY: ${{ github.event_name == 'schedule' || inputs.NIGHTLY == true }} + CONSOLIDATE_DEPENDENCY_SUMMARY: true build-ubuntu: - uses: ./.github/workflows/build-ubuntu.yml - secrets: inherit + needs: check-skippable + if: needs.check-skippable.outputs.skip != 'true' + permissions: + actions: read + contents: write + uses: ./.github/workflows/cppcmake-ubuntu.yml + with: + NIGHTLY: ${{ github.event_name == 'schedule' || inputs.NIGHTLY == true }} + CONSOLIDATE_DEPENDENCY_SUMMARY: true build-windows: - uses: ./.github/workflows/build-windows.yml + needs: check-skippable + if: needs.check-skippable.outputs.skip != 'true' + permissions: + actions: read + contents: write + uses: ./.github/workflows/cppcmake-windows.yml + secrets: inherit + with: + NIGHTLY: ${{ github.event_name == 'schedule' || inputs.NIGHTLY == true }} + CONSOLIDATE_DEPENDENCY_SUMMARY: true + + build-windows_on_arm: + needs: check-skippable + if: needs.check-skippable.outputs.skip != 'true' + permissions: + actions: read + contents: write + uses: ./.github/workflows/cppcmake-windows_on_arm.yml secrets: inherit + with: + NIGHTLY: ${{ github.event_name == 'schedule' || inputs.NIGHTLY == true }} + CONSOLIDATE_DEPENDENCY_SUMMARY: true + + summarize-dependencies: + needs: + [ + check-skippable, + build-macos, + build-ubuntu, + build-windows, + build-windows_on_arm, + ] + if: always() && needs.check-skippable.outputs.skip != 'true' && !cancelled() + name: Summarize Dependencies + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: read + steps: + - name: Download dependency summaries + uses: actions/download-artifact@v8 + continue-on-error: true + with: + pattern: dependency-summary-* + path: dependency-summary + merge-multiple: true + + - name: Summary + run: | + { + echo "## Dependencies used" + echo "" + echo "| Workflow | Target | OpenSSL | Qt | SQLCipher | SQLite |" + echo "| :---: | :---: | :---: | :---: | :---: | :---: |" + } >> "$GITHUB_STEP_SUMMARY" + + if [ -d dependency-summary ] && find dependency-summary -type f -name '*.md' | grep -q .; then + find dependency-summary -type f -name '*.md' -print | sort | while IFS= read -r file; do + cat "$file" >> "$GITHUB_STEP_SUMMARY" + done + else + echo "| Not available | No dependency summaries were uploaded. | - | - | - | - |" >> "$GITHUB_STEP_SUMMARY" + fi release: if: github.event_name != 'pull_request' - needs: [build-appimage, build-macos, build-windows] + needs: [build-macos, build-ubuntu, build-windows, build-windows_on_arm] + name: Release + permissions: + actions: read + contents: write uses: ./.github/workflows/release.yml - secrets: inherit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 80e773889e..5b54f1e840 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,49 +1,118 @@ -name: Release +name: Release Artifacts on: - workflow_call: - inputs: - NIGHTLY: - default: false - type: boolean - -env: - tag_name: ${{ inputs.NIGHTLY == true && 'nightly' || 'continuous' }} + workflow_call: jobs: release: - runs-on: ubuntu-latest + name: Release + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: write + concurrency: + group: release-${{ github.repository }}-${{ contains(github.workflow_ref, '/cppcmake.yml@') && ((github.event_name == 'schedule' || github.event.inputs.NIGHTLY == 'true') && 'nightly' || 'continuous') || format('{0}-{1}', github.workflow, github.sha) }} + cancel-in-progress: false + env: + GH_TOKEN: ${{ github.token }} steps: - - name: Delete existing tag and release - uses: dev-drprasad/delete-tag-and-release@v1.1 - with: - delete_release: true - github_token: ${{ secrets.GITHUB_TOKEN }} - tag_name: ${{ env.tag_name }} + - name: Set release tag + id: release-tag + run: | + case "${{ github.workflow_ref }}" in + "$GITHUB_REPOSITORY/.github/workflows/cppcmake.yml@"*) + if [ "${{ github.event_name }}" = "schedule" ] || [ "${{ github.event.inputs.NIGHTLY }}" = "true" ]; then + TAG_NAME="nightly" + else + TAG_NAME="continuous" + fi + ;; + "$GITHUB_REPOSITORY/.github/workflows/cppcmake-ubuntu.yml@"*) + TAG_NAME="${GITHUB_SHA}-ubuntu" + ;; + "$GITHUB_REPOSITORY/.github/workflows/cppcmake-macos.yml@"*) + TAG_NAME="${GITHUB_SHA}-macos" + ;; + "$GITHUB_REPOSITORY/.github/workflows/cppcmake-windows.yml@"*) + TAG_NAME="${GITHUB_SHA}-windows" + ;; + "$GITHUB_REPOSITORY/.github/workflows/cppcmake-windows_on_arm.yml@"*) + TAG_NAME="${GITHUB_SHA}-windows-on-arm" + ;; + *) + echo "::error::Unsupported release caller workflow: ${{ github.workflow_ref }}" + exit 1 + ;; + esac + + echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT" - - run: mkdir target + - run: mkdir -v target release - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: target - - name: Remove unsigned Windows build - run: rm -rfv target/*unsigned* + - name: Remove dependency summary artifacts + run: find target -depth -name 'dependency-summary-*' -exec rm -rfv {} + + + - name: Remove unsigned Windows artifacts + run: find target -depth -name '*unsigned*' -exec rm -rfv {} + + + - name: Flatten artifacts + run: find target -type f -exec mv -v {} release \; + + - name: Check release files + run: | + if ! find release -type f | grep -q .; then + echo "::error::No release files were prepared." + exit 1 + fi - - run: find target -type f -exec mv -v {} target \; + - name: Check if release is still current + id: publish-guard + run: | + case "${{ github.workflow_ref }}" in + "$GITHUB_REPOSITORY/.github/workflows/cppcmake.yml@"*) + ;; + *) + echo "releasable=true" >> "$GITHUB_OUTPUT" + exit 0 + ;; + esac - - name: Unarchive Windows's build artifacts - run: for f in target/*.zip; do unzip -d target/ "$f"; done + REMOTE_SHA=$(git ls-remote "https://github.com/${GITHUB_REPOSITORY}.git" "refs/heads/${{ github.ref_name }}" | awk '{print $1}') - - name: Remove archived Windows's build artifacts - run: rm -v target/build-artifacts-*.zip + if [ -z "$REMOTE_SHA" ]; then + echo "::warning::Unable to resolve the latest commit for refs/heads/${{ github.ref_name }} right before publishing. Skipping release." + echo "releasable=false" >> "$GITHUB_OUTPUT" + exit 0 + fi - - name: Release + if [ "$REMOTE_SHA" = "$GITHUB_SHA" ]; then + echo "releasable=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "::notice::Skipping release for ${GITHUB_SHA} because a newer commit exists on refs/heads/${{ github.ref_name }} (${REMOTE_SHA}) right before publishing." + echo "releasable=false" >> "$GITHUB_OUTPUT" + + - if: steps.publish-guard.outputs.releasable != 'true' + name: Summarize skipped publish + run: | + echo "## Publish skipped" >> "$GITHUB_STEP_SUMMARY" + echo "This run finished building, but a newer commit appeared while preparing artifacts, so the shared \`${{ steps.release-tag.outputs.tag_name }}\` release was left untouched." >> "$GITHUB_STEP_SUMMARY" + + - if: steps.publish-guard.outputs.releasable == 'true' + name: Delete existing tag and release + run: gh release delete ${{ steps.release-tag.outputs.tag_name }} --cleanup-tag --yes --repo $GITHUB_REPOSITORY + continue-on-error: true + + - if: steps.publish-guard.outputs.releasable == 'true' + name: Release uses: softprops/action-gh-release@v2 with: - files: target/* + files: release/* prerelease: true - tag_name: ${{ env.tag_name }} - - # For reference: Uploading nightly builds to the nightly server is processed on the nightly server side. + tag_name: ${{ steps.release-tag.outputs.tag_name }} diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index 8e4f5b8594..4f5da2bfd3 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -4,6 +4,9 @@ on: release: types: [released] +permissions: + contents: read + jobs: publish: runs-on: ubuntu-latest @@ -11,5 +14,5 @@ jobs: - uses: vedantmgoyal9/winget-releaser@main with: identifier: DBBrowserForSQLite.DBBrowserForSQLite - installers-regex: '\.msi$' # Only .msi files + installers-regex: '\.msi$' # Only .msi files token: ${{ secrets.WINGET_TOKEN }} diff --git a/BUILDING.md b/BUILDING.md index 5d6761ae5b..d8805a9e3e 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -164,7 +164,7 @@ mv DB\ Browser\ for\ SQLite.app /Applications An icon for "DB Browser for SQLite" should now be in your main macOS Applications list, ready to launch. -> Also, we have a CI workflow for macOS, you can check it out [here](https://github.com/sqlitebrowser/sqlitebrowser/blob/master/.github/workflows/build-macos.yml) +> Also, we have a CI workflow for macOS, you can check it out [here](https://github.com/sqlitebrowser/sqlitebrowser/blob/master/.github/workflows/cppcmake-macos.yml) ### Windows #### Compiling on Windows with MSVC diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e7610c352..1a17f534aa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,12 +144,8 @@ target_sources(${PROJECT_NAME} src/FileDialog.h src/ColumnDisplayFormatDialog.h src/FilterLineEdit.h - src/RemoteDatabase.h src/ForeignKeyEditorDelegate.h src/PlotDock.h - src/RemoteDock.h - src/RemoteModel.h - src/RemotePushDialog.h src/FindReplaceDialog.h src/ExtendedScintilla.h src/FileExtensionManager.h @@ -162,8 +158,6 @@ target_sources(${PROJECT_NAME} src/SelectItemsPopup.h src/TableBrowser.h src/ImageViewer.h - src/RemoteLocalFilesModel.h - src/RemoteCommitsModel.h src/RemoteNetwork.h src/TableBrowserDock.h ) @@ -203,12 +197,8 @@ target_sources(${PROJECT_NAME} src/FileDialog.cpp src/ColumnDisplayFormatDialog.cpp src/FilterLineEdit.cpp - src/RemoteDatabase.cpp src/ForeignKeyEditorDelegate.cpp src/PlotDock.cpp - src/RemoteDock.cpp - src/RemoteModel.cpp - src/RemotePushDialog.cpp src/FindReplaceDialog.cpp src/ExtendedScintilla.cpp src/FileExtensionManager.cpp @@ -226,8 +216,6 @@ target_sources(${PROJECT_NAME} src/sql/parser/sqlite3_lexer.cpp src/sql/parser/sqlite3_parser.cpp src/ImageViewer.cpp - src/RemoteLocalFilesModel.cpp - src/RemoteCommitsModel.cpp src/RemoteNetwork.cpp src/TableBrowserDock.cpp ) @@ -250,8 +238,6 @@ target_sources(${PROJECT_NAME} src/ExportSqlDialog.ui src/ColumnDisplayFormatDialog.ui src/PlotDock.ui - src/RemoteDock.ui - src/RemotePushDialog.ui src/FindReplaceDialog.ui src/FileExtensionManager.ui src/CondFormatManager.ui @@ -268,7 +254,6 @@ target_sources(${PROJECT_NAME} PRIVATE # General - src/certs/CaCerts.qrc src/sql/parser/sqlite3_parser.yy src/sql/parser/sqlite3_lexer.ll diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000000..e6b170b125 --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,5 @@ +Current Maintainers: + - You can check the list of maintainers at: https://github.com/orgs/sqlitebrowser/people + +Release Manager: + - SeongTae Jeong seongtaejg@sqlitebrowser.org 17ABC291B166F699409851AA9503B162E0416CE3 diff --git a/README.md b/README.md index 03be86bb46..0b327abb65 100644 --- a/README.md +++ b/README.md @@ -267,7 +267,7 @@ or this command: #### Snap Nightlies - snap install sqlitebrowser --devmode + snap install sqlitebrowser --edge #### Snap Stable diff --git a/cmake/FindQCustomPlot.cmake b/cmake/FindQCustomPlot.cmake index 7429ccfb30..abb2eb1c26 100644 --- a/cmake/FindQCustomPlot.cmake +++ b/cmake/FindQCustomPlot.cmake @@ -31,7 +31,7 @@ # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -find_library(QCustomPlot_LIBRARY NAMES qcustomplot qcustomplot-qt5) +find_library(QCustomPlot_LIBRARY NAMES qcustomplot qcustomplot-qt5 QCustomPlot) set(QCustomPlot_LIBRARIES "${QCustomPlot_LIBRARY}") find_path(QCustomPlot_INCLUDE_DIR qcustomplot.h) diff --git a/config/install.cmake b/config/install.cmake index 5c538cf927..c10c608f73 100644 --- a/config/install.cmake +++ b/config/install.cmake @@ -19,7 +19,7 @@ if(UNIX) DESTINATION ${CMAKE_INSTALL_DATADIR}/applications/ ) - install(FILES distri/${PROJECT_NAME}.desktop.appdata.xml + install(FILES distri/${PROJECT_NAME}.metainfo.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo/ ) endif() diff --git a/distri/sqlitebrowser.desktop b/distri/sqlitebrowser.desktop index b90b244799..965211ce4c 100644 --- a/distri/sqlitebrowser.desktop +++ b/distri/sqlitebrowser.desktop @@ -4,6 +4,7 @@ Comment=DB Browser for SQLite is a light GUI editor for SQLite databases Comment[de]=DB Browser for SQLite ist ein GUI-Editor für SQLite-Datenbanken Comment[fr]=Un éditeur graphique léger pour les bases de données SQLite Comment[es]=«DB Browser for SQLite» es un editor gráfico de bases de datos SQLite +Keywords=sqlite;database;browser; Exec=sqlitebrowser %f Icon=sqlitebrowser Terminal=false diff --git a/distri/sqlitebrowser.desktop.appdata.xml b/distri/sqlitebrowser.metainfo.xml similarity index 74% rename from distri/sqlitebrowser.desktop.appdata.xml rename to distri/sqlitebrowser.metainfo.xml index 0d41d16cda..246144f6df 100644 --- a/distri/sqlitebrowser.desktop.appdata.xml +++ b/distri/sqlitebrowser.metainfo.xml @@ -1,11 +1,13 @@ - sqlitebrowser.desktop + org.sqlitebrowser.desktop CC0-1.0 MPL-2.0 and GPL-3.0+ - DB Browser for SQLite developers + + DB Browser for SQLite developers + DB Browser for SQLite - light GUI editor for SQLite databases + Light GUI editor for SQLite databases

DB Browser for SQLite is a high quality, visual, open source tool to create, design, and edit database files compatible with SQLite.

It is for users and developers wanting to create databases, search, and edit data. It uses a familiar spreadsheet-like interface, and you don't need to learn complicated SQL commands.

@@ -44,10 +46,18 @@ https://sqlitebrowser.org/ https://github.com/sqlitebrowser/sqlitebrowser/issues + https://github.com/sqlitebrowser/sqlitebrowser/wiki/Frequently-Asked-Questions + https://github.com/sqlitebrowser/sqlitebrowser/wiki + https://www.patreon.com/db4s + https://github.com/sqlitebrowser/sqlitebrowser/wiki/Translations + https://github.com/sqlitebrowser/sqlitebrowser/wiki#for-developers + https://github.com/sqlitebrowser/sqlitebrowser - + + + sqlitebrowser.desktop
diff --git a/installer/macos/macapp-nightly.icns b/installer/macos/macapp-nightly.icns index 23f2d1b367..60aa2030e2 100644 Binary files a/installer/macos/macapp-nightly.icns and b/installer/macos/macapp-nightly.icns differ diff --git a/installer/macos/macapp.icns b/installer/macos/macapp.icns index 33cfe25c55..3300679d3a 100644 Binary files a/installer/macos/macapp.icns and b/installer/macos/macapp.icns differ diff --git a/installer/macos/notarize.sh b/installer/macos/notarize.sh index 7cb2095589..04d8d0f561 100644 --- a/installer/macos/notarize.sh +++ b/installer/macos/notarize.sh @@ -84,7 +84,7 @@ mv build/*.app installer/macos # Create the DMG export DATE=$(date +%Y%m%d) -if [ "$SQLCIPHER" = "1" ]; then +if [ "$SQLCIPHER" = "true" ]; then if [ "$NIGHTLY" = "false" ]; then # Continuous with SQLCipher sed -i "" 's/"DB Browser for SQLCipher Nightly.app"/"DB Browser for SQLCipher-dev-'$(git rev-parse --short --verify HEAD)'.app"/' installer/macos/sqlcipher-nightly.json diff --git a/installer/other/get_nightlies_from_github_actions.sh b/installer/other/get_nightlies_from_github_actions.sh deleted file mode 100644 index 8e6f554b9c..0000000000 --- a/installer/other/get_nightlies_from_github_actions.sh +++ /dev/null @@ -1,56 +0,0 @@ -# SPDX-FileCopyrightText: (C) 2024 SeongTae Jeong -# This script downloads the daily build output from GitHub Release, built by GitHub Actions, and archives it on our nightly server. - -#!/usr/bin/env bash - -source /root/.gh_token_secure - -set -ex - -echo "$(TZ=UTC date +"%Y-%m-%d %H:%M:%S %Z"): [START]" -echo "Clear the incoming directory" -DOWNLOAD_DIR="/tmp/incoming/" -rm -rfv $DOWNLOAD_DIR -mkdir -v $DOWNLOAD_DIR - -if [ $(ls -l /nightlies/macos-universal | grep -c "$(date +%Y%m%d)") ] && - [ $(ls -l /nightlies/win32 /nightlies/win64 | grep -c "$(date +%Y-%m-%d)") -ne 0 ]; then - echo "Nightly build already exists" - exit 1 -fi - -if ! gh auth login --with-token <<< "$GH_TOKEN"; then - echo "Unable to authenticate with GitHub" -fi -echo "Successfully authenticated with GitHub" - -IS_BUILD_SUCCESS=$(gh run list --created $(date '+%Y-%m-%d') --limit 1 --status "success" --workflow "cppcmake-nightly.yml" --repo "sqlitebrowser/sqlitebrowser" | wc -l) -if [ $IS_BUILD_SUCCESS -eq 0 ]; then - echo "No successful build found" - exit 1 -fi -echo "Found a successful build" - -if ! gh release download --dir /tmp/incoming/ -R "sqlitebrowser/sqlitebrowser" nightly; then - echo "Unable to download the nightly build" -fi -echo "Successfully downloaded the nightly build" - -mv -v $DOWNLOAD_DIR*win32* /nightlies/win32/ -mv -v $DOWNLOAD_DIR*win64* /nightlies/win64/ -mv -v $DOWNLOAD_DIR*dmg /nightlies/macos-universal/ - -rm -v /nightlies/latest/*.dmg -rm -v /nightlies/latest/*.msi -rm -v /nightlies/latest/*.zip - -DATE=$(date +%Y%m%d) -ln -sv /nightlies/macos-universal/DB.Browser.for.SQLCipher-universal_$DATE.dmg /nightlies/latest/DB.Browser.for.SQLCipher-universal.dmg -ln -sv /nightlies/macos-universal/DB.Browser.for.SQLite-universal_$DATE.dmg /nightlies/latest/DB.Browser.for.SQLite-universal.dmg -DATE=$(date +%Y-%m-%d) -ln -sv /nightlies/win32/DB.Browser.for.SQLite-$DATE-win32.msi /nightlies/latest/DB.Browser.for.SQLite-win32.msi -ln -sv /nightlies/win32/DB.Browser.for.SQLite-$DATE-win32.zip /nightlies/latest/DB.Browser.for.SQLite-win32.zip -ln -sv /nightlies/win64/DB.Browser.for.SQLite-$DATE-win64.msi /nightlies/latest/DB.Browser.for.SQLite-win64.msi -ln -sv /nightlies/win64/DB.Browser.for.SQLite-$DATE-win64.zip /nightlies/latest/DB.Browser.for.SQLite-win64.zip - -echo "[STOP]" diff --git a/installer/other/move_nightlies_into_dirs.sh b/installer/other/move_nightlies_into_dirs.sh deleted file mode 100644 index 9e5d8ad6e1..0000000000 --- a/installer/other/move_nightlies_into_dirs.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -# Moving the nightly builds into appropriate subdirs. Designed to be -# run automatically from cron, using something like this: -# 10 0 14 * * /usr/local/bin/move_nightlies_into_dirs.sh - -# Retrieve the month number for last month -YEARMONTH=`date -d "last month 13:00" '+%Y-%m'` -YEARMONTHOSX=`date -d "last month 13:00" '+%Y%m'` - -# Create appropriate new subfolders -mkdir /nightlies/macos-universal/${YEARMONTH} -mkdir /nightlies/win32/${YEARMONTH} -mkdir /nightlies/win64/${YEARMONTH} - -# Move builds -mv /nightlies/macos-universal/DB*${YEARMONTHOSX}* /nightlies/macos-universal/night*${YEARMONTHOSX}* /nightlies/macos-universal/${YEARMONTH}/ -mv /nightlies/win32/DB*${YEARMONTH}* /nightlies/win32/${YEARMONTH}/ -mv /nightlies/win64/DB*${YEARMONTH}* /nightlies/win64/${YEARMONTH}/ - -# Fix ownership and SELinux context's -chown -Rh nightlies: /nightlies/macos-universal/${YEARMONTH} /nightlies/win32/${YEARMONTH} /nightlies/win64/${YEARMONTH} - -echo Nightlies moved for $YEARMONTH diff --git a/installer/windows/build.cmd b/installer/windows/build.cmd index af86b2b67c..345b73843d 100644 --- a/installer/windows/build.cmd +++ b/installer/windows/build.cmd @@ -7,9 +7,9 @@ set MSI=DB.Browser.for.SQLite-%1 if "%1"=="" ( echo ERROR: You must select a build type, either "win64" or "win32" goto :eof -) else if "%1"=="win32" ( +) else if "%1"=="x86" ( set ARCH=x86 -) else if "%1"=="win64" ( +) else if "%1"=="x64" ( set ARCH=x64 ) else ( echo ERROR: Unknown build type="%1" diff --git a/installer/windows/product.wxs b/installer/windows/product.wxs index df52263f3b..385895a81f 100644 --- a/installer/windows/product.wxs +++ b/installer/windows/product.wxs @@ -61,9 +61,11 @@ - - + + + + @@ -84,6 +86,9 @@ + + + @@ -143,14 +148,11 @@ - - - - - + + @@ -169,6 +171,7 @@ + @@ -186,6 +189,9 @@ + + + diff --git a/installer/windows/translations.wxs b/installer/windows/translations.wxs index f842e05b44..0743a202c0 100644 --- a/installer/windows/translations.wxs +++ b/installer/windows/translations.wxs @@ -97,7 +97,7 @@ - + @@ -197,7 +197,7 @@ - + diff --git a/installer/windows/variables.wxi b/installer/windows/variables.wxi index fbedf0c318..8fc77959a8 100644 --- a/installer/windows/variables.wxi +++ b/installer/windows/variables.wxi @@ -40,8 +40,8 @@ Visual Studio 2017. The build "ARCH" will be set automatically. --> - - + + - - - - - - - - - - - - - - - + + + + + + + diff --git a/installer/windows_on_arm/background.bmp b/installer/windows_on_arm/background.bmp new file mode 100644 index 0000000000..821cb57373 Binary files /dev/null and b/installer/windows_on_arm/background.bmp differ diff --git a/installer/windows_on_arm/banner.bmp b/installer/windows_on_arm/banner.bmp new file mode 100644 index 0000000000..b4898aa187 Binary files /dev/null and b/installer/windows_on_arm/banner.bmp differ diff --git a/installer/windows_on_arm/build.cmd b/installer/windows_on_arm/build.cmd new file mode 100644 index 0000000000..8a988c4909 --- /dev/null +++ b/installer/windows_on_arm/build.cmd @@ -0,0 +1,28 @@ +@echo off + +set "WIX=C:\Program Files (x86)\WiX Toolset v3.14\bin" + +:: Output file name +set MSI=DB.Browser.for.SQLite-%1 + +set ARCH=arm64 + +:: Suppress some ICE checks +:: - 61 (major upgrade) +:: - 03 & 82 (merge module) +:: - 38 & 43 & 57 (non-advertised shortcuts) +set ICE=-sice:ICE03 -sice:ICE82 -sice:ICE61 -sice:ICE38 -sice:ICE43 -sice:ICE57 + +:: Suppress 'light.exe' warning +:: - 1104 (vcredist merge module installer version) +set LIGHT=-sw1104 + +:: Compile & Link +"%WIX%\candle.exe" -nologo -pedantic -arch %ARCH% product.wxs translations.wxs ui.wxs +"%WIX%\light.exe" -sval -nologo -pedantic %LIGHT% %ICE% -ext WixUIExtension -ext WixUtilExtension -cultures:en-us -loc strings.wxl product.wixobj translations.wixobj ui.wixobj -out %MSI%.msi + +:: Cleanup +del product.wixobj +del translations.wixobj +del ui.wixobj +del %MSI%.wixpdb diff --git a/installer/windows_on_arm/license.rtf b/installer/windows_on_arm/license.rtf new file mode 100644 index 0000000000..2601ae9054 --- /dev/null +++ b/installer/windows_on_arm/license.rtf @@ -0,0 +1,1179 @@ +{\rtf1\ansi\ansicpg1252\cocoartf2761 +\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Arial-BoldMT;\f1\fswiss\fcharset0 ArialMT;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\paperw11900\paperh16840\margl1440\margr1440\vieww16560\viewh8400\viewkind0 +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 + +\f0\b\fs28 \cf0 DB Browser for SQLite is bi-licensed under the Mozilla Public License\ +Version 2, as well as the GNU General Public License Version 3 or later.\ +\ +Modification or redistribution is permitted under the conditions of these licenses.\ +\ +Check `LICENSE-GPL-3.0` for the full text of the GNU General Public License Version 3.\ +Check `LICENSE-MPL-2.0` for the full text of the Mozilla Public License Version 2.\ +Check `LICENSE-MIT` for the full text of the MIT License. and that is the license for the `nalgeon/sqlean` library.\ +Check `LICENSE-PLUGINS` for other rights regarding included third-party resources. +\f1\b0\fs24 \ +\ + +\f0\b\fs26 LICENSE-GPL-3.0 +\f1\b0\fs24 \ + GNU GENERAL PUBLIC LICENSE\ + Version 3, 29 June 2007\ +\ + Copyright (C) 2007 Free Software Foundation, Inc. \ + Everyone is permitted to copy and distribute verbatim copies\ + of this license document, but changing it is not allowed.\ +\ + Preamble\ +\ + The GNU General Public License is a free, copyleft license for\ +software and other kinds of works.\ +\ + The licenses for most software and other practical works are designed\ +to take away your freedom to share and change the works. By contrast,\ +the GNU General Public License is intended to guarantee your freedom to\ +share and change all versions of a program--to make sure it remains free\ +software for all its users. We, the Free Software Foundation, use the\ +GNU General Public License for most of our software; it applies also to\ +any other work released this way by its authors. You can apply it to\ +your programs, too.\ +\ + When we speak of free software, we are referring to freedom, not\ +price. Our General Public Licenses are designed to make sure that you\ +have the freedom to distribute copies of free software (and charge for\ +them if you wish), that you receive source code or can get it if you\ +want it, that you can change the software or use pieces of it in new\ +free programs, and that you know you can do these things.\ +\ + To protect your rights, we need to prevent others from denying you\ +these rights or asking you to surrender the rights. Therefore, you have\ +certain responsibilities if you distribute copies of the software, or if\ +you modify it: responsibilities to respect the freedom of others.\ +\ + For example, if you distribute copies of such a program, whether\ +gratis or for a fee, you must pass on to the recipients the same\ +freedoms that you received. You must make sure that they, too, receive\ +or can get the source code. And you must show them these terms so they\ +know their rights.\ +\ + Developers that use the GNU GPL protect your rights with two steps:\ +(1) assert copyright on the software, and (2) offer you this License\ +giving you legal permission to copy, distribute and/or modify it.\ +\ + For the developers' and authors' protection, the GPL clearly explains\ +that there is no warranty for this free software. For both users' and\ +authors' sake, the GPL requires that modified versions be marked as\ +changed, so that their problems will not be attributed erroneously to\ +authors of previous versions.\ +\ + Some devices are designed to deny users access to install or run\ +modified versions of the software inside them, although the manufacturer\ +can do so. This is fundamentally incompatible with the aim of\ +protecting users' freedom to change the software. The systematic\ +pattern of such abuse occurs in the area of products for individuals to\ +use, which is precisely where it is most unacceptable. Therefore, we\ +have designed this version of the GPL to prohibit the practice for those\ +products. If such problems arise substantially in other domains, we\ +stand ready to extend this provision to those domains in future versions\ +of the GPL, as needed to protect the freedom of users.\ +\ + Finally, every program is threatened constantly by software patents.\ +States should not allow patents to restrict development and use of\ +software on general-purpose computers, but in those that do, we wish to\ +avoid the special danger that patents applied to a free program could\ +make it effectively proprietary. To prevent this, the GPL assures that\ +patents cannot be used to render the program non-free.\ +\ + The precise terms and conditions for copying, distribution and\ +modification follow.\ +\ + TERMS AND CONDITIONS\ +\ + 0. Definitions.\ +\ + "This License" refers to version 3 of the GNU General Public License.\ +\ + "Copyright" also means copyright-like laws that apply to other kinds of\ +works, such as semiconductor masks.\ +\ + "The Program" refers to any copyrightable work licensed under this\ +License. Each licensee is addressed as "you". "Licensees" and\ +"recipients" may be individuals or organizations.\ +\ + To "modify" a work means to copy from or adapt all or part of the work\ +in a fashion requiring copyright permission, other than the making of an\ +exact copy. The resulting work is called a "modified version" of the\ +earlier work or a work "based on" the earlier work.\ +\ + A "covered work" means either the unmodified Program or a work based\ +on the Program.\ +\ + To "propagate" a work means to do anything with it that, without\ +permission, would make you directly or secondarily liable for\ +infringement under applicable copyright law, except executing it on a\ +computer or modifying a private copy. Propagation includes copying,\ +distribution (with or without modification), making available to the\ +public, and in some countries other activities as well.\ +\ + To "convey" a work means any kind of propagation that enables other\ +parties to make or receive copies. Mere interaction with a user through\ +a computer network, with no transfer of a copy, is not conveying.\ +\ + An interactive user interface displays "Appropriate Legal Notices"\ +to the extent that it includes a convenient and prominently visible\ +feature that (1) displays an appropriate copyright notice, and (2)\ +tells the user that there is no warranty for the work (except to the\ +extent that warranties are provided), that licensees may convey the\ +work under this License, and how to view a copy of this License. If\ +the interface presents a list of user commands or options, such as a\ +menu, a prominent item in the list meets this criterion.\ +\ + 1. Source Code.\ +\ + The "source code" for a work means the preferred form of the work\ +for making modifications to it. "Object code" means any non-source\ +form of a work.\ +\ + A "Standard Interface" means an interface that either is an official\ +standard defined by a recognized standards body, or, in the case of\ +interfaces specified for a particular programming language, one that\ +is widely used among developers working in that language.\ +\ + The "System Libraries" of an executable work include anything, other\ +than the work as a whole, that (a) is included in the normal form of\ +packaging a Major Component, but which is not part of that Major\ +Component, and (b) serves only to enable use of the work with that\ +Major Component, or to implement a Standard Interface for which an\ +implementation is available to the public in source code form. A\ +"Major Component", in this context, means a major essential component\ +(kernel, window system, and so on) of the specific operating system\ +(if any) on which the executable work runs, or a compiler used to\ +produce the work, or an object code interpreter used to run it.\ +\ + The "Corresponding Source" for a work in object code form means all\ +the source code needed to generate, install, and (for an executable\ +work) run the object code and to modify the work, including scripts to\ +control those activities. However, it does not include the work's\ +System Libraries, or general-purpose tools or generally available free\ +programs which are used unmodified in performing those activities but\ +which are not part of the work. For example, Corresponding Source\ +includes interface definition files associated with source files for\ +the work, and the source code for shared libraries and dynamically\ +linked subprograms that the work is specifically designed to require,\ +such as by intimate data communication or control flow between those\ +subprograms and other parts of the work.\ +\ + The Corresponding Source need not include anything that users\ +can regenerate automatically from other parts of the Corresponding\ +Source.\ +\ + The Corresponding Source for a work in source code form is that\ +same work.\ +\ + 2. Basic Permissions.\ +\ + All rights granted under this License are granted for the term of\ +copyright on the Program, and are irrevocable provided the stated\ +conditions are met. This License explicitly affirms your unlimited\ +permission to run the unmodified Program. The output from running a\ +covered work is covered by this License only if the output, given its\ +content, constitutes a covered work. This License acknowledges your\ +rights of fair use or other equivalent, as provided by copyright law.\ +\ + You may make, run and propagate covered works that you do not\ +convey, without conditions so long as your license otherwise remains\ +in force. You may convey covered works to others for the sole purpose\ +of having them make modifications exclusively for you, or provide you\ +with facilities for running those works, provided that you comply with\ +the terms of this License in conveying all material for which you do\ +not control copyright. Those thus making or running the covered works\ +for you must do so exclusively on your behalf, under your direction\ +and control, on terms that prohibit them from making any copies of\ +your copyrighted material outside their relationship with you.\ +\ + Conveying under any other circumstances is permitted solely under\ +the conditions stated below. Sublicensing is not allowed; section 10\ +makes it unnecessary.\ +\ + 3. Protecting Users' Legal Rights From Anti-Circumvention Law.\ +\ + No covered work shall be deemed part of an effective technological\ +measure under any applicable law fulfilling obligations under article\ +11 of the WIPO copyright treaty adopted on 20 December 1996, or\ +similar laws prohibiting or restricting circumvention of such\ +measures.\ +\ + When you convey a covered work, you waive any legal power to forbid\ +circumvention of technological measures to the extent such circumvention\ +is effected by exercising rights under this License with respect to\ +the covered work, and you disclaim any intention to limit operation or\ +modification of the work as a means of enforcing, against the work's\ +users, your or third parties' legal rights to forbid circumvention of\ +technological measures.\ +\ + 4. Conveying Verbatim Copies.\ +\ + You may convey verbatim copies of the Program's source code as you\ +receive it, in any medium, provided that you conspicuously and\ +appropriately publish on each copy an appropriate copyright notice;\ +keep intact all notices stating that this License and any\ +non-permissive terms added in accord with section 7 apply to the code;\ +keep intact all notices of the absence of any warranty; and give all\ +recipients a copy of this License along with the Program.\ +\ + You may charge any price or no price for each copy that you convey,\ +and you may offer support or warranty protection for a fee.\ +\ + 5. Conveying Modified Source Versions.\ +\ + You may convey a work based on the Program, or the modifications to\ +produce it from the Program, in the form of source code under the\ +terms of section 4, provided that you also meet all of these conditions:\ +\ + a) The work must carry prominent notices stating that you modified\ + it, and giving a relevant date.\ +\ + b) The work must carry prominent notices stating that it is\ + released under this License and any conditions added under section\ + 7. This requirement modifies the requirement in section 4 to\ + "keep intact all notices".\ +\ + c) You must license the entire work, as a whole, under this\ + License to anyone who comes into possession of a copy. This\ + License will therefore apply, along with any applicable section 7\ + additional terms, to the whole of the work, and all its parts,\ + regardless of how they are packaged. This License gives no\ + permission to license the work in any other way, but it does not\ + invalidate such permission if you have separately received it.\ +\ + d) If the work has interactive user interfaces, each must display\ + Appropriate Legal Notices; however, if the Program has interactive\ + interfaces that do not display Appropriate Legal Notices, your\ + work need not make them do so.\ +\ + A compilation of a covered work with other separate and independent\ +works, which are not by their nature extensions of the covered work,\ +and which are not combined with it such as to form a larger program,\ +in or on a volume of a storage or distribution medium, is called an\ +"aggregate" if the compilation and its resulting copyright are not\ +used to limit the access or legal rights of the compilation's users\ +beyond what the individual works permit. Inclusion of a covered work\ +in an aggregate does not cause this License to apply to the other\ +parts of the aggregate.\ +\ + 6. Conveying Non-Source Forms.\ +\ + You may convey a covered work in object code form under the terms\ +of sections 4 and 5, provided that you also convey the\ +machine-readable Corresponding Source under the terms of this License,\ +in one of these ways:\ +\ + a) Convey the object code in, or embodied in, a physical product\ + (including a physical distribution medium), accompanied by the\ + Corresponding Source fixed on a durable physical medium\ + customarily used for software interchange.\ +\ + b) Convey the object code in, or embodied in, a physical product\ + (including a physical distribution medium), accompanied by a\ + written offer, valid for at least three years and valid for as\ + long as you offer spare parts or customer support for that product\ + model, to give anyone who possesses the object code either (1) a\ + copy of the Corresponding Source for all the software in the\ + product that is covered by this License, on a durable physical\ + medium customarily used for software interchange, for a price no\ + more than your reasonable cost of physically performing this\ + conveying of source, or (2) access to copy the\ + Corresponding Source from a network server at no charge.\ +\ + c) Convey individual copies of the object code with a copy of the\ + written offer to provide the Corresponding Source. This\ + alternative is allowed only occasionally and noncommercially, and\ + only if you received the object code with such an offer, in accord\ + with subsection 6b.\ +\ + d) Convey the object code by offering access from a designated\ + place (gratis or for a charge), and offer equivalent access to the\ + Corresponding Source in the same way through the same place at no\ + further charge. You need not require recipients to copy the\ + Corresponding Source along with the object code. If the place to\ + copy the object code is a network server, the Corresponding Source\ + may be on a different server (operated by you or a third party)\ + that supports equivalent copying facilities, provided you maintain\ + clear directions next to the object code saying where to find the\ + Corresponding Source. Regardless of what server hosts the\ + Corresponding Source, you remain obligated to ensure that it is\ + available for as long as needed to satisfy these requirements.\ +\ + e) Convey the object code using peer-to-peer transmission, provided\ + you inform other peers where the object code and Corresponding\ + Source of the work are being offered to the general public at no\ + charge under subsection 6d.\ +\ + A separable portion of the object code, whose source code is excluded\ +from the Corresponding Source as a System Library, need not be\ +included in conveying the object code work.\ +\ + A "User Product" is either (1) a "consumer product", which means any\ +tangible personal property which is normally used for personal, family,\ +or household purposes, or (2) anything designed or sold for incorporation\ +into a dwelling. In determining whether a product is a consumer product,\ +doubtful cases shall be resolved in favor of coverage. For a particular\ +product received by a particular user, "normally used" refers to a\ +typical or common use of that class of product, regardless of the status\ +of the particular user or of the way in which the particular user\ +actually uses, or expects or is expected to use, the product. A product\ +is a consumer product regardless of whether the product has substantial\ +commercial, industrial or non-consumer uses, unless such uses represent\ +the only significant mode of use of the product.\ +\ + "Installation Information" for a User Product means any methods,\ +procedures, authorization keys, or other information required to install\ +and execute modified versions of a covered work in that User Product from\ +a modified version of its Corresponding Source. The information must\ +suffice to ensure that the continued functioning of the modified object\ +code is in no case prevented or interfered with solely because\ +modification has been made.\ +\ + If you convey an object code work under this section in, or with, or\ +specifically for use in, a User Product, and the conveying occurs as\ +part of a transaction in which the right of possession and use of the\ +User Product is transferred to the recipient in perpetuity or for a\ +fixed term (regardless of how the transaction is characterized), the\ +Corresponding Source conveyed under this section must be accompanied\ +by the Installation Information. But this requirement does not apply\ +if neither you nor any third party retains the ability to install\ +modified object code on the User Product (for example, the work has\ +been installed in ROM).\ +\ + The requirement to provide Installation Information does not include a\ +requirement to continue to provide support service, warranty, or updates\ +for a work that has been modified or installed by the recipient, or for\ +the User Product in which it has been modified or installed. Access to a\ +network may be denied when the modification itself materially and\ +adversely affects the operation of the network or violates the rules and\ +protocols for communication across the network.\ +\ + Corresponding Source conveyed, and Installation Information provided,\ +in accord with this section must be in a format that is publicly\ +documented (and with an implementation available to the public in\ +source code form), and must require no special password or key for\ +unpacking, reading or copying.\ +\ + 7. Additional Terms.\ +\ + "Additional permissions" are terms that supplement the terms of this\ +License by making exceptions from one or more of its conditions.\ +Additional permissions that are applicable to the entire Program shall\ +be treated as though they were included in this License, to the extent\ +that they are valid under applicable law. If additional permissions\ +apply only to part of the Program, that part may be used separately\ +under those permissions, but the entire Program remains governed by\ +this License without regard to the additional permissions.\ +\ + When you convey a copy of a covered work, you may at your option\ +remove any additional permissions from that copy, or from any part of\ +it. (Additional permissions may be written to require their own\ +removal in certain cases when you modify the work.) You may place\ +additional permissions on material, added by you to a covered work,\ +for which you have or can give appropriate copyright permission.\ +\ + Notwithstanding any other provision of this License, for material you\ +add to a covered work, you may (if authorized by the copyright holders of\ +that material) supplement the terms of this License with terms:\ +\ + a) Disclaiming warranty or limiting liability differently from the\ + terms of sections 15 and 16 of this License; or\ +\ + b) Requiring preservation of specified reasonable legal notices or\ + author attributions in that material or in the Appropriate Legal\ + Notices displayed by works containing it; or\ +\ + c) Prohibiting misrepresentation of the origin of that material, or\ + requiring that modified versions of such material be marked in\ + reasonable ways as different from the original version; or\ +\ + d) Limiting the use for publicity purposes of names of licensors or\ + authors of the material; or\ +\ + e) Declining to grant rights under trademark law for use of some\ + trade names, trademarks, or service marks; or\ +\ + f) Requiring indemnification of licensors and authors of that\ + material by anyone who conveys the material (or modified versions of\ + it) with contractual assumptions of liability to the recipient, for\ + any liability that these contractual assumptions directly impose on\ + those licensors and authors.\ +\ + All other non-permissive additional terms are considered "further\ +restrictions" within the meaning of section 10. If the Program as you\ +received it, or any part of it, contains a notice stating that it is\ +governed by this License along with a term that is a further\ +restriction, you may remove that term. If a license document contains\ +a further restriction but permits relicensing or conveying under this\ +License, you may add to a covered work material governed by the terms\ +of that license document, provided that the further restriction does\ +not survive such relicensing or conveying.\ +\ + If you add terms to a covered work in accord with this section, you\ +must place, in the relevant source files, a statement of the\ +additional terms that apply to those files, or a notice indicating\ +where to find the applicable terms.\ +\ + Additional terms, permissive or non-permissive, may be stated in the\ +form of a separately written license, or stated as exceptions;\ +the above requirements apply either way.\ +\ + 8. Termination.\ +\ + You may not propagate or modify a covered work except as expressly\ +provided under this License. Any attempt otherwise to propagate or\ +modify it is void, and will automatically terminate your rights under\ +this License (including any patent licenses granted under the third\ +paragraph of section 11).\ +\ + However, if you cease all violation of this License, then your\ +license from a particular copyright holder is reinstated (a)\ +provisionally, unless and until the copyright holder explicitly and\ +finally terminates your license, and (b) permanently, if the copyright\ +holder fails to notify you of the violation by some reasonable means\ +prior to 60 days after the cessation.\ +\ + Moreover, your license from a particular copyright holder is\ +reinstated permanently if the copyright holder notifies you of the\ +violation by some reasonable means, this is the first time you have\ +received notice of violation of this License (for any work) from that\ +copyright holder, and you cure the violation prior to 30 days after\ +your receipt of the notice.\ +\ + Termination of your rights under this section does not terminate the\ +licenses of parties who have received copies or rights from you under\ +this License. If your rights have been terminated and not permanently\ +reinstated, you do not qualify to receive new licenses for the same\ +material under section 10.\ +\ + 9. Acceptance Not Required for Having Copies.\ +\ + You are not required to accept this License in order to receive or\ +run a copy of the Program. Ancillary propagation of a covered work\ +occurring solely as a consequence of using peer-to-peer transmission\ +to receive a copy likewise does not require acceptance. However,\ +nothing other than this License grants you permission to propagate or\ +modify any covered work. These actions infringe copyright if you do\ +not accept this License. Therefore, by modifying or propagating a\ +covered work, you indicate your acceptance of this License to do so.\ +\ + 10. Automatic Licensing of Downstream Recipients.\ +\ + Each time you convey a covered work, the recipient automatically\ +receives a license from the original licensors, to run, modify and\ +propagate that work, subject to this License. You are not responsible\ +for enforcing compliance by third parties with this License.\ +\ + An "entity transaction" is a transaction transferring control of an\ +organization, or substantially all assets of one, or subdividing an\ +organization, or merging organizations. If propagation of a covered\ +work results from an entity transaction, each party to that\ +transaction who receives a copy of the work also receives whatever\ +licenses to the work the party's predecessor in interest had or could\ +give under the previous paragraph, plus a right to possession of the\ +Corresponding Source of the work from the predecessor in interest, if\ +the predecessor has it or can get it with reasonable efforts.\ +\ + You may not impose any further restrictions on the exercise of the\ +rights granted or affirmed under this License. For example, you may\ +not impose a license fee, royalty, or other charge for exercise of\ +rights granted under this License, and you may not initiate litigation\ +(including a cross-claim or counterclaim in a lawsuit) alleging that\ +any patent claim is infringed by making, using, selling, offering for\ +sale, or importing the Program or any portion of it.\ +\ + 11. Patents.\ +\ + A "contributor" is a copyright holder who authorizes use under this\ +License of the Program or a work on which the Program is based. The\ +work thus licensed is called the contributor's "contributor version".\ +\ + A contributor's "essential patent claims" are all patent claims\ +owned or controlled by the contributor, whether already acquired or\ +hereafter acquired, that would be infringed by some manner, permitted\ +by this License, of making, using, or selling its contributor version,\ +but do not include claims that would be infringed only as a\ +consequence of further modification of the contributor version. For\ +purposes of this definition, "control" includes the right to grant\ +patent sublicenses in a manner consistent with the requirements of\ +this License.\ +\ + Each contributor grants you a non-exclusive, worldwide, royalty-free\ +patent license under the contributor's essential patent claims, to\ +make, use, sell, offer for sale, import and otherwise run, modify and\ +propagate the contents of its contributor version.\ +\ + In the following three paragraphs, a "patent license" is any express\ +agreement or commitment, however denominated, not to enforce a patent\ +(such as an express permission to practice a patent or covenant not to\ +sue for patent infringement). To "grant" such a patent license to a\ +party means to make such an agreement or commitment not to enforce a\ +patent against the party.\ +\ + If you convey a covered work, knowingly relying on a patent license,\ +and the Corresponding Source of the work is not available for anyone\ +to copy, free of charge and under the terms of this License, through a\ +publicly available network server or other readily accessible means,\ +then you must either (1) cause the Corresponding Source to be so\ +available, or (2) arrange to deprive yourself of the benefit of the\ +patent license for this particular work, or (3) arrange, in a manner\ +consistent with the requirements of this License, to extend the patent\ +license to downstream recipients. "Knowingly relying" means you have\ +actual knowledge that, but for the patent license, your conveying the\ +covered work in a country, or your recipient's use of the covered work\ +in a country, would infringe one or more identifiable patents in that\ +country that you have reason to believe are valid.\ +\ + If, pursuant to or in connection with a single transaction or\ +arrangement, you convey, or propagate by procuring conveyance of, a\ +covered work, and grant a patent license to some of the parties\ +receiving the covered work authorizing them to use, propagate, modify\ +or convey a specific copy of the covered work, then the patent license\ +you grant is automatically extended to all recipients of the covered\ +work and works based on it.\ +\ + A patent license is "discriminatory" if it does not include within\ +the scope of its coverage, prohibits the exercise of, or is\ +conditioned on the non-exercise of one or more of the rights that are\ +specifically granted under this License. You may not convey a covered\ +work if you are a party to an arrangement with a third party that is\ +in the business of distributing software, under which you make payment\ +to the third party based on the extent of your activity of conveying\ +the work, and under which the third party grants, to any of the\ +parties who would receive the covered work from you, a discriminatory\ +patent license (a) in connection with copies of the covered work\ +conveyed by you (or copies made from those copies), or (b) primarily\ +for and in connection with specific products or compilations that\ +contain the covered work, unless you entered into that arrangement,\ +or that patent license was granted, prior to 28 March 2007.\ +\ + Nothing in this License shall be construed as excluding or limiting\ +any implied license or other defenses to infringement that may\ +otherwise be available to you under applicable patent law.\ +\ + 12. No Surrender of Others' Freedom.\ +\ + If conditions are imposed on you (whether by court order, agreement or\ +otherwise) that contradict the conditions of this License, they do not\ +excuse you from the conditions of this License. If you cannot convey a\ +covered work so as to satisfy simultaneously your obligations under this\ +License and any other pertinent obligations, then as a consequence you may\ +not convey it at all. For example, if you agree to terms that obligate you\ +to collect a royalty for further conveying from those to whom you convey\ +the Program, the only way you could satisfy both those terms and this\ +License would be to refrain entirely from conveying the Program.\ +\ + 13. Use with the GNU Affero General Public License.\ +\ + Notwithstanding any other provision of this License, you have\ +permission to link or combine any covered work with a work licensed\ +under version 3 of the GNU Affero General Public License into a single\ +combined work, and to convey the resulting work. The terms of this\ +License will continue to apply to the part which is the covered work,\ +but the special requirements of the GNU Affero General Public License,\ +section 13, concerning interaction through a network will apply to the\ +combination as such.\ +\ + 14. Revised Versions of this License.\ +\ + The Free Software Foundation may publish revised and/or new versions of\ +the GNU General Public License from time to time. Such new versions will\ +be similar in spirit to the present version, but may differ in detail to\ +address new problems or concerns.\ +\ + Each version is given a distinguishing version number. If the\ +Program specifies that a certain numbered version of the GNU General\ +Public License "or any later version" applies to it, you have the\ +option of following the terms and conditions either of that numbered\ +version or of any later version published by the Free Software\ +Foundation. If the Program does not specify a version number of the\ +GNU General Public License, you may choose any version ever published\ +by the Free Software Foundation.\ +\ + If the Program specifies that a proxy can decide which future\ +versions of the GNU General Public License can be used, that proxy's\ +public statement of acceptance of a version permanently authorizes you\ +to choose that version for the Program.\ +\ + Later license versions may give you additional or different\ +permissions. However, no additional obligations are imposed on any\ +author or copyright holder as a result of your choosing to follow a\ +later version.\ +\ + 15. Disclaimer of Warranty.\ +\ + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\ +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\ +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY\ +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\ +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\ +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\ +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\ +ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\ +\ + 16. Limitation of Liability.\ +\ + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\ +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\ +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\ +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\ +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\ +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\ +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\ +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\ +SUCH DAMAGES.\ +\ + 17. Interpretation of Sections 15 and 16.\ +\ + If the disclaimer of warranty and limitation of liability provided\ +above cannot be given local legal effect according to their terms,\ +reviewing courts shall apply local law that most closely approximates\ +an absolute waiver of all civil liability in connection with the\ +Program, unless a warranty or assumption of liability accompanies a\ +copy of the Program in return for a fee.\ +\ + END OF TERMS AND CONDITIONS\ +\ + How to Apply These Terms to Your New Programs\ +\ + If you develop a new program, and you want it to be of the greatest\ +possible use to the public, the best way to achieve this is to make it\ +free software which everyone can redistribute and change under these terms.\ +\ + To do so, attach the following notices to the program. It is safest\ +to attach them to the start of each source file to most effectively\ +state the exclusion of warranty; and each file should have at least\ +the "copyright" line and a pointer to where the full notice is found.\ +\ + \ + Copyright (C) \ +\ + This program is free software: you can redistribute it and/or modify\ + it under the terms of the GNU General Public License as published by\ + the Free Software Foundation, either version 3 of the License, or\ + (at your option) any later version.\ +\ + This program is distributed in the hope that it will be useful,\ + but WITHOUT ANY WARRANTY; without even the implied warranty of\ + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\ + GNU General Public License for more details.\ +\ + You should have received a copy of the GNU General Public License\ + along with this program. If not, see .\ +\ +Also add information on how to contact you by electronic and paper mail.\ +\ + If the program does terminal interaction, make it output a short\ +notice like this when it starts in an interactive mode:\ +\ + Copyright (C) \ + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\ + This is free software, and you are welcome to redistribute it\ + under certain conditions; type `show c' for details.\ +\ +The hypothetical commands `show w' and `show c' should show the appropriate\ +parts of the General Public License. Of course, your program's commands\ +might be different; for a GUI interface, you would use an "about box".\ +\ + You should also get your employer (if you work as a programmer) or school,\ +if any, to sign a "copyright disclaimer" for the program, if necessary.\ +For more information on this, and how to apply and follow the GNU GPL, see\ +.\ +\ + The GNU General Public License does not permit incorporating your program\ +into proprietary programs. If your program is a subroutine library, you\ +may consider it more useful to permit linking proprietary applications with\ +the library. If this is what you want to do, use the GNU Lesser General\ +Public License instead of this License. But first, please read\ +.\ +\ + +\f0\b\fs26 LICENSE-MPL-2.0 +\f1\b0\fs24 \ +Mozilla Public License Version 2.0\ +==================================\ +\ +1. Definitions\ +--------------\ +\ +1.1. "Contributor"\ + means each individual or legal entity that creates, contributes to\ + the creation of, or owns Covered Software.\ +\ +1.2. "Contributor Version"\ + means the combination of the Contributions of others (if any) used\ + by a Contributor and that particular Contributor's Contribution.\ +\ +1.3. "Contribution"\ + means Covered Software of a particular Contributor.\ +\ +1.4. "Covered Software"\ + means Source Code Form to which the initial Contributor has attached\ + the notice in Exhibit A, the Executable Form of such Source Code\ + Form, and Modifications of such Source Code Form, in each case\ + including portions thereof.\ +\ +1.5. "Incompatible With Secondary Licenses"\ + means\ +\ + (a) that the initial Contributor has attached the notice described\ + in Exhibit B to the Covered Software; or\ +\ + (b) that the Covered Software was made available under the terms of\ + version 1.1 or earlier of the License, but not also under the\ + terms of a Secondary License.\ +\ +1.6. "Executable Form"\ + means any form of the work other than Source Code Form.\ +\ +1.7. "Larger Work"\ + means a work that combines Covered Software with other material, in\ + a separate file or files, that is not Covered Software.\ +\ +1.8. "License"\ + means this document.\ +\ +1.9. "Licensable"\ + means having the right to grant, to the maximum extent possible,\ + whether at the time of the initial grant or subsequently, any and\ + all of the rights conveyed by this License.\ +\ +1.10. "Modifications"\ + means any of the following:\ +\ + (a) any file in Source Code Form that results from an addition to,\ + deletion from, or modification of the contents of Covered\ + Software; or\ +\ + (b) any new file in Source Code Form that contains any Covered\ + Software.\ +\ +1.11. "Patent Claims" of a Contributor\ + means any patent claim(s), including without limitation, method,\ + process, and apparatus claims, in any patent Licensable by such\ + Contributor that would be infringed, but for the grant of the\ + License, by the making, using, selling, offering for sale, having\ + made, import, or transfer of either its Contributions or its\ + Contributor Version.\ +\ +1.12. "Secondary License"\ + means either the GNU General Public License, Version 2.0, the GNU\ + Lesser General Public License, Version 2.1, the GNU Affero General\ + Public License, Version 3.0, or any later versions of those\ + licenses.\ +\ +1.13. "Source Code Form"\ + means the form of the work preferred for making modifications.\ +\ +1.14. "You" (or "Your")\ + means an individual or a legal entity exercising rights under this\ + License. For legal entities, "You" includes any entity that\ + controls, is controlled by, or is under common control with You. For\ + purposes of this definition, "control" means (a) the power, direct\ + or indirect, to cause the direction or management of such entity,\ + whether by contract or otherwise, or (b) ownership of more than\ + fifty percent (50%) of the outstanding shares or beneficial\ + ownership of such entity.\ +\ +2. License Grants and Conditions\ +--------------------------------\ +\ +2.1. Grants\ +\ +Each Contributor hereby grants You a world-wide, royalty-free,\ +non-exclusive license:\ +\ +(a) under intellectual property rights (other than patent or trademark)\ + Licensable by such Contributor to use, reproduce, make available,\ + modify, display, perform, distribute, and otherwise exploit its\ + Contributions, either on an unmodified basis, with Modifications, or\ + as part of a Larger Work; and\ +\ +(b) under Patent Claims of such Contributor to make, use, sell, offer\ + for sale, have made, import, and otherwise transfer either its\ + Contributions or its Contributor Version.\ +\ +2.2. Effective Date\ +\ +The licenses granted in Section 2.1 with respect to any Contribution\ +become effective for each Contribution on the date the Contributor first\ +distributes such Contribution.\ +\ +2.3. Limitations on Grant Scope\ +\ +The licenses granted in this Section 2 are the only rights granted under\ +this License. No additional rights or licenses will be implied from the\ +distribution or licensing of Covered Software under this License.\ +Notwithstanding Section 2.1(b) above, no patent license is granted by a\ +Contributor:\ +\ +(a) for any code that a Contributor has removed from Covered Software;\ + or\ +\ +(b) for infringements caused by: (i) Your and any other third party's\ + modifications of Covered Software, or (ii) the combination of its\ + Contributions with other software (except as part of its Contributor\ + Version); or\ +\ +(c) under Patent Claims infringed by Covered Software in the absence of\ + its Contributions.\ +\ +This License does not grant any rights in the trademarks, service marks,\ +or logos of any Contributor (except as may be necessary to comply with\ +the notice requirements in Section 3.4).\ +\ +2.4. Subsequent Licenses\ +\ +No Contributor makes additional grants as a result of Your choice to\ +distribute the Covered Software under a subsequent version of this\ +License (see Section 10.2) or under the terms of a Secondary License (if\ +permitted under the terms of Section 3.3).\ +\ +2.5. Representation\ +\ +Each Contributor represents that the Contributor believes its\ +Contributions are its original creation(s) or it has sufficient rights\ +to grant the rights to its Contributions conveyed by this License.\ +\ +2.6. Fair Use\ +\ +This License is not intended to limit any rights You have under\ +applicable copyright doctrines of fair use, fair dealing, or other\ +equivalents.\ +\ +2.7. Conditions\ +\ +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\ +in Section 2.1.\ +\ +3. Responsibilities\ +-------------------\ +\ +3.1. Distribution of Source Form\ +\ +All distribution of Covered Software in Source Code Form, including any\ +Modifications that You create or to which You contribute, must be under\ +the terms of this License. You must inform recipients that the Source\ +Code Form of the Covered Software is governed by the terms of this\ +License, and how they can obtain a copy of this License. You may not\ +attempt to alter or restrict the recipients' rights in the Source Code\ +Form.\ +\ +3.2. Distribution of Executable Form\ +\ +If You distribute Covered Software in Executable Form then:\ +\ +(a) such Covered Software must also be made available in Source Code\ + Form, as described in Section 3.1, and You must inform recipients of\ + the Executable Form how they can obtain a copy of such Source Code\ + Form by reasonable means in a timely manner, at a charge no more\ + than the cost of distribution to the recipient; and\ +\ +(b) You may distribute such Executable Form under the terms of this\ + License, or sublicense it under different terms, provided that the\ + license for the Executable Form does not attempt to limit or alter\ + the recipients' rights in the Source Code Form under this License.\ +\ +3.3. Distribution of a Larger Work\ +\ +You may create and distribute a Larger Work under terms of Your choice,\ +provided that You also comply with the requirements of this License for\ +the Covered Software. If the Larger Work is a combination of Covered\ +Software with a work governed by one or more Secondary Licenses, and the\ +Covered Software is not Incompatible With Secondary Licenses, this\ +License permits You to additionally distribute such Covered Software\ +under the terms of such Secondary License(s), so that the recipient of\ +the Larger Work may, at their option, further distribute the Covered\ +Software under the terms of either this License or such Secondary\ +License(s).\ +\ +3.4. Notices\ +\ +You may not remove or alter the substance of any license notices\ +(including copyright notices, patent notices, disclaimers of warranty,\ +or limitations of liability) contained within the Source Code Form of\ +the Covered Software, except that You may alter any license notices to\ +the extent required to remedy known factual inaccuracies.\ +\ +3.5. Application of Additional Terms\ +\ +You may choose to offer, and to charge a fee for, warranty, support,\ +indemnity or liability obligations to one or more recipients of Covered\ +Software. However, You may do so only on Your own behalf, and not on\ +behalf of any Contributor. You must make it absolutely clear that any\ +such warranty, support, indemnity, or liability obligation is offered by\ +You alone, and You hereby agree to indemnify every Contributor for any\ +liability incurred by such Contributor as a result of warranty, support,\ +indemnity or liability terms You offer. You may include additional\ +disclaimers of warranty and limitations of liability specific to any\ +jurisdiction.\ +\ +4. Inability to Comply Due to Statute or Regulation\ +---------------------------------------------------\ +\ +If it is impossible for You to comply with any of the terms of this\ +License with respect to some or all of the Covered Software due to\ +statute, judicial order, or regulation then You must: (a) comply with\ +the terms of this License to the maximum extent possible; and (b)\ +describe the limitations and the code they affect. Such description must\ +be placed in a text file included with all distributions of the Covered\ +Software under this License. Except to the extent prohibited by statute\ +or regulation, such description must be sufficiently detailed for a\ +recipient of ordinary skill to be able to understand it.\ +\ +5. Termination\ +--------------\ +\ +5.1. The rights granted under this License will terminate automatically\ +if You fail to comply with any of its terms. However, if You become\ +compliant, then the rights granted under this License from a particular\ +Contributor are reinstated (a) provisionally, unless and until such\ +Contributor explicitly and finally terminates Your grants, and (b) on an\ +ongoing basis, if such Contributor fails to notify You of the\ +non-compliance by some reasonable means prior to 60 days after You have\ +come back into compliance. Moreover, Your grants from a particular\ +Contributor are reinstated on an ongoing basis if such Contributor\ +notifies You of the non-compliance by some reasonable means, this is the\ +first time You have received notice of non-compliance with this License\ +from such Contributor, and You become compliant prior to 30 days after\ +Your receipt of the notice.\ +\ +5.2. If You initiate litigation against any entity by asserting a patent\ +infringement claim (excluding declaratory judgment actions,\ +counter-claims, and cross-claims) alleging that a Contributor Version\ +directly or indirectly infringes any patent, then the rights granted to\ +You by any and all Contributors for the Covered Software under Section\ +2.1 of this License shall terminate.\ +\ +5.3. In the event of termination under Sections 5.1 or 5.2 above, all\ +end user license agreements (excluding distributors and resellers) which\ +have been validly granted by You or Your distributors under this License\ +prior to termination shall survive termination.\ +\ +************************************************************************\ +* *\ +* 6. Disclaimer of Warranty *\ +* ------------------------- *\ +* *\ +* Covered Software is provided under this License on an "as is" *\ +* basis, without warranty of any kind, either expressed, implied, or *\ +* statutory, including, without limitation, warranties that the *\ +* Covered Software is free of defects, merchantable, fit for a *\ +* particular purpose or non-infringing. The entire risk as to the *\ +* quality and performance of the Covered Software is with You. *\ +* Should any Covered Software prove defective in any respect, You *\ +* (not any Contributor) assume the cost of any necessary servicing, *\ +* repair, or correction. This disclaimer of warranty constitutes an *\ +* essential part of this License. No use of any Covered Software is *\ +* authorized under this License except under this disclaimer. *\ +* *\ +************************************************************************\ +\ +************************************************************************\ +* *\ +* 7. Limitation of Liability *\ +* -------------------------- *\ +* *\ +* Under no circumstances and under no legal theory, whether tort *\ +* (including negligence), contract, or otherwise, shall any *\ +* Contributor, or anyone who distributes Covered Software as *\ +* permitted above, be liable to You for any direct, indirect, *\ +* special, incidental, or consequential damages of any character *\ +* including, without limitation, damages for lost profits, loss of *\ +* goodwill, work stoppage, computer failure or malfunction, or any *\ +* and all other commercial damages or losses, even if such party *\ +* shall have been informed of the possibility of such damages. This *\ +* limitation of liability shall not apply to liability for death or *\ +* personal injury resulting from such party's negligence to the *\ +* extent applicable law prohibits such limitation. Some *\ +* jurisdictions do not allow the exclusion or limitation of *\ +* incidental or consequential damages, so this exclusion and *\ +* limitation may not apply to You. *\ +* *\ +************************************************************************\ +\ +8. Litigation\ +-------------\ +\ +Any litigation relating to this License may be brought only in the\ +courts of a jurisdiction where the defendant maintains its principal\ +place of business and such litigation shall be governed by laws of that\ +jurisdiction, without reference to its conflict-of-law provisions.\ +Nothing in this Section shall prevent a party's ability to bring\ +cross-claims or counter-claims.\ +\ +9. Miscellaneous\ +----------------\ +\ +This License represents the complete agreement concerning the subject\ +matter hereof. If any provision of this License is held to be\ +unenforceable, such provision shall be reformed only to the extent\ +necessary to make it enforceable. Any law or regulation which provides\ +that the language of a contract shall be construed against the drafter\ +shall not be used to construe this License against a Contributor.\ +\ +10. Versions of the License\ +---------------------------\ +\ +10.1. New Versions\ +\ +Mozilla Foundation is the license steward. Except as provided in Section\ +10.3, no one other than the license steward has the right to modify or\ +publish new versions of this License. Each version will be given a\ +distinguishing version number.\ +\ +10.2. Effect of New Versions\ +\ +You may distribute the Covered Software under the terms of the version\ +of the License under which You originally received the Covered Software,\ +or under the terms of any subsequent version published by the license\ +steward.\ +\ +10.3. Modified Versions\ +\ +If you create software not governed by this License, and you want to\ +create a new license for such software, you may create and use a\ +modified version of this License if you rename the license and remove\ +any references to the name of the license steward (except to note that\ +such modified license differs from this License).\ +\ +10.4. Distributing Source Code Form that is Incompatible With Secondary\ +Licenses\ +\ +If You choose to distribute Source Code Form that is Incompatible With\ +Secondary Licenses under the terms of this version of the License, the\ +notice described in Exhibit B of this License must be attached.\ +\ +Exhibit A - Source Code Form License Notice\ +-------------------------------------------\ +\ + This Source Code Form is subject to the terms of the Mozilla Public\ + License, v. 2.0. If a copy of the MPL was not distributed with this\ + file, You can obtain one at http://mozilla.org/MPL/2.0/.\ +\ +If it is not possible or desirable to put the notice in a particular\ +file, then You may include the notice in a location (such as a LICENSE\ +file in a relevant directory) where a recipient would be likely to look\ +for such a notice.\ +\ +You may add additional accurate notices of copyright ownership.\ +\ +Exhibit B - "Incompatible With Secondary Licenses" Notice\ +---------------------------------------------------------\ +\ + This Source Code Form is "Incompatible With Secondary Licenses", as\ + defined by the Mozilla Public License, v. 2.0.\ +\ + +\f0\b\fs26 LICENSE-MIT +\f1\b0\fs24 \ +MIT License\ +\ +Copyright (c) 2021+ Anton Zhiyanov \ +\ +Permission is hereby granted, free of charge, to any person obtaining a copy\ +of this software and associated documentation files (the "Software"), to deal\ +in the Software without restriction, including without limitation the rights\ +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ +copies of the Software, and to permit persons to whom the Software is\ +furnished to do so, subject to the following conditions:\ +\ +The above copyright notice and this permission notice shall be included in all\ +copies or substantial portions of the Software.\ +\ +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ +SOFTWARE.\ +\ + +\f0\b\fs26 LICENSE-PLUGINS +\f1\b0\fs24 \ +DB Browser for SQLite includes support for TIFF and WebP images. The\ +support for these comes from the LibTIFF and WebP projects, which have\ +their own (Open Source) licenses, different to ours.\ +\ +LibTIFF - http://www.simplesystems.org/libtiff/\ +\ + Copyright (c) 1988-1997 Sam Leffler\ + Copyright (c) 1991-1997 Silicon Graphics, Inc.\ +\ + Permission to use, copy, modify, distribute, and sell this software and \ + its documentation for any purpose is hereby granted without fee, provided\ + that (i) the above copyright notices and this permission notice appear in\ + all copies of the software and related documentation, and (ii) the names of\ + Sam Leffler and Silicon Graphics may not be used in any advertising or\ + publicity relating to the software without the specific, prior written\ + permission of Sam Leffler and Silicon Graphics.\ +\ + THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, \ + EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY \ + WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. \ +\ + IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR\ + ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND,\ + OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,\ + WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF \ + LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE \ + OF THIS SOFTWARE. \ +\ +WebP - https://developers.google.com/speed/webp/\ +\ + Copyright (c) 2010, Google Inc. All rights reserved.\ +\ + Redistribution and use in source and binary forms, with or without\ + modification, are permitted provided that the following conditions are\ + met:\ +\ + * Redistributions of source code must retain the above copyright\ + notice, this list of conditions and the following disclaimer.\ +\ + * Redistributions in binary form must reproduce the above copyright\ + notice, this list of conditions and the following disclaimer in\ + the documentation and/or other materials provided with the\ + distribution.\ +\ + * Neither the name of Google nor the names of its contributors may\ + be used to endorse or promote products derived from this software\ + without specific prior written permission.\ +\ + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\ + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\ + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\ + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\ + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\ + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\ + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\ + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\ + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\ + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\ + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\ +\ +Icons - https://codefisher.org/pastel-svg/\ +\ +Most of the icons come from the Pastel SVG icon set created by Michael\ +Buckley. We have obtained a special license (Creative Commons\ +Attribution Share Alike 4.0\ +http://creativecommons.org/licenses/by-sa/4.0/) but you might be\ +required to redistribute it under Creative Commons Attribution\ +NonCommercial Share Alike 4.0\ +http://creativecommons.org/licenses/by-nc-sa/4.0/\ +Check https://codefisher.org/pastel-svg/ for clarification.\ +\ +The construction emoji for the icon used in the nightly version come\ +from the OpenMoji under CC BY-SA 4.0 license.\ +Check https://openmoji.org/library/emoji-1F6A7/ and\ +https://openmoji.org/faq/ for clarification.\ +\ +Some icons might have other open licenses, check history of the files\ +under `src/icons`.} \ No newline at end of file diff --git a/installer/windows_on_arm/product.wxs b/installer/windows_on_arm/product.wxs new file mode 100644 index 0000000000..dac12a4efc --- /dev/null +++ b/installer/windows_on_arm/product.wxs @@ -0,0 +1,309 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SHORTCUT_SQLITE_DESKTOP + + + + + SHORTCUT_SQLCIPHER_DESKTOP + + + + + + + SHORTCUT_SQLITE_PROGRAMMENU + + + + + SHORTCUT_SQLCIPHER_PROGRAMMENU + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LicenseAccepted = "1" + NOT Installed + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSIS_INSTALLDIR + + + + + + + NSIS_INSTALLDIR + + + + + diff --git a/installer/windows_on_arm/resources/background.xcf b/installer/windows_on_arm/resources/background.xcf new file mode 100644 index 0000000000..ebf51cf0f1 Binary files /dev/null and b/installer/windows_on_arm/resources/background.xcf differ diff --git a/installer/windows_on_arm/resources/banner.xcf b/installer/windows_on_arm/resources/banner.xcf new file mode 100644 index 0000000000..10a3f83b79 Binary files /dev/null and b/installer/windows_on_arm/resources/banner.xcf differ diff --git a/installer/windows_on_arm/resources/icon.png b/installer/windows_on_arm/resources/icon.png new file mode 100644 index 0000000000..f6505e913f Binary files /dev/null and b/installer/windows_on_arm/resources/icon.png differ diff --git a/installer/windows_on_arm/strings.wxl b/installer/windows_on_arm/strings.wxl new file mode 100644 index 0000000000..f5af817deb --- /dev/null +++ b/installer/windows_on_arm/strings.wxl @@ -0,0 +1,14 @@ + + + This Setup Wizard will install [ProductName] on your computer. If you have a previous version already installed, this installation process will update it. + + + [ProductName] Setup + {\WixUI_Font_Title}Shortcuts + Select the shortcuts for the application. + [ProductName] uses the latest version of SQLite, so you can enjoy all of its new features and bug fixes, but it does not have encryption support. It is also built with SQLCipher as a separate application. SQLCipher is an open source extension to SQLite providing transparent 256-bit AES encryption of database files, but uses a slightly older version of SQLite. Both applications (with and without SQLCipher) are installed and can run concurrently. This page allows you to choose the shortcuts for each application and where to place them. + DB Browser (SQLite) + DB Browser (SQLCipher) + Desktop + Program Menu + diff --git a/installer/windows_on_arm/translations.wxs b/installer/windows_on_arm/translations.wxs new file mode 100644 index 0000000000..0bba898606 --- /dev/null +++ b/installer/windows_on_arm/translations.wxs @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/installer/windows_on_arm/ui.wxs b/installer/windows_on_arm/ui.wxs new file mode 100644 index 0000000000..7fee7011f2 --- /dev/null +++ b/installer/windows_on_arm/ui.wxs @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + 1 + + + 1 + + + 1 + + + + + diff --git a/installer/windows_on_arm/variables.wxi b/installer/windows_on_arm/variables.wxi new file mode 100644 index 0000000000..7c65c684e8 --- /dev/null +++ b/installer/windows_on_arm/variables.wxi @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Application.cpp b/src/Application.cpp index 0769a0d13f..9114153581 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -330,6 +330,7 @@ Application::~Application() { if(m_mainWindow) delete m_mainWindow; + Settings::sync(); } bool Application::event(QEvent* event) diff --git a/src/ColumnDisplayFormatDialog.cpp b/src/ColumnDisplayFormatDialog.cpp index 69ca8c8b13..a7c7e3a3d7 100644 --- a/src/ColumnDisplayFormatDialog.cpp +++ b/src/ColumnDisplayFormatDialog.cpp @@ -42,6 +42,7 @@ ColumnDisplayFormatDialog::ColumnDisplayFormatDialog(DBBrowserDB& db, const sqlb ui->comboDisplayFormat->addItem(tr("Binary GUID to text"), "guid"); ui->comboDisplayFormat->addItem(tr("SpatiaLite Geometry to SVG"), "geomToSVG"); ui->comboDisplayFormat->insertSeparator(ui->comboDisplayFormat->count()); + ui->comboDisplayFormat->addItem(tr("JSON"), "json"); ui->comboDisplayFormat->addItem(tr("Custom"), "custom"); ui->labelDisplayFormat->setText(ui->labelDisplayFormat->text().arg(column_name)); @@ -87,6 +88,7 @@ R"('' , 1, 5) || '" stroke="darkblue" fill="#b5cfed" stroke-width="1"/>' || '')").arg(e_column_name); + formatFunctions["json"] = "json(" + e_column_name + ")"; // Set the current format, if it's empty set the default format if(current_format.isEmpty()) diff --git a/src/DbStructureModel.cpp b/src/DbStructureModel.cpp index bcded108ea..485fffb086 100644 --- a/src/DbStructureModel.cpp +++ b/src/DbStructureModel.cpp @@ -8,14 +8,18 @@ #include #include #include +#include -DbStructureModel::DbStructureModel(DBBrowserDB& db, QObject* parent) +DbStructureModel::DbStructureModel(DBBrowserDB& db, QObject* parent, + bool dropSelectQuery, + bool dropInsert) : QAbstractItemModel(parent), m_db(db), browsablesRootItem(nullptr), m_dropQualifiedNames(false), m_dropEnquotedNames(false), - m_dropSelectQuery(true) + m_dropSelectQuery(dropSelectQuery), + m_dropInsert(dropInsert) { // Create root item and use its columns to store the header strings QStringList header; @@ -210,7 +214,7 @@ QStringList DbStructureModel::mimeTypes() const QMimeData* DbStructureModel::mimeData(const QModelIndexList& indices) const { // We store the SQL data and the names data separately - QByteArray sqlData, namesData; + QByteArray sqlData, namesData, parametersData; // For dropping SELECT queries, these variables take account of // whether all objects are of the same type and belong to the same table. @@ -235,6 +239,8 @@ QMimeData* DbStructureModel::mimeData(const QModelIndexList& indices) const // For names, export a (qualified) (escaped) identifier of the item for statement composition in SQL editor. if(objectType == "field") { namesData.append(getNameForDropping(item->text(ColumnSchema), item->parent()->text(ColumnName), item->text(ColumnName)).toUtf8()); + parametersData.append(QString("?" + item->text(ColumnName) + ", ").toUtf8()); + QString table = getNameForDropping(item->text(ColumnSchema), item->parent()->text(ColumnName), ""); if (tableSet.isEmpty() || tableSet == table) { tableSet = table; @@ -299,14 +305,27 @@ QMimeData* DbStructureModel::mimeData(const QModelIndexList& indices) const else if (namesData.endsWith(".")) namesData.chop(1); + if (parametersData.endsWith(", ")) + parametersData.chop(2); + else if (parametersData.endsWith(".")) + parametersData.chop(1); + if (tableSet.endsWith(".")) tableSet.chop(1); - if (m_dropSelectQuery && !tableSet.isEmpty() && tableSet != "*" && !objectTypeSet.isEmpty()) { - if (objectTypeSet == "field") { - namesData = ("SELECT " + QString::fromUtf8(namesData) + " FROM " + tableSet + ";").toUtf8(); - } else if (objectTypeSet == "table") { - namesData = ("SELECT * FROM " + tableSet + ";").toUtf8(); + if (!tableSet.isEmpty() && tableSet != "*" && !objectTypeSet.isEmpty()) { + if (m_dropSelectQuery) { + if (objectTypeSet == "field") { + namesData = ("SELECT " + QString::fromUtf8(namesData) + " FROM " + tableSet + ";\n").toUtf8(); + } else if (objectTypeSet == "table") { + namesData = ("SELECT * FROM " + tableSet + ";").toUtf8(); + } + } else if (m_dropInsert) { + if (objectTypeSet == "field") { + namesData = ("INSERT INTO " + tableSet + " (" + QString::fromUtf8(namesData) + ") VALUES (" + parametersData + ");\n").toUtf8(); + } else if (objectTypeSet == "table") { + namesData = ("INSERT INTO " + tableSet + " DEFAULT VALUES;\n").toUtf8(); + } } } mime->setData("text/plain", namesData); @@ -315,6 +334,12 @@ QMimeData* DbStructureModel::mimeData(const QModelIndexList& indices) const return mime; } +void DbStructureModel::copy(const QModelIndexList& indices) const +{ + QMimeData *mimeData = DbStructureModel::mimeData(indices); + qApp->clipboard()->setMimeData(mimeData); +} + bool DbStructureModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&) { if(action == Qt::IgnoreAction) diff --git a/src/DbStructureModel.h b/src/DbStructureModel.h index f12dc3f3b5..7c2c3ae12f 100644 --- a/src/DbStructureModel.h +++ b/src/DbStructureModel.h @@ -12,7 +12,9 @@ class DbStructureModel : public QAbstractItemModel Q_OBJECT public: - explicit DbStructureModel(DBBrowserDB& db, QObject* parent = nullptr); + explicit DbStructureModel(DBBrowserDB& db, QObject* parent = nullptr, + bool dropSelectQuery = true, + bool dropInsert = false); ~DbStructureModel() override; QVariant data(const QModelIndex& index, int role) const override; @@ -27,6 +29,9 @@ class DbStructureModel : public QAbstractItemModel QMimeData* mimeData(const QModelIndexList& indices) const override; bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; + // Copy selected items to clipboard + void copy(const QModelIndexList& indices) const; + enum Columns { ColumnName, @@ -41,6 +46,7 @@ public slots: void setDropQualifiedNames(bool value) { m_dropQualifiedNames = value; } void setDropEnquotedNames(bool value) { m_dropEnquotedNames = value; } void setDropSelectQuery(bool value) { m_dropSelectQuery = value; } + void setDropInsert(bool value) { m_dropInsert = value; } private: DBBrowserDB& m_db; @@ -49,6 +55,7 @@ public slots: bool m_dropQualifiedNames; bool m_dropEnquotedNames; bool m_dropSelectQuery; + bool m_dropInsert; void buildTree(QTreeWidgetItem* parent, const std::string& schema); QTreeWidgetItem* addNode(const std::string& schema, const std::string& name, const std::string& object_type, const std::string& sql, QTreeWidgetItem* parent_item, const std::string& data_type = {}, const std::string& icon_suffix = {}); diff --git a/src/EditDialog.cpp b/src/EditDialog.cpp index 5457cb3b15..c9dfbfbaa7 100644 --- a/src/EditDialog.cpp +++ b/src/EditDialog.cpp @@ -24,6 +24,7 @@ #include using json = nlohmann::json; +using ordered_json = nlohmann::ordered_json; EditDialog::EditDialog(QWidget* parent) : QDialog(parent), @@ -95,19 +96,13 @@ EditDialog::EditDialog(QWidget* parent) }); connect(ui->actionOpenInExternal, &QAction::triggered, this, &EditDialog::openDataWithExternal); - mustIndentAndCompact = Settings::getValue("databrowser", "indent_compact").toBool(); - ui->actionIndent->setChecked(mustIndentAndCompact); - - ui->buttonAutoSwitchMode->setChecked(Settings::getValue("databrowser", "auto_switch_mode").toBool()); - ui->actionWordWrap->setChecked(Settings::getValue("databrowser", "editor_word_wrap").toBool()); - setWordWrapping(ui->actionWordWrap->isChecked()); - reloadSettings(); } EditDialog::~EditDialog() { Settings::setValue("databrowser", "indent_compact", mustIndentAndCompact); + Settings::setValue("databrowser", "sort_keys", mustSortKeys); Settings::setValue("databrowser", "auto_switch_mode", ui->buttonAutoSwitchMode->isChecked()); Settings::setValue("databrowser", "editor_word_wrap", ui->actionWordWrap->isChecked()); delete ui; @@ -614,10 +609,13 @@ void EditDialog::accept() QString newData; bool proceed = true; - json jsonDoc; + ordered_json jsonDoc; try { - jsonDoc = json::parse(sciEdit->text().toStdString()); + if (mustSortKeys) + jsonDoc = json::parse(sciEdit->text().toStdString()); + else + jsonDoc = ordered_json::parse(sciEdit->text().toStdString()); } catch(json::parse_error& parseError) { sciEdit->setErrorIndicator(static_cast(parseError.byte - 1)); @@ -747,10 +745,14 @@ void EditDialog::setDataInBuffer(const QByteArray& bArrdata, DataSources source) { sciEdit->clearErrorIndicators(); - json jsonDoc; + ordered_json jsonDoc; try { + if (mustSortKeys) jsonDoc = json::parse(std::string(bArrdata.constData(), static_cast(bArrdata.size()))); + else + jsonDoc = ordered_json::parse(std::string(bArrdata.constData(), static_cast(bArrdata.size()))); + } catch(json::parse_error& parseError) { sciEdit->setErrorIndicator(static_cast(parseError.byte - 1)); } @@ -808,8 +810,9 @@ void EditDialog::setDataInBuffer(const QByteArray& bArrdata, DataSources source) void EditDialog::editModeChanged(int newMode) { ui->actionIndent->setEnabled(newMode == JsonEditor || newMode == XmlEditor); + ui->actionSortKeys->setEnabled(newMode == JsonEditor); setStackCurrentIndex(newMode); - + // Change focus from the mode combo to the editor to start typing. if (ui->comboMode->hasFocus()) setFocus(); @@ -931,6 +934,8 @@ void EditDialog::editTextChanged() void EditDialog::setMustIndentAndCompact(bool enable) { mustIndentAndCompact = enable; + + ui->actionSortKeys->setEnabled(enable); // Indent or compact if necessary. If data has changed (button Apply indicates so), reload from the widget, else from the table. if (ui->buttonApply->isEnabled()) { @@ -939,6 +944,19 @@ void EditDialog::setMustIndentAndCompact(bool enable) setCurrentIndex(m_currentIndex); } +void EditDialog::setMustSortKeys(bool enable) +{ + mustSortKeys = enable; + + // Must order the json keys. If data has changed (button Apply indicates so), reload from the widget, else from the table. + if (ui->buttonApply->isEnabled()) { + setDataInBuffer(sciEdit->text().toUtf8(), SciBuffer); + } + else + setCurrentIndex(m_currentIndex); +} + + // Determine the type of data in the cell int EditDialog::checkDataType(const QByteArray& bArrdata) const { @@ -1136,6 +1154,14 @@ void EditDialog::updateCellInfoAndMode(const QByteArray& bArrdata) void EditDialog::reloadSettings() { + mustIndentAndCompact = Settings::getValue("databrowser", "indent_compact").toBool(); + ui->actionIndent->setChecked(mustIndentAndCompact); + + mustSortKeys = Settings::getValue("databrowser", "sort_keys").toBool(); + ui->actionSortKeys->setChecked(mustSortKeys); + + ui->buttonAutoSwitchMode->setChecked(Settings::getValue("databrowser", "auto_switch_mode").toBool()); + // Set the (SQL) editor font for hex editor, since it needs a // Monospace font and the databrowser font would be usually of // variable width. @@ -1152,6 +1178,9 @@ void EditDialog::reloadSettings() (Settings::getValue("General", "toolbarStyleEditCell").toInt())); sciEdit->reloadSettings(); + + ui->actionWordWrap->setChecked(Settings::getValue("databrowser", "editor_word_wrap").toBool()); + setWordWrapping(ui->actionWordWrap->isChecked()); } void EditDialog::setStackCurrentIndex(int editMode) diff --git a/src/EditDialog.h b/src/EditDialog.h index a9242404fe..a7cea29f86 100644 --- a/src/EditDialog.h +++ b/src/EditDialog.h @@ -48,6 +48,7 @@ private slots: void switchEditorMode(bool autoSwitchForType); void updateCellInfoAndMode(const QByteArray& bArrdata); void setMustIndentAndCompact(bool enable); + void setMustSortKeys(bool enable); void openPrintDialog(); void copyHexAscii(); void setWordWrapping(bool value); @@ -67,6 +68,7 @@ private slots: int dataType; bool isReadOnly; bool mustIndentAndCompact; + bool mustSortKeys; QByteArray removedBom; enum DataSources { diff --git a/src/EditDialog.ui b/src/EditDialog.ui index 3131ab5851..af932643dd 100644 --- a/src/EditDialog.ui +++ b/src/EditDialog.ui @@ -151,6 +151,7 @@ + @@ -313,6 +314,24 @@ When enabled, the auto-format feature formats the data on loading, breaking the text in lines and indenting it for maximum readability. On data saving, the auto-format feature compacts the data removing end of lines, and unnecessary whitespace. + + + true + + + + :/icons/text_sort_ascending:/icons/text_sort_ascending + + + SortKeys + + + Sort Keys: when pretty print is on, will sort the json keys alphabetically. + + + When enabled alongside pretty print, the json keys will be ordered alphabetically by their name + + @@ -504,6 +523,22 @@ + + actionSortKeys + toggled(bool) + EditDialog + setMustSortKeys(bool) + + + -1 + -1 + + + 308 + 190 + + + buttonAutoSwitchMode toggled(bool) diff --git a/src/ExtendedTableWidget.cpp b/src/ExtendedTableWidget.cpp index 7bf611f9ea..72c0721342 100644 --- a/src/ExtendedTableWidget.cpp +++ b/src/ExtendedTableWidget.cpp @@ -75,7 +75,7 @@ std::vector parseClipboard(QString clipboard) }; while (offset >= 0) { - const QRegularExpressionMatch match = re.match(clipboard, offset); + const QRegularExpressionMatch match = re.match(clipboard, offset, QRegularExpression::PartialPreferFirstMatch); const int pos = match.capturedStart(0); if (pos < 0) { // insert everything that left @@ -257,6 +257,8 @@ ExtendedTableWidget::ExtendedTableWidget(QWidget* parent) : m_frozen_column_count(0), m_item_border_delegate(new ItemBorderDelegate(this)) { + setWordWrap(Settings::getValue("databrowser", "cell_word_wrap").toBool()); + setHorizontalScrollMode(ExtendedTableWidget::ScrollPerPixel); // Force ScrollPerItem, so scrolling shows all table rows setVerticalScrollMode(ExtendedTableWidget::ScrollPerItem); @@ -511,9 +513,11 @@ void ExtendedTableWidget::reloadSettings() verticalHeader()->setDefaultSectionSize(fontMetrics.height()+10); if(m_frozen_table_view) m_frozen_table_view->reloadSettings(); + + setWordWrap(Settings::getValue("databrowser", "cell_word_wrap").toBool()); } -void ExtendedTableWidget::copyMimeData(const QModelIndexList& fromIndices, QMimeData* mimeData, const bool withHeaders, const bool inSQL) +bool ExtendedTableWidget::copyMimeData(const QModelIndexList& fromIndices, QMimeData* mimeData, const bool withHeaders, const bool inSQL) { QModelIndexList indices = fromIndices; @@ -528,7 +532,7 @@ void ExtendedTableWidget::copyMimeData(const QModelIndexList& fromIndices, QMime // Abort if there's nothing to copy if (indices.isEmpty()) - return; + return false; SqliteTableModel* m = qobject_cast(model()); @@ -544,7 +548,7 @@ void ExtendedTableWidget::copyMimeData(const QModelIndexList& fromIndices, QMime { // If it's an image, copy the image data to the clipboard mimeData->setImageData(img); - return; + return false; } } @@ -637,7 +641,7 @@ void ExtendedTableWidget::copyMimeData(const QModelIndexList& fromIndices, QMime // Disable context help button on Windows progress.setWindowFlags(progress.windowFlags() & ~Qt::WindowContextHelpButtonHint); - progress.setRange(*rowsInIndexes.begin(), *rowsInIndexes.end()); + progress.setRange(*rowsInIndexes.begin(), *rowsInIndexes.rbegin()); progress.setMinimumDuration(2000); // Iterate over rows x cols checking if the index actually exists when needed, in order @@ -744,7 +748,7 @@ void ExtendedTableWidget::copyMimeData(const QModelIndexList& fromIndices, QMime progress.setValue(row); // Abort the operation if the user pressed ESC key or Cancel button if (progress.wasCanceled()) { - return; + return false; } } @@ -757,6 +761,7 @@ void ExtendedTableWidget::copyMimeData(const QModelIndexList& fromIndices, QMime result.resize(result.size() - rowSepText.size()); } mimeData->setText(result); + return true; } void ExtendedTableWidget::copy(const bool withHeaders, const bool inSQL ) @@ -1243,7 +1248,12 @@ void ExtendedTableWidget::openPrintDialog() // Copy the specified indices content to mimeData for getting the HTML representation of // the table with headers. We can then print it using an HTML text document. - copyMimeData(indices, mimeData, true, false); + bool mimeReady = copyMimeData(indices, mimeData, true, false); + if (!mimeReady) + { + delete mimeData; + return; + } QPrinter printer; QPrintPreviewDialog *dialog = new QPrintPreviewDialog(&printer); diff --git a/src/ExtendedTableWidget.h b/src/ExtendedTableWidget.h index a4d78d939b..9c4469e16e 100644 --- a/src/ExtendedTableWidget.h +++ b/src/ExtendedTableWidget.h @@ -128,7 +128,7 @@ public slots: void requestUrlOrFileOpen(const QString& urlString); private: - void copyMimeData(const QModelIndexList& fromIndices, QMimeData* mimeData, const bool withHeaders, const bool inSQL); + bool copyMimeData(const QModelIndexList& fromIndices, QMimeData* mimeData, const bool withHeaders, const bool inSQL); void copy(const bool withHeaders, const bool inSQL); void paste(); void cut(); diff --git a/src/FileDialog.cpp b/src/FileDialog.cpp index a3b6117471..ed22c202b9 100644 --- a/src/FileDialog.cpp +++ b/src/FileDialog.cpp @@ -15,7 +15,7 @@ QStringList FileDialog::getOpenFileNames(const FileDialogTypes dialogType, QWidg if(!result.isEmpty()) { QFileInfo path = QFileInfo(result.first()); - setFileDialogPath(dialogType, path.absolutePath()); + setFileDialogPath(dialogType, path.absoluteFilePath()); } return result; } @@ -48,9 +48,9 @@ QString FileDialog::getFileDialogPath(const FileDialogTypes dialogType) case 2: { // Remember last location for current session only QHash lastLocations = Settings::getValue("db", "lastlocations").toHash(); - return lastLocations[QString(QChar(dialogType))].toString(); + return lastLocations[QString::number(static_cast(dialogType))].toString(); } - case 1: // Always use this locations + case 1: // Always use this location return Settings::getValue("db", "defaultlocation").toString(); default: return QString(); @@ -62,7 +62,7 @@ void FileDialog::setFileDialogPath(const FileDialogTypes dialogType, const QStri QString dir = QFileInfo(new_path).absolutePath(); QHash lastLocations = Settings::getValue("db", "lastlocations").toHash(); - lastLocations[QString(QChar(dialogType))] = dir; + lastLocations[QString::number(static_cast(dialogType))] = dir; switch(Settings::getValue("db", "savedefaultlocation").toInt()) { diff --git a/src/ForeignKeyEditorDelegate.cpp b/src/ForeignKeyEditorDelegate.cpp index 3da7ad5e3c..07afdedc23 100644 --- a/src/ForeignKeyEditorDelegate.cpp +++ b/src/ForeignKeyEditorDelegate.cpp @@ -29,6 +29,7 @@ class ForeignKeyEditor : public QWidget layout->addWidget(clauseEdit); layout->addWidget(m_btnReset); layout->setSpacing(0); + layout->setContentsMargins(0, 0, 0, 0); setLayout(layout); connect(m_btnReset, &QPushButton::clicked, this, [&] diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 1d183d833d..b2c1aa90ce 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1,4 +1,6 @@ #include "MainWindow.h" +#include "sql/ObjectIdentifier.h" +#include "sql/sqlitetypes.h" #include "ui_MainWindow.h" #include "Application.h" @@ -20,11 +22,11 @@ #include "ExportSqlDialog.h" #include "SqlUiLexer.h" #include "FileDialog.h" -#include "RemoteDock.h" #include "FindReplaceDialog.h" #include "RunSql.h" #include "ExtendedTableWidget.h" #include "Data.h" +#include "RemoteNetwork.h" #include "TableBrowser.h" #include "TableBrowserDock.h" @@ -53,6 +55,7 @@ #include #include #include +#include #ifdef Q_OS_MACX //Needed only on macOS #include @@ -69,7 +72,6 @@ MainWindow::MainWindow(QWidget* parent) m_currentTabTableModel(nullptr), editDock(new EditDialog(this)), plotDock(new PlotDock(this)), - remoteDock(new RemoteDock(this)), currentTableBrowser(nullptr), findReplaceDialog(new FindReplaceDialog(this)), execute_sql_worker(nullptr), @@ -99,7 +101,6 @@ void MainWindow::init() // Load window settings tabifyDockWidget(ui->dockLog, ui->dockPlot); tabifyDockWidget(ui->dockLog, ui->dockSchema); - tabifyDockWidget(ui->dockLog, ui->dockRemote); #ifdef Q_OS_MACX // Add OpenGL Context for macOS @@ -125,7 +126,9 @@ void MainWindow::init() connect(&db, &DBBrowserDB::requestCollation, this, &MainWindow::requestCollation); // Set up DB structure tab - dbStructureModel = new DbStructureModel(db, this); + dbStructureModel = new DbStructureModel(db, this, + Settings::getValue("SchemaDock", "dropSelectQuery").toBool(), + Settings::getValue("SchemaDock", "dropInsert").toBool()); connect(&db, &DBBrowserDB::structureUpdated, this, [this]() { std::vector old_tables; for(const auto& d : allTableBrowserDocks()) @@ -153,7 +156,6 @@ void MainWindow::init() // Create docks ui->dockEdit->setWidget(editDock); ui->dockPlot->setWidget(plotDock); - ui->dockRemote->setWidget(remoteDock); // Set up edit dock editDock->setReadOnly(true); @@ -246,9 +248,32 @@ void MainWindow::init() popupSchemaDockMenu->addAction(ui->actionPopupSchemaDockBrowseTable); popupSchemaDockMenu->addAction(ui->actionPopupSchemaDockDetachDatabase); popupSchemaDockMenu->addSeparator(); - popupSchemaDockMenu->addAction(ui->actionDropSelectQueryCheck); - popupSchemaDockMenu->addAction(ui->actionDropQualifiedCheck); - popupSchemaDockMenu->addAction(ui->actionEnquoteNamesCheck); + + auto dropSchemaDockMenu = new QMenu(popupSchemaDockMenu); + popupSchemaDockMenu->addMenu(dropSchemaDockMenu); + dropSchemaDockMenu->setTitle(tr("Clipboard/Drop Options")); + dropSchemaDockMenu->setStatusTip(tr("Options for Drag && Drop and Copy to Clipboard operations.")); + dropSchemaDockMenu->addAction(ui->actionDropSelectQueryCheck); + dropSchemaDockMenu->addAction(ui->actionDropInsertCheck); + dropSchemaDockMenu->addAction(ui->actionDropNamesCheck); + + QActionGroup* dropGroup = new QActionGroup(dropSchemaDockMenu); + dropGroup->addAction(ui->actionDropSelectQueryCheck); + dropGroup->addAction(ui->actionDropInsertCheck); + dropGroup->addAction(ui->actionDropNamesCheck); + + dropSchemaDockMenu->addSeparator(); + dropSchemaDockMenu->addAction(ui->actionDropQualifiedCheck); + dropSchemaDockMenu->addAction(ui->actionEnquoteNamesCheck); + + popupSchemaDockMenu->addAction(ui->actionCopyInSchema); + connect(ui->actionCopyInSchema, &QAction::triggered, this, [=]() { + dbStructureModel->copy(ui->treeSchemaDock->selectionModel()->selectedIndexes()); + }); + auto copyShortcut = new QShortcut(QKeySequence::Copy, ui->treeSchemaDock, nullptr, nullptr, Qt::WidgetShortcut); + connect(copyShortcut, &QShortcut::activated, this, [=]() { + dbStructureModel->copy(ui->treeSchemaDock->selectionModel()->selectedIndexes()); + }); popupOpenDbMenu = new QMenu(this); popupOpenDbMenu->addAction(ui->fileOpenAction); @@ -287,10 +312,6 @@ void MainWindow::init() ui->viewMenu->actions().at(3)->setShortcut(QKeySequence(tr("Ctrl+E"))); ui->viewMenu->actions().at(3)->setIcon(QIcon(":/icons/log_dock")); - // Add menu item for plot dock - ui->viewMenu->insertAction(ui->viewDBToolbarAction, ui->dockRemote->toggleViewAction()); - ui->viewMenu->actions().at(4)->setIcon(QIcon(":/icons/log_dock")); - // Set checked state if toolbar is visible ui->viewDBToolbarAction->setChecked(!ui->toolbarDB->isHidden()); ui->viewExtraDBToolbarAction->setChecked(!ui->toolbarExtraDB->isHidden()); @@ -341,7 +362,6 @@ void MainWindow::init() ui->dockPlot->hide(); ui->dockSchema->hide(); ui->dockEdit->hide(); - ui->dockRemote->hide(); }); QAction* atBottomLayoutAction = layoutMenu->addAction(tr("Dock Windows at Bottom")); connect(atBottomLayoutAction, &QAction::triggered, this, [=]() { @@ -415,18 +435,19 @@ void MainWindow::init() connect(editDock, &EditDialog::recordTextUpdated, this, &MainWindow::updateRecordText); connect(editDock, &EditDialog::evaluateText, this, &MainWindow::evaluateText); connect(editDock, &EditDialog::requestUrlOrFileOpen, this, &MainWindow::openUrlOrFile); - connect(ui->dbTreeWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::changeTreeSelection); + connect(ui->dbTreeWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::changeObjectSelection); connect(ui->dockEdit, &QDockWidget::visibilityChanged, this, &MainWindow::toggleEditDock); - connect(remoteDock, SIGNAL(openFile(QString)), this, SLOT(fileOpen(QString))); connect(ui->actionDropSelectQueryCheck, &QAction::toggled, dbStructureModel, &DbStructureModel::setDropSelectQuery); + connect(ui->actionDropInsertCheck, &QAction::toggled, dbStructureModel, &DbStructureModel::setDropInsert); + connect(ui->actionDropNamesCheck, &QAction::toggled, dbStructureModel, + [this]() { + dbStructureModel->setDropSelectQuery(false); + dbStructureModel->setDropInsert(false); + }); connect(ui->actionDropQualifiedCheck, &QAction::toggled, dbStructureModel, &DbStructureModel::setDropQualifiedNames); connect(ui->actionEnquoteNamesCheck, &QAction::toggled, dbStructureModel, &DbStructureModel::setDropEnquotedNames); connect(&db, &DBBrowserDB::databaseInUseChanged, this, &MainWindow::updateDatabaseBusyStatus); - ui->actionDropSelectQueryCheck->setChecked(Settings::getValue("SchemaDock", "dropSelectQuery").toBool()); - ui->actionDropQualifiedCheck->setChecked(Settings::getValue("SchemaDock", "dropQualifiedNames").toBool()); - ui->actionEnquoteNamesCheck->setChecked(Settings::getValue("SchemaDock", "dropEnquotedNames").toBool()); - connect(ui->actionSqlStop, &QAction::triggered, this, [this]() { if(execute_sql_worker && execute_sql_worker->isRunning()) execute_sql_worker->stop(); @@ -489,7 +510,6 @@ void MainWindow::init() ui->dockLog->setWindowTitle(ui->dockLog->windowTitle().remove('&')); ui->dockPlot->setWindowTitle(ui->dockPlot->windowTitle().remove('&')); ui->dockSchema->setWindowTitle(ui->dockSchema->windowTitle().remove('&')); - ui->dockRemote->setWindowTitle(ui->dockRemote->windowTitle().remove('&')); } bool MainWindow::fileOpen(const QString& fileName, bool openFromProject, bool readOnly) @@ -557,16 +577,17 @@ bool MainWindow::fileOpen(const QString& fileName, bool openFromProject, bool re // loadProject will init the rest return true; } - if(ui->tabSqlAreas->count() == 0) + if(ui->tabSqlAreas->count() == 0) // Create a new tab if there isn't one. openSqlTab(true); - else if(ui->mainTab->currentWidget() == ui->pragmas) + if(ui->mainTab->currentWidget() == ui->pragmas) loadPragmas(); refreshTableBrowsers(); - - // Update remote dock - remoteDock->fileOpened(wFile); - + if (db.readOnly()) { + if (!fileSystemWatch.files().isEmpty()) + fileSystemWatch.removePaths(fileSystemWatch.files()); + fileSystemWatch.addPath(wFile); + } retval = true; } else { QMessageBox::warning(this, qApp->applicationName(), tr("Could not open database file.\nReason: %1").arg(db.lastError())); @@ -611,7 +632,6 @@ void MainWindow::fileNewInMemoryDatabase(bool open_create_dialog) statusEncodingLabel->setText(db.getPragma("encoding")); statusEncryptionLabel->setVisible(false); statusReadOnlyLabel->setVisible(false); - remoteDock->fileOpened(":memory:"); refreshTableBrowsers(); if(ui->tabSqlAreas->count() == 0) openSqlTab(true); @@ -684,6 +704,10 @@ void MainWindow::refreshTableBrowsers(bool all) QApplication::restoreOverrideCursor(); } +void MainWindow::refreshDb() { + refreshTableBrowsers(); +} + bool MainWindow::fileSaveAs() { QString fileName = FileDialog::getSaveFileName( @@ -727,6 +751,8 @@ bool MainWindow::fileClose() if(!db.close()) return false; + if (!fileSystemWatch.files().isEmpty()) + fileSystemWatch.removePaths(fileSystemWatch.files()); TableBrowser::resetSharedSettings(); setCurrentFile(QString()); loadPragmas(); @@ -754,9 +780,6 @@ bool MainWindow::fileClose() for(int i=0; i < ui->tabSqlAreas->count(); i++) qobject_cast(ui->tabSqlAreas->widget(i))->getEditor()->reloadKeywords(); - // Clear remote dock - remoteDock->fileOpened(QString()); - return true; } @@ -770,6 +793,7 @@ void MainWindow::closeEvent( QCloseEvent* event ) Settings::setValue("SQLLogDock", "Log", ui->comboLogSubmittedBy->currentText()); Settings::setValue("SchemaDock", "dropSelectQuery", ui->actionDropSelectQueryCheck->isChecked()); + Settings::setValue("SchemaDock", "dropInsert", ui->actionDropInsertCheck->isChecked()); Settings::setValue("SchemaDock", "dropQualifiedNames", ui->actionDropQualifiedCheck->isChecked()); Settings::setValue("SchemaDock", "dropEnquotedNames", ui->actionEnquoteNamesCheck->isChecked()); @@ -887,8 +911,9 @@ void MainWindow::compact() void MainWindow::deleteObject() { // Get name and type of object to delete - sqlb::ObjectIdentifier obj = dbSelected->object(); - QString type = dbSelected->objectType(); + sqlb::ObjectIdentifier obj; + QString type; + getSelectedObject(type, obj); // Due to different grammar in languages (e.g. gender or declension), each message must be given separately to translation. QString message; @@ -922,19 +947,45 @@ void MainWindow::deleteObject() QMessageBox::warning(this, QApplication::applicationName(), message + " " + error); } else { refreshTableBrowsers(); - changeTreeSelection(); + changeObjectSelection(); + } + } +} + +void MainWindow::getSelectedObject(QString &type, sqlb::ObjectIdentifier& obj) { + + type = ""; + obj = sqlb::ObjectIdentifier(); + + QWidget* currentTab = ui->mainTab->currentWidget(); + if (currentTab == ui->structure) { + + if(!dbSelected->hasSelection()) + return; + + // Get name and type of the object to edit + obj = dbSelected->object(); + type = dbSelected->objectType(); + + } else if (currentTab == ui->browser) { + // Get name of the current table from the Data Browser + obj = currentlyBrowsedTableName(); + + sqlb::TablePtr tablePtr = db.getTableByName(obj); + if (!tablePtr) { + return; + } else { + type = tablePtr->isView()? "view" : "table"; } } } void MainWindow::editObject() { - if(!dbSelected->hasSelection()) - return; - - // Get name and type of the object to edit - sqlb::ObjectIdentifier obj = dbSelected->object(); - QString type = dbSelected->objectType(); + QString type; + sqlb::ObjectIdentifier obj; + // Get name and type of object to edit + getSelectedObject(type, obj); if(type == "table") { @@ -1084,6 +1135,7 @@ void MainWindow::dataTableSelectionChanged(const QModelIndex& index) if (editDock->isVisible()) { editDock->setCurrentIndex(index); } + changeObjectSelection(); } /* @@ -1118,7 +1170,7 @@ void MainWindow::executeQuery() SqlExecutionArea* sqlWidget = qobject_cast(ui->tabSqlAreas->currentWidget()); SqlTextEdit* editor = sqlWidget->getEditor(); auto* current_tab = ui->tabSqlAreas->currentWidget(); - const QString tabName = ui->tabSqlAreas->tabText(ui->tabSqlAreas->currentIndex()).remove('&'); + const QString tabName = ui->tabSqlAreas->tabText(ui->tabSqlAreas->currentIndex()).remove('&').remove(QRegularExpression("\\*$")); // Remove any error indicators editor->clearErrorIndicators(); @@ -1203,7 +1255,8 @@ void MainWindow::executeQuery() if (char_at_index == '\r' || char_at_index == '\n') { execute_from_line++; // The next lines could be empty, so skip all of them too. - while(editor->text(execute_from_line).trimmed().isEmpty()) + while(execute_from_line <= editor->lines() && + editor->text(execute_from_line).trimmed().isEmpty()) execute_from_line++; execute_from_index = 0; } @@ -1284,7 +1337,11 @@ void MainWindow::executeQuery() auto time_end = std::chrono::high_resolution_clock::now(); auto time_in_ms = std::chrono::duration_cast(time_end-time_start); - query_logger(true, tr("%1 rows returned in %2ms").arg(model->rowCount()).arg(time_in_ms.count()+time_in_ms_so_far), from_position, to_position); + if (model->hasError()) { + query_logger(false, model->lastError(), from_position, to_position); + } else { + query_logger(true, tr("%1 rows returned in %2ms").arg(model->rowCount()).arg(time_in_ms.count()+time_in_ms_so_far), from_position, to_position); + } execute_sql_worker->startNextStatement(); }); }, Qt::QueuedConnection); @@ -1410,7 +1467,7 @@ void MainWindow::importTableFromCSV() } else if(sender() == ui->actionFileImportCsvClipboard) { // Save clipboard content to temporary file - QTemporaryFile temp("csv_clipboard"); + QTemporaryFile temp(QDir::tempPath() + QDir::separator() + "csv_clipboard"); temp.open(); QClipboard* clipboard = QGuiApplication::clipboard(); temp.write(clipboard->text().toUtf8()); @@ -1428,40 +1485,32 @@ void MainWindow::exportTableToCSV() { // Get the current table name if we are in the Browse Data tab sqlb::ObjectIdentifier current_table; - if(ui->mainTab->currentWidget() == ui->structure) - { - QString type = dbSelected->objectType(); - if(type == "table" || type == "view") - { - current_table = dbSelected->object(); - } - } else if(ui->mainTab->currentWidget() == ui->browser) { - current_table = currentlyBrowsedTableName(); - } - // Open dialog - ExportDataDialog dialog(db, ExportDataDialog::ExportFormatCsv, this, "", current_table); - dialog.exec(); + QString type; + // Get name and type of object to export + getSelectedObject(type, current_table); + + if(type == "table" || type == "view") { + // Open dialog + ExportDataDialog dialog(db, ExportDataDialog::ExportFormatCsv, this, "", current_table); + dialog.exec(); + } } void MainWindow::exportTableToJson() { // Get the current table name if we are in the Browse Data tab sqlb::ObjectIdentifier current_table; - if(ui->mainTab->currentWidget() == ui->structure) - { - QString type = dbSelected->objectType(); - if(type == "table" || type == "view") - { - current_table = dbSelected->object(); - } - } else if(ui->mainTab->currentWidget() == ui->browser) { - current_table = currentlyBrowsedTableName(); - } - // Open dialog - ExportDataDialog dialog(db, ExportDataDialog::ExportFormatJson, this, "", current_table); - dialog.exec(); + QString type; + // Get name and type of object to export + getSelectedObject(type, current_table); + + if(type == "table" || type == "view") { + // Open dialog + ExportDataDialog dialog(db, ExportDataDialog::ExportFormatJson, this, "", current_table); + dialog.exec(); + } } void MainWindow::dbState(bool dirty) @@ -1609,7 +1658,7 @@ void MainWindow::createTreeContextMenu(const QPoint &qPoint) if(type == "table" || type == "view" || type == "trigger" || type == "index" || type == "database") { // needed for first click on treeView as for first time change QItemSelectionModel::currentChanged doesn't fire - changeTreeSelection(); + changeObjectSelection(); popupTableMenu->exec(ui->dbTreeWidget->mapToGlobal(qPoint)); } } @@ -1634,7 +1683,7 @@ void MainWindow::createSchemaDockContextMenu(const QPoint &qPoint) popupSchemaDockMenu->exec(ui->treeSchemaDock->mapToGlobal(qPoint)); } -void MainWindow::changeTreeSelection() +void MainWindow::changeObjectSelection() { // Just assume first that something's selected that can not be edited at all ui->editDeleteObjectAction->setEnabled(false); @@ -1646,12 +1695,14 @@ void MainWindow::changeTreeSelection() ui->fileDetachAction->setVisible(false); - if(!dbSelected->hasSelection()) + QString type; + sqlb::ObjectIdentifier obj; + getSelectedObject(type, obj); + if(obj.isEmpty()) return; // Change the text and tooltips of the actions - QString type = dbSelected->objectType(); - QString schema = dbSelected->schema(); + QString schema = QString::fromStdString(obj.schema()); if (type.isEmpty()) { @@ -1925,8 +1976,6 @@ void MainWindow::activateFields(bool enable) if(!enable) ui->actionSqlResultsSave->setEnabled(false); - remoteDock->enableButtons(); - for(const auto& d : allTableBrowserDocks()) d->tableBrowser()->setEnabled(enable); } @@ -2120,6 +2169,26 @@ void MainWindow::closeSqlTab(int index, bool force, bool askSaving) focusSqlEditor(); } +void MainWindow::markTabsModified() +{ + // Add (and remove) an asterisk next to the filename of modified file tabs. + for (int i = 0; i < ui->tabSqlAreas->count(); ++i) { + SqlExecutionArea* sqlWidget = qobject_cast(ui->tabSqlAreas->widget(i)); + QString currentText = ui->tabSqlAreas->tabText(i); + if (!currentText.endsWith("*")) { + if (sqlWidget->getEditor()->isModified()) { + ui->tabSqlAreas->setTabText(i, currentText + "*"); + } + } + else { + if (!sqlWidget->getEditor()->isModified()) { + currentText.chop(1); + ui->tabSqlAreas->setTabText(i, currentText); + } + } + } +} + int MainWindow::openSqlTab(bool resetCounter) { static int tabNumber = 0; @@ -2137,23 +2206,7 @@ int MainWindow::openSqlTab(bool resetCounter) w->getEditor()->setEnabledFindDialog(false); w->getEditor()->setFocus(); connect(w, &SqlExecutionArea::findFrameVisibilityChanged, ui->actionSqlFind, &QAction::setChecked); - // Add (and remove) an asterisk next to the filename of modified file tabs. - connect(w->getEditor(), &SqlTextEdit::modificationChanged, this, [this](bool) { - for(int i=0; i < ui->tabSqlAreas->count(); ++i) { - SqlExecutionArea* sqlWidget = qobject_cast(ui->tabSqlAreas->widget(i)); - QString currentText = ui->tabSqlAreas->tabText(i); - if(!currentText.endsWith("*")) { - if(sqlWidget->getEditor()->isModified()) { - ui->tabSqlAreas->setTabText(i, currentText + "*"); - } - } else { - if(!sqlWidget->getEditor()->isModified()) { - currentText.chop(1); - ui->tabSqlAreas->setTabText(i, currentText); - } - } - } - }); + connect(w->getEditor(), &SqlTextEdit::modificationChanged, this, &MainWindow::markTabsModified); // Connect now the find shortcut to the editor with widget context, so it isn't ambiguous with other Scintilla Widgets. QShortcut* shortcutFind = new QShortcut(ui->actionSqlFind->shortcut(), w->getEditor(), nullptr, nullptr, Qt::WidgetShortcut); @@ -2412,24 +2465,32 @@ void MainWindow::reloadSettings() emit db.structureUpdated(); refreshTableBrowsers(); - // Hide or show the remote dock as needed - bool showRemoteActions = Settings::getValue("remote", "active").toBool(); - ui->viewMenu->actions().at(4)->setVisible(showRemoteActions); - if(!showRemoteActions) - ui->dockRemote->setHidden(true); + sqlb::setIdentifierQuoting(static_cast(Settings::getValue("editor", "identifier_quotes").toInt())); - // Reload remote dock settings - remoteDock->reloadSettings(); + ui->tabSqlAreas->setTabsClosable( + Settings::getValue("editor", "close_button_on_tabs").toBool()); - sqlb::setIdentifierQuoting(static_cast(Settings::getValue("editor", "identifier_quotes").toInt())); + ui->actionDropSelectQueryCheck->setChecked(Settings::getValue("SchemaDock", "dropSelectQuery").toBool()); + ui->actionDropInsertCheck->setChecked(Settings::getValue("SchemaDock", "dropInsert").toBool()); + ui->actionDropNamesCheck->setChecked(!ui->actionDropSelectQueryCheck->isChecked() && + !ui->actionDropInsertCheck->isChecked()); - ui->tabSqlAreas->setTabsClosable(Settings::getValue("editor", "close_button_on_tabs").toBool()); + ui->actionDropQualifiedCheck->setChecked(Settings::getValue("SchemaDock", "dropQualifiedNames").toBool()); + ui->actionEnquoteNamesCheck->setChecked(Settings::getValue("SchemaDock", "dropEnquotedNames").toBool()); + + if (Settings::getValue("db", "watcher").toBool()) + connect(&fileSystemWatch, &QFileSystemWatcher::fileChanged, this, &MainWindow::refreshDb, Qt::UniqueConnection); + else { + disconnect(&fileSystemWatch, &QFileSystemWatcher::fileChanged, nullptr, nullptr); + if (!fileSystemWatch.files().isEmpty()) + fileSystemWatch.removePaths(fileSystemWatch.files()); + } } void MainWindow::checkNewVersion(const bool automatic) { RemoteNetwork::get().fetch(QUrl("https://download.sqlitebrowser.org/currentrelease"), RemoteNetwork::RequestTypeCustom, - QString(), [this, automatic](const QByteArray& reply) { + [this, automatic](const QByteArray& reply) { QList info = reply.split('\n'); if(info.size() >= 2) { @@ -3244,7 +3305,7 @@ void MainWindow::saveProject(const QString& currentFilename) SqlExecutionArea* sqlArea = qobject_cast(ui->tabSqlAreas->widget(i)); QString sqlFilename = sqlArea->fileName(); xml.writeStartElement("sql"); - xml.writeAttribute("name", ui->tabSqlAreas->tabText(i)); + xml.writeAttribute("name", ui->tabSqlAreas->tabText(i).remove(QRegularExpression("\\*$"))); if(sqlFilename.isEmpty()) { xml.writeCharacters(sqlArea->getSql()); sqlArea->getEditor()->setModified(false); @@ -3501,10 +3562,12 @@ void MainWindow::renameSqlTab(int index) qApp->applicationName(), tr("Set a new name for the SQL tab. Use the '&&' character to allow using the following character as a keyboard shortcut."), QLineEdit::EchoMode::Normal, - ui->tabSqlAreas->tabText(index)); + ui->tabSqlAreas->tabText(index).remove(QRegularExpression("\\*$"))); + if(!new_name.isNull()) // Don't do anything if the Cancel button was clicked ui->tabSqlAreas->setTabText(index, new_name); + markTabsModified(); } void MainWindow::setFindFrameVisibility(bool show) @@ -3788,7 +3851,7 @@ void MainWindow::showContextMenuSqlTabBar(const QPoint& pos) QAction* actionDuplicate = new QAction(this); actionDuplicate->setText(tr("Duplicate Tab")); connect(actionDuplicate, &QAction::triggered, this, [this, tab]() { - QString tab_name = ui->tabSqlAreas->tabText(tab).remove("&").remove(QRegularExpression(" \\(\\d+\\)$")); + QString tab_name = ui->tabSqlAreas->tabText(tab).remove("&").remove(QRegularExpression("\\*$")).remove(QRegularExpression(" \\(\\d+\\)$")); QString new_tab_name; for(int i=1;;i++) { @@ -3796,7 +3859,7 @@ void MainWindow::showContextMenuSqlTabBar(const QPoint& pos) bool name_already_exists = false; for(int j=0;jtabSqlAreas->count();j++) { - if(ui->tabSqlAreas->tabText(j).remove("&") == new_tab_name) + if(ui->tabSqlAreas->tabText(j).remove("&").remove(QRegularExpression("\\*$")) == new_tab_name) { name_already_exists = true; break; @@ -3856,7 +3919,6 @@ void MainWindow::moveDocksTo(Qt::DockWidgetArea area) addDockWidget(area, ui->dockLog); tabifyDockWidget(ui->dockLog, ui->dockPlot); tabifyDockWidget(ui->dockLog, ui->dockSchema); - tabifyDockWidget(ui->dockLog, ui->dockRemote); } void MainWindow::clearRecentFiles() diff --git a/src/MainWindow.h b/src/MainWindow.h index 25f4818513..75d5e16c6c 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -6,7 +6,9 @@ #include #include + #include +#include struct BrowseDataTableSettings; class DbStructureModel; @@ -14,7 +16,6 @@ class EditDialog; class ExtendedTableWidget; class FindReplaceDialog; class PlotDock; -class RemoteDock; class RunSql; class SqliteTableModel; class TableBrowser; @@ -111,7 +112,6 @@ friend TableBrowserDock; EditDialog* editDock; PlotDock* plotDock; - RemoteDock* remoteDock; TableBrowser* currentTableBrowser; FindReplaceDialog* findReplaceDialog; @@ -123,6 +123,8 @@ friend TableBrowserDock; QString currentProjectFilename; bool isProjectModified; + QFileSystemWatcher fileSystemWatch; + void init(); void clearCompleterModelsFields(); @@ -148,6 +150,7 @@ friend TableBrowserDock; sqlb::ObjectIdentifier currentlyBrowsedTableName() const; QList allTableBrowserDocks() const; + void getSelectedObject(QString &type, sqlb::ObjectIdentifier& obj); protected: void closeEvent(QCloseEvent *) override; @@ -172,11 +175,12 @@ public slots: private slots: void createTreeContextMenu(const QPoint & qPoint); void createSchemaDockContextMenu(const QPoint & qPoint); - void changeTreeSelection(); + void changeObjectSelection(); void fileNew(); void fileNewInMemoryDatabase(bool open_create_dialog = true); // Refresh visible table browsers. When all is true, refresh all browsers. void refreshTableBrowsers(bool all = false); + void refreshDb(); bool fileClose(); bool fileSaveAs(); void createTable(); @@ -244,6 +248,7 @@ private slots: void openUrlOrFile(const QString& urlString); void newRowCountsTab(); + void markTabsModified(); int openSqlTab(bool resetCounter = false); void closeSqlTab(int index, bool force = false, bool askSaving = true); void changeSqlTab(int index); diff --git a/src/MainWindow.ui b/src/MainWindow.ui index b8304b83ba..e3cc4b5066 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -1480,15 +1480,6 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed - - - &Remote - - - 2 - - - Project Toolbar @@ -2406,7 +2397,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed true - Drag && Drop SELECT Query + SELECT Statement When dragging fields from the same table or a single table, drop a SELECT query into the editor @@ -2415,12 +2406,40 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed When dragging fields from the same table or a single table, drop a SELECT query into the editor + + + true + + + INSERT Statement + + + When dragging fields from the same table or a single table, drop an INSERT statement into the editor + + + When dragging fields from the same table or a single table, drop an INSERT statement into the editor + + + + + true + + + Just Names + + + When dragging fields or tables, drop just the names into the editor + + + When dragging fields or tables, drop just the names into the editor + + true - Drag && Drop Qualified Names + Qualified Names Use qualified names (e.g. "Table"."Field") when dragging the objects and dropping them into the editor @@ -2434,7 +2453,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed true - Drag && Drop Enquoted Names + Enquoted Names Use escaped identifiers (e.g. "Table1") when dragging the objects and dropping them into the editor @@ -2443,6 +2462,24 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Use escaped identifiers (e.g. "Table1") when dragging the objects and dropping them into the editor + + + Copy + + + + :/icons/copy:/icons/copy + + + Ctrl+C + + + Copies to the clipboard the selection according to the chosen Drag & Drop/Clipboard options. + + + Copies to the clipboard the selection according to the chosen Drag & Drop/Clipboard options. + + &Integrity Check diff --git a/src/PreferencesDialog.cpp b/src/PreferencesDialog.cpp index 46640c8951..7c98f99052 100644 --- a/src/PreferencesDialog.cpp +++ b/src/PreferencesDialog.cpp @@ -2,9 +2,7 @@ #include "ui_PreferencesDialog.h" #include "FileDialog.h" #include "Settings.h" -#include "Application.h" #include "MainWindow.h" -#include "RemoteNetwork.h" #include "FileExtensionManager.h" #include "ProxyDialog.h" @@ -24,7 +22,6 @@ PreferencesDialog::PreferencesDialog(QWidget* parent, Tabs tab) { ui->setupUi(this); ui->treeSyntaxHighlighting->setColumnHidden(0, true); - ui->tableClientCerts->setColumnHidden(0, true); ui->fr_bin_bg->installEventFilter(this); ui->fr_bin_fg->installEventFilter(this); @@ -102,6 +99,7 @@ void PreferencesDialog::loadSettings() } ui->spinStructureFontSize->setValue(Settings::getValue("db", "fontsize").toInt()); + ui->watcherCheckBox->setChecked(Settings::getValue("db", "watcher").toBool()); // Gracefully handle the preferred Data Browser font not being available int matchingFont = ui->comboDataBrowserFont->findText(Settings::getValue("databrowser", "font").toString(), Qt::MatchExactly); @@ -122,6 +120,7 @@ void PreferencesDialog::loadSettings() ui->spinSymbolLimit->setValue(Settings::getValue("databrowser", "symbol_limit").toInt()); ui->spinCompleteThreshold->setValue(Settings::getValue("databrowser", "complete_threshold").toInt()); ui->checkShowImagesInline->setChecked(Settings::getValue("databrowser", "image_preview").toBool()); + ui->checkCellWordWrap->setChecked(Settings::getValue("databrowser", "cell_word_wrap").toBool()); ui->txtNull->setText(Settings::getValue("databrowser", "null_text").toString()); ui->txtBlob->setText(Settings::getValue("databrowser", "blob_text").toString()); ui->editFilterEscape->setText(Settings::getValue("databrowser", "filter_escape").toString()); @@ -148,47 +147,6 @@ void PreferencesDialog::loadSettings() } } - // Remote settings - ui->checkUseRemotes->setChecked(Settings::getValue("remote", "active").toBool()); - { - auto ca_certs = RemoteNetwork::get().caCertificates(); - ui->tableCaCerts->setRowCount(ca_certs.size()); - for(int i=0;isetFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - ui->tableCaCerts->setItem(i, 0, cert_cn); - - QTableWidgetItem* cert_o = new QTableWidgetItem(cert.subjectInfo(QSslCertificate::Organization).at(0)); - cert_o->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - ui->tableCaCerts->setItem(i, 1, cert_o); - - QTableWidgetItem* cert_from = new QTableWidgetItem(cert.effectiveDate().toString()); - cert_from->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - ui->tableCaCerts->setItem(i, 2, cert_from); - - QTableWidgetItem* cert_to = new QTableWidgetItem(cert.expiryDate().toString()); - cert_to->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - ui->tableCaCerts->setItem(i, 3, cert_to); - - QTableWidgetItem* cert_serialno = new QTableWidgetItem(QString(cert.serialNumber())); - cert_serialno->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - ui->tableCaCerts->setItem(i, 4, cert_serialno); - } - } - { - const QStringList client_certs = Settings::getValue("remote", "client_certificates").toStringList(); - for(const QString& file : client_certs) - { - const auto certs = QSslCertificate::fromPath(file); - for(const QSslCertificate& cert : certs) - addClientCertToTable(file, cert); - } - } - ui->editRemoteCloneDirectory->setText(QDir::toNativeSeparators(Settings::getValue("remote", "clonedirectory").toString())); - // Gracefully handle the preferred Editor font not being available matchingFont = ui->comboEditorFont->findText(Settings::getValue("editor", "font").toString(), Qt::MatchExactly); if (matchingFont == -1) @@ -240,12 +198,14 @@ void PreferencesDialog::saveSettings(bool accept) Settings::setValue("db", "defaultsqltext", ui->editDatabaseDefaultSqlText->text()); Settings::setValue("db", "defaultfieldtype", ui->defaultFieldTypeComboBox->currentIndex()); Settings::setValue("db", "fontsize", ui->spinStructureFontSize->value()); + Settings::setValue("db", "watcher", ui->watcherCheckBox->isChecked()); Settings::setValue("checkversion", "enabled", ui->checkUpdates->isChecked()); Settings::setValue("databrowser", "font", ui->comboDataBrowserFont->currentText()); Settings::setValue("databrowser", "fontsize", ui->spinDataBrowserFontSize->value()); Settings::setValue("databrowser", "image_preview", ui->checkShowImagesInline->isChecked()); + Settings::setValue("databrowser", "cell_word_wrap", ui->checkCellWordWrap->isChecked()); saveColorSetting(ui->fr_null_fg, "null_fg"); saveColorSetting(ui->fr_null_bg, "null_bg"); saveColorSetting(ui->fr_reg_fg, "reg_fg"); @@ -294,61 +254,6 @@ void PreferencesDialog::saveSettings(bool accept) builtinExtList.insert(ui->listBuiltinExtensions->item(i)->text(), ui->listBuiltinExtensions->item(i)->checkState()); Settings::setValue("extensions", "builtin", QVariant::fromValue(builtinExtList)); - // Save remote settings - Settings::setValue("remote", "active", ui->checkUseRemotes->isChecked()); - QStringList old_client_certs = Settings::getValue("remote", "client_certificates").toStringList(); - QStringList new_client_certs; - for(int i=0;itableClientCerts->rowCount();i++) - { - // Loop through the new list of client certs - - // If this certificate was already imported, remove it from the list of old certificates. All remaining certificates on this - // list will be deleted later on. - QString path = ui->tableClientCerts->item(i, 0)->text(); - if(old_client_certs.contains(path)) - { - // This is a cert that is already imported - old_client_certs.removeAll(path); - new_client_certs.push_back(path); - } else { - // This is a new certificate. Copy file to a safe place. - - // Generate unique destination file name - QString copy_to = QStandardPaths::writableLocation( -#if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0) - QStandardPaths::AppDataLocation -#else - QStandardPaths::GenericDataLocation -#endif - ).append("/").append(QFileInfo(path).fileName()); - int suffix = 0; - do - { - suffix++; - } while(QFile::exists(copy_to + QString::number(suffix))); - - // Copy file - copy_to.append(QString::number(suffix)); - QDir().mkpath(QStandardPaths::writableLocation( -#if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0) - QStandardPaths::AppDataLocation -#else - QStandardPaths::GenericDataLocation -#endif - )); - QFile::copy(path, copy_to); - - new_client_certs.push_back(copy_to); - } - } - for(const QString& file : qAsConst(old_client_certs)) - { - // Now only the deleted client certs are still in the old list. Delete the cert files associated with them. - QFile::remove(file); - } - Settings::setValue("remote", "client_certificates", new_client_certs); - Settings::setValue("remote", "clonedirectory", ui->editRemoteCloneDirectory->text()); - // Warn about restarting to change language QVariant newLanguage = ui->languageComboBox->itemData(ui->languageComboBox->currentIndex()); if (newLanguage != Settings::getValue("General", "language")) @@ -654,96 +559,6 @@ void PreferencesDialog::adjustColorsToStyle(int style) } } -void PreferencesDialog::activateRemoteTab(bool active) -{ - ui->tabWidget->setTabEnabled(ui->tabWidget->indexOf(ui->tabRemote), active); -} - -void PreferencesDialog::addClientCertificate() -{ - // Get certificate file to import and abort here if no file gets selected - // NOTE: We assume here that this file contains both, certificate and private key! - QString path = FileDialog::getOpenFileName(OpenCertificateFile, this, tr("Import certificate file"), "*.pem"); - if(path.isEmpty()) - return; - - // Open file and check if any certificates were imported - auto certs = QSslCertificate::fromPath(path); - if(certs.size() == 0) - { - QMessageBox::warning(this, qApp->applicationName(), tr("No certificates found in this file.")); - return; - } - - // Add certificates to list - for(int i=0;itableClientCerts->currentRow(); - if(row == -1) - return; - - // Double check - if(QMessageBox::question(this, qApp->applicationName(), tr("Are you sure you want do remove this certificate? All certificate " - "data will be deleted from the application settings!"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) - { - ui->tableClientCerts->removeRow(row); - } -} - -void PreferencesDialog::addClientCertToTable(const QString& path, const QSslCertificate& cert) -{ - // Do nothing if the file doesn't even exist - if(!QFile::exists(path)) - return; - - // Add new row - int row = ui->tableClientCerts->rowCount(); - ui->tableClientCerts->setRowCount(row + 1); - - // Fill row with data - QTableWidgetItem* cert_file = new QTableWidgetItem(path); - cert_file->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - ui->tableClientCerts->setItem(row, 0, cert_file); - - QTableWidgetItem* cert_subject_cn = new QTableWidgetItem(cert.subjectInfo(QSslCertificate::CommonName).at(0)); - cert_subject_cn->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - ui->tableClientCerts->setItem(row, 1, cert_subject_cn); - - QTableWidgetItem* cert_issuer_cn = new QTableWidgetItem(cert.issuerInfo(QSslCertificate::CommonName).at(0)); - cert_issuer_cn->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - ui->tableClientCerts->setItem(row, 2, cert_issuer_cn); - - QTableWidgetItem* cert_from = new QTableWidgetItem(cert.effectiveDate().toString()); - cert_from->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - ui->tableClientCerts->setItem(row, 3, cert_from); - - QTableWidgetItem* cert_to = new QTableWidgetItem(cert.expiryDate().toString()); - cert_to->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - ui->tableClientCerts->setItem(row, 4, cert_to); - - QTableWidgetItem* cert_serialno = new QTableWidgetItem(QString(cert.serialNumber())); - cert_serialno->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - ui->tableClientCerts->setItem(row, 5, cert_serialno); -} - -void PreferencesDialog::chooseRemoteCloneDirectory() -{ - QString s = FileDialog::getExistingDirectory( - NoSpecificType, - this, - tr("Choose a directory"), - QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); - - if(!s.isEmpty() && QDir().mkpath(s)) - ui->editRemoteCloneDirectory->setText(s); -} - void PreferencesDialog::updatePreviewFont() { if (ui->spinDataBrowserFontSize->value() != 0) { diff --git a/src/PreferencesDialog.h b/src/PreferencesDialog.h index b42e8935a4..fae1e5bc96 100644 --- a/src/PreferencesDialog.h +++ b/src/PreferencesDialog.h @@ -5,7 +5,6 @@ class QTreeWidgetItem; class QFrame; -class QSslCertificate; class QAbstractButton; class ProxyDialog; @@ -25,8 +24,7 @@ class PreferencesDialog : public QDialog TabDatabase, TabDataBrowser, TabSql, - TabExtensions, - TabRemote + TabExtensions }; explicit PreferencesDialog(QWidget* parent = nullptr, Tabs tab = TabGeneral); @@ -41,10 +39,6 @@ private slots: void addExtension(); void createBuiltinExtensionList(); void removeExtension(); - void activateRemoteTab(bool active); - void addClientCertificate(); - void removeClientCertificate(); - void chooseRemoteCloneDirectory(); void updatePreviewFont(); void adjustColorsToStyle(int style); void configureProxy(); @@ -63,7 +57,6 @@ private slots: void loadColorSetting(QFrame *frame, const std::string& name); void setColorSetting(QFrame* frame, const QColor &color); void saveColorSetting(QFrame* frame, const std::string& name); - void addClientCertToTable(const QString& path, const QSslCertificate& cert); void exportSettings(); void importSettings(); diff --git a/src/PreferencesDialog.ui b/src/PreferencesDialog.ui index 2a5cb785ef..45f9f1ee3b 100644 --- a/src/PreferencesDialog.ui +++ b/src/PreferencesDialog.ui @@ -122,26 +122,29 @@ - - + + - Toolbar style + Application style - toolbarStyleComboMain + toolbarStyleComboStructure - - + + 0 0 + + When this value is changed, all the other color preferences are also set to matching colors. + - 2 + 0 QComboBox::AdjustToContents @@ -157,33 +160,85 @@ - Only display the icon - - - - - Only display the text - - - - - The text appears beside the icon + Follow the desktop style - The text appears under the icon + Dark style - Follow the style + Light style - - + + + + This sets the font size for all UI elements which do not have their own font size option. + + + Font size + + + spinGeneralFontSize + + + + + + + + + + Max Recent Files + + + spinMaxRecentFiles + + + + + + + QAbstractSpinBox::CorrectToNearestValue + + + 1 + + + 40 + + + + + + + Toolbar style + + + toolbarStyleComboMain + + + + + + + Main Window + + + 20 + + + toolbarStyleComboMain + + + + + 0 @@ -232,8 +287,21 @@ - - + + + + Database Structure + + + 20 + + + toolbarStyleComboStructure + + + + + 0 @@ -241,7 +309,7 @@ - 0 + 2 QComboBox::AdjustToContents @@ -282,8 +350,21 @@ - - + + + + Browse Data + + + 20 + + + toolbarStyleComboBrowse + + + + + 0 @@ -332,99 +413,6 @@ - - - - Show remote options - - - checkUseRemotes - - - - - - - enabled - - - true - - - - - - - Automatic &updates - - - checkUpdates - - - - - - - enabled - - - - - - - DB file extensions - - - buttonManageFileExtension - - - - - - - Manage - - - - - - - Main Window - - - 20 - - - toolbarStyleComboMain - - - - - - - Database Structure - - - 20 - - - toolbarStyleComboStructure - - - - - - - Browse Data - - - 20 - - - toolbarStyleComboBrowse - - - @@ -438,8 +426,8 @@ - - + + 0 @@ -501,17 +489,14 @@ - - + + 0 0 - - When this value is changed, all the other color preferences are also set to matching colors. - 0 @@ -529,88 +514,103 @@ - Follow the desktop style + Only display the icon - Dark style + Only display the text - Light style + The text appears beside the icon + + + + + The text appears under the icon + + + + + Follow the style - - + + - Application style + Prompt to save SQL tabs +in new project file - toolbarStyleComboStructure + checkPromptSQLTabsInNewProject - - + + - This sets the font size for all UI elements which do not have their own font size option. + If this is turned on, then changes to the SQL editor generate a save a project confirmation dialog when closing the SQL editor tab. - Font size + enabled + + + true + + + + + + + Automatic &updates - spinGeneralFontSize + checkUpdates + + + + + + + enabled - - - - - + + - Max Recent Files + DB file extensions - spinMaxRecentFiles + buttonManageFileExtension - - - - 1 - - - 40 + + + + Manage - - + + - Prompt to save SQL tabs -in new project file + Proxy - checkPromptSQLTabsInNewProject + buttonProxy - - - - If this is turned on, then changes to the SQL editor generate a save a project confirmation dialog when closing the SQL editor tab. - + + - enabled - - - true + Configure @@ -637,7 +637,7 @@ in new project file - + Open databases with foreign keys enabled. @@ -684,6 +684,9 @@ in new project file + + QAbstractSpinBox::CorrectToNearestValue + 255 @@ -736,6 +739,29 @@ in new project file + + + + When the database is open in readonly mode, watch the database file and refresh when it is updated. + + + &When readonly, refresh on changed file + + + watcherCheckBox + + + + + + + enabled + + + true + + + @@ -751,7 +777,7 @@ in new project file - + 20 @@ -1209,8 +1235,21 @@ in new project file + + + + Enable word wrap in cell + + + checkCellWordWrap + + + + + QAbstractSpinBox::CorrectToNearestValue + 1 @@ -1231,6 +1270,9 @@ Can be set to 0 for disabling the functionalities. This is the maximum number of rows in a table for enabling the value completion based on current values in the column. Can be set to 0 for disabling completion. + + QAbstractSpinBox::CorrectToNearestValue + 0 @@ -1246,6 +1288,13 @@ Can be set to 0 for disabling completion. + + + + Enable this option to turn on word wrap in the cells. + + + @@ -1541,6 +1590,9 @@ Can be set to 0 for disabling completion. + + QAbstractSpinBox::CorrectToNearestValue + 1 @@ -1560,6 +1612,9 @@ Can be set to 0 for disabling completion. + + QAbstractSpinBox::CorrectToNearestValue + 1 @@ -1577,6 +1632,9 @@ Can be set to 0 for disabling completion. + + QAbstractSpinBox::CorrectToNearestValue + 1 @@ -1882,242 +1940,6 @@ Can be set to 0 for disabling completion. - - - Remote - - - - - - 0 - - - - Your certificates - - - - - - - 550 - 0 - - - - QAbstractItemView::ExtendedSelection - - - QAbstractItemView::SelectRows - - - QAbstractItemView::ScrollPerPixel - - - false - - - - File - - - - - Subject CN - - - Subject Common Name - - - - - Issuer CN - - - Issuer Common Name - - - - - Valid from - - - - - Valid to - - - - - Serial number - - - - - - - - - - - :/icons/trigger_create:/icons/trigger_create - - - - - - - ... - - - - :/icons/trigger_delete:/icons/trigger_delete - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - CA certificates - - - - - - - 550 - 0 - - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - QAbstractItemView::ScrollPerPixel - - - false - - - - Subject CN - - - Common Name - - - - - Subject O - - - Organization - - - - - Valid from - - - - - Valid to - - - - - Serial number - - - - - - - - - - - - - - Clone databases into - - - editRemoteCloneDirectory - - - - - - - - - false - - - - 0 - 0 - - - - - 316 - 0 - - - - - - - - ... - - - - - - - - - Proxy - - - buttonProxy - - - - - - - Configure - - - - - - - @@ -2153,7 +1975,7 @@ Can be set to 0 for disabling completion. SqlTextEdit - QTextEdit + QWidget
sqltextedit.h
1
@@ -2173,8 +1995,8 @@ Can be set to 0 for disabling completion. toolbarStyleComboSql toolbarStyleComboEditCell checkPromptSQLTabsInNewProject - checkUseRemotes checkUpdates + buttonProxy buttonManageFileExtension encodingComboBox foreignKeysCheckBox @@ -2182,7 +2004,6 @@ Can be set to 0 for disabling completion. spinPrefetchSize defaultFieldTypeComboBox spinStructureFontSize - editDatabaseDefaultSqlText comboDataBrowserFont spinDataBrowserFontSize fr_null_fg @@ -2200,6 +2021,7 @@ Can be set to 0 for disabling completion. spinSymbolLimit spinCompleteThreshold checkShowImagesInline + checkCellWordWrap spinFilterDelay editFilterEscape treeSyntaxHighlighting @@ -2218,16 +2040,9 @@ Can be set to 0 for disabling completion. listExtensions buttonAddExtension buttonRemoveExtension + listBuiltinExtensions checkRegexDisabled checkAllowLoadExtension - tabCertificates - tableClientCerts - buttonClientCertAdd - buttonClientCertRemove - buttonProxy - editRemoteCloneDirectory - buttonRemoteBrowseCloneDirectory - tableCaCerts buttonExportSettings buttonImportSettings @@ -2299,70 +2114,6 @@ Can be set to 0 for disabling completion. - - checkUseRemotes - toggled(bool) - PreferencesDialog - activateRemoteTab(bool) - - - 301 - 503 - - - 382 - 572 - - - - - buttonClientCertAdd - clicked() - PreferencesDialog - addClientCertificate() - - - 723 - 108 - - - 596 - 243 - - - - - buttonClientCertRemove - clicked() - PreferencesDialog - removeClientCertificate() - - - 723 - 137 - - - 597 - 295 - - - - - buttonRemoteBrowseCloneDirectory - clicked() - PreferencesDialog - chooseRemoteCloneDirectory() - - - 733 - 568 - - - 595 - 41 - - - checkAutoCompletion toggled(bool) @@ -2396,34 +2147,34 @@ Can be set to 0 for disabling completion. - buttonProxy + buttonManageFileExtension clicked() PreferencesDialog - configureProxy() + showFileExtensionManager() - 251 - 537 + -1 + -1 - 385 - 306 + -1 + -1 - buttonManageFileExtension + buttonProxy clicked() PreferencesDialog - showFileExtensionManager() + configureProxy() - -1 - -1 + 247 + 538 - -1 - -1 + 385 + 306 @@ -2434,10 +2185,6 @@ Can be set to 0 for disabling completion. showColourDialog(QTreeWidgetItem*,int) addExtension() removeExtension() - activateRemoteTab(bool) - addClientCertificate() - removeClientCertificate() - chooseRemoteCloneDirectory() configureProxy() buttonBoxClicked() showFileExtensionManager() diff --git a/src/RemoteCommitsModel.cpp b/src/RemoteCommitsModel.cpp deleted file mode 100644 index 056af1526a..0000000000 --- a/src/RemoteCommitsModel.cpp +++ /dev/null @@ -1,171 +0,0 @@ -#include - -#include "Data.h" -#include "RemoteCommitsModel.h" -#include "Settings.h" - -using json = nlohmann::json; - -RemoteCommitsModel::RemoteCommitsModel(QObject* parent) : - QAbstractItemModel(parent) -{ - QStringList header; - header << tr("Commit ID") << tr("Message") << tr("Date") << tr("Author") << tr("Size"); - rootItem = new QTreeWidgetItem(header); -} - -RemoteCommitsModel::~RemoteCommitsModel() -{ - delete rootItem; -} - -void RemoteCommitsModel::clear() -{ - beginResetModel(); - - // Remove all data except for the root item - while(rootItem->childCount()) - delete rootItem->child(0); - - endResetModel(); -} - -void RemoteCommitsModel::refresh(const std::string& json_data, const std::string& last_commit_id, const std::string& current_commit_id) -{ - // Clear previous items - clear(); - beginResetModel(); - - // Read in all commits - json j = json::parse(json_data, nullptr, false); - std::unordered_map commit_to_iterator; - for(auto it=j.begin();it!=j.end();++it) - commit_to_iterator.insert({it.value()["id"], it}); - - // Starting from the last commit add follow the chain up to the first commit - std::string parent_id = last_commit_id; - while(true) - { - if(commit_to_iterator.find(parent_id) == commit_to_iterator.end()) - break; - - json commit = commit_to_iterator[parent_id].value(); - - QTreeWidgetItem* item = new QTreeWidgetItem(rootItem); - item->setText(ColumnCommitId, QString::fromStdString(commit["id"])); - item->setText(ColumnMessage, QString::fromStdString(commit["message"])); - item->setToolTip(ColumnMessage, QString::fromStdString(commit["message"])); - - item->setText(ColumnDate, isoDateTimeStringToLocalDateTimeString(QString::fromStdString(commit["timestamp"]))); - - QString authored_by = QString::fromStdString(commit["author_name"]) + " <" + QString::fromStdString(commit["author_email"]) + ">"; - QString committed_by = QString::fromStdString(commit["committer_name"]) + " <" + QString::fromStdString(commit["committer_email"]) + ">"; - item->setText(ColumnAuthor, authored_by); - if(committed_by == " <>" || authored_by == committed_by) // The first check effectively checks for no committer details - item->setToolTip(ColumnAuthor, tr("Authored and committed by %1").arg(authored_by)); - else - item->setToolTip(ColumnAuthor, tr("Authored by %1, committed by %2").arg(authored_by, committed_by)); - - for(const auto& e : commit["tree"]["entries"]) - { - if(e["entry_type"] == "db") - { - item->setText(ColumnSize, humanReadableSize(e["size"])); - break; - } - } - - // Make the currently checked out commit id bold - if(current_commit_id == commit["id"]) - { - QFont bold_font = item->font(ColumnCommitId); - bold_font.setBold(true); - item->setFont(0, bold_font); - } - - parent_id = commit["parent"].get(); - } - - // Refresh the view - endResetModel(); -} - -QModelIndex RemoteCommitsModel::index(int row, int column, const QModelIndex& parent) const -{ - if(!hasIndex(row, column, parent)) - return QModelIndex(); - - QTreeWidgetItem *parentItem; - if(!parent.isValid()) - parentItem = rootItem; - else - parentItem = static_cast(parent.internalPointer()); - - QTreeWidgetItem* childItem = parentItem->child(row); - if(childItem) - return createIndex(row, column, childItem); - else - return QModelIndex(); -} - -QModelIndex RemoteCommitsModel::parent(const QModelIndex& index) const -{ - if(!index.isValid()) - return QModelIndex(); - - QTreeWidgetItem* childItem = static_cast(index.internalPointer()); - QTreeWidgetItem* parentItem = childItem->parent(); - - if(parentItem == rootItem) - return QModelIndex(); - else - return createIndex(0, 0, parentItem); -} - -QVariant RemoteCommitsModel::data(const QModelIndex& index, int role) const -{ - if(!index.isValid()) - return QVariant(); - - // Get the item the index points at - QTreeWidgetItem* item = static_cast(index.internalPointer()); - - // Return data depending on the role - switch(role) - { - case Qt::DisplayRole: - case Qt::EditRole: - return item->text(index.column()); - case Qt::ToolTipRole: - return item->toolTip(index.column()); - case Qt::FontRole: - return item->font(0); // Choose font for the entire row depending on the first column - default: - return QVariant(); - } -} - -QVariant RemoteCommitsModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - // Get the header string from the root item - if(orientation == Qt::Horizontal && role == Qt::DisplayRole) - return rootItem->data(section, role); - - return QVariant(); -} - -int RemoteCommitsModel::rowCount(const QModelIndex& parent) const -{ - if(parent.column() > 0) - return 0; - - if(!parent.isValid()) - return rootItem->childCount(); - else - return static_cast(parent.internalPointer())->childCount(); -} - -int RemoteCommitsModel::columnCount(const QModelIndex& /*parent*/) const -{ - return rootItem->columnCount(); -} diff --git a/src/RemoteCommitsModel.h b/src/RemoteCommitsModel.h deleted file mode 100644 index d2b540cbc9..0000000000 --- a/src/RemoteCommitsModel.h +++ /dev/null @@ -1,44 +0,0 @@ -#ifndef REMOTECOMMITSMODEL_H -#define REMOTECOMMITSMODEL_H - -#include - -#include - -class QTreeWidgetItem; - -class RemoteCommitsModel : public QAbstractItemModel -{ - Q_OBJECT - -public: - explicit RemoteCommitsModel(QObject* parent); - ~RemoteCommitsModel() override; - - void clear(); - void refresh(const std::string& json_data, const std::string& last_commit_id, const std::string& current_commit_id); - - QModelIndex index(int row, int column,const QModelIndex& parent = QModelIndex()) const override; - QModelIndex parent(const QModelIndex& index) const override; - - QVariant data(const QModelIndex& index, int role) const override; - QVariant headerData(int section, Qt::Orientation orientation, int role) const override; - - int rowCount(const QModelIndex& parent = QModelIndex()) const override; - int columnCount(const QModelIndex& parent = QModelIndex()) const override; - - enum Columns - { - ColumnCommitId, - ColumnMessage, - ColumnDate, - ColumnAuthor, - ColumnSize, - }; - -private: - // Pointer to the root item. This contains all the actual item data. - QTreeWidgetItem* rootItem; -}; - -#endif diff --git a/src/RemoteDatabase.cpp b/src/RemoteDatabase.cpp deleted file mode 100644 index 5ce215e3df..0000000000 --- a/src/RemoteDatabase.cpp +++ /dev/null @@ -1,436 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "FileDialog.h" -#include "RemoteDatabase.h" -#include "Settings.h" -#include "sqlite.h" -#include "version.h" - -RemoteDatabase::RemoteDatabase() : - m_dbLocal(nullptr) -{ -} - -RemoteDatabase::~RemoteDatabase() -{ - // Close local storage db - but only if it was created/opened in the meantime - if(m_dbLocal) - sqlite3_close(m_dbLocal); -} - -void RemoteDatabase::localAssureOpened() -{ - // This function should be called first in each RemoteDatabase::local* function. It assures the database for storing - // the local database information is opened and ready. If the database file doesn't exist yet it is created by this - // function. If the database file is already created and opened this function does nothing. The reason to open the - // database on first use instead of doing that in the constructor of this class is that this way no database file is - // going to be created and no database handle is held when it's not actually needed. For people not interested in - // the dbhub.io functionality this means no unnecessary files being created. - - // Check if database is already opened and return if it is - if(m_dbLocal) - return; - - // Make sure the directory exists - QString database_directory = QStandardPaths::writableLocation( -#if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0) - QStandardPaths::AppDataLocation -#else - QStandardPaths::GenericDataLocation -#endif - ); - QDir().mkpath(database_directory); - - // Open file - QString database_file = database_directory + "/remotedbs.db"; - if(sqlite3_open_v2(database_file.toUtf8(), &m_dbLocal, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr) != SQLITE_OK) - { - QMessageBox::warning(nullptr, qApp->applicationName(), tr("Error opening local databases list.\n%1").arg(QString::fromUtf8(sqlite3_errmsg(m_dbLocal)))); - return; - } - - // Create local local table if it doesn't exists yet - char* errmsg; - QString statement = QString("CREATE TABLE IF NOT EXISTS \"local\"(" - "\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," - "\"identity\" TEXT NOT NULL," - "\"name\" TEXT NOT NULL," - "\"url\" TEXT NOT NULL," - "\"commit_id\" TEXT NOT NULL," - "\"file\" TEXT NOT NULL UNIQUE," - "\"modified\" INTEGER DEFAULT 0," - "\"branch\" TEXT NOT NULL DEFAULT \"main\"" - ")"); - if(sqlite3_exec(m_dbLocal, statement.toUtf8(), nullptr, nullptr, &errmsg) != SQLITE_OK) - { - QMessageBox::warning(nullptr, qApp->applicationName(), tr("Error creating local databases list.\n%1").arg(QString::fromUtf8(errmsg))); - sqlite3_free(errmsg); - sqlite3_close(m_dbLocal); - m_dbLocal = nullptr; - return; - } -} - -QString RemoteDatabase::localAdd(QString filename, QString identity, const QUrl& url, const std::string& new_commit_id, const std::string& branch) -{ - localAssureOpened(); - - // Remove the path - QFileInfo f(identity); - identity = f.fileName(); - - // Check if this file has already been checked in - std::string last_commit_id = localLastCommitId(identity, url.toString(), branch); - if(last_commit_id.empty()) - { - // The file hasn't been checked in yet. So add a new record for it. - - // Generate a new file name to save the file under - filename = QString("%2_%1.remotedb").arg(QDateTime::currentMSecsSinceEpoch()).arg(filename); - - // Insert database into local database list - QString sql = QString("INSERT INTO local(identity, name, url, commit_id, file, branch) VALUES(?, ?, ?, ?, ?, ?)"); - sqlite3_stmt* stmt; - if(sqlite3_prepare_v2(m_dbLocal, sql.toUtf8(), -1, &stmt, nullptr) != SQLITE_OK) - return QString(); - - if(sqlite3_bind_text(stmt, 1, identity.toUtf8(), identity.toUtf8().length(), SQLITE_TRANSIENT)) - { - sqlite3_finalize(stmt); - return QString(); - } - - if(sqlite3_bind_text(stmt, 2, url.fileName().toUtf8(), url.fileName().toUtf8().length(), SQLITE_TRANSIENT)) - { - sqlite3_finalize(stmt); - return QString(); - } - - if(sqlite3_bind_text(stmt, 3, url.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery).toUtf8(), - url.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery).toUtf8().length(), SQLITE_TRANSIENT)) - { - sqlite3_finalize(stmt); - return QString(); - } - - if(sqlite3_bind_text(stmt, 4, new_commit_id.c_str(), static_cast(new_commit_id.size()), SQLITE_TRANSIENT)) - { - sqlite3_finalize(stmt); - return QString(); - } - - if(sqlite3_bind_text(stmt, 5, filename.toUtf8(), filename.size(), SQLITE_TRANSIENT)) - { - sqlite3_finalize(stmt); - return QString(); - } - - if(sqlite3_bind_text(stmt, 6, branch.c_str(), static_cast(branch.size()), SQLITE_TRANSIENT)) - { - sqlite3_finalize(stmt); - return QString(); - } - - if(sqlite3_step(stmt) != SQLITE_DONE) - { - sqlite3_finalize(stmt); - return QString(); - } - - sqlite3_finalize(stmt); - - // Return full path to the new file - return Settings::getValue("remote", "clonedirectory").toString() + "/" + filename; - } - - // If we get here, the file has been checked in before. Check next if it has been updated in the meantime. - if(last_commit_id != new_commit_id) - { - // The file has already been checked in and the commit ids are different. If they weren't we wouldn't need to update anything - - QString sql = QString("UPDATE local SET commit_id=? WHERE identity=? AND url=? AND branch=?"); - sqlite3_stmt* stmt; - if(sqlite3_prepare_v2(m_dbLocal, sql.toUtf8(), -1, &stmt, nullptr) != SQLITE_OK) - return QString(); - - if(sqlite3_bind_text(stmt, 1, new_commit_id.c_str(), static_cast(new_commit_id.size()), SQLITE_TRANSIENT)) - { - sqlite3_finalize(stmt); - return QString(); - } - - if(sqlite3_bind_text(stmt, 2, identity.toUtf8(), identity.toUtf8().length(), SQLITE_TRANSIENT)) - { - sqlite3_finalize(stmt); - return QString(); - } - - if(sqlite3_bind_text(stmt, 3, url.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery).toUtf8(), - url.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery).toUtf8().length(), SQLITE_TRANSIENT)) - { - sqlite3_finalize(stmt); - return QString(); - } - - if(sqlite3_bind_text(stmt, 4, branch.c_str(), static_cast(branch.size()), SQLITE_TRANSIENT)) - { - sqlite3_finalize(stmt); - return QString(); - } - - if(sqlite3_step(stmt) != SQLITE_DONE) - { - sqlite3_finalize(stmt); - return QString(); - } - - sqlite3_finalize(stmt); - } - - // If we got here, the file was already checked in (and was either updated or not (obviously)). This mean we can just return the file name as - // we know it. - return localExists(url, identity, branch); -} - -QString RemoteDatabase::localExists(const QUrl& url, QString identity, const std::string& branch) -{ - localAssureOpened(); - - // Extract commit id from url and remove query part afterwards - QString url_commit_id = QUrlQuery(url).queryItemValue("commit"); - - // Query commit id and filename for the given combination of url and identity - QString sql = QString("SELECT id, commit_id, file FROM local WHERE url=? AND identity=? AND branch=?"); - sqlite3_stmt* stmt; - if(sqlite3_prepare_v2(m_dbLocal, sql.toUtf8(), -1, &stmt, nullptr) != SQLITE_OK) - return QString(); - - if(sqlite3_bind_text(stmt, 1, url.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery).toUtf8(), url.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery).toUtf8().length(), SQLITE_TRANSIENT)) - { - sqlite3_finalize(stmt); - return QString(); - } - - QFileInfo f(identity); // Remove the path - identity = f.fileName(); - if(sqlite3_bind_text(stmt, 2, identity.toUtf8(), identity.toUtf8().length(), SQLITE_TRANSIENT)) - { - sqlite3_finalize(stmt); - return QString(); - } - - if(sqlite3_bind_text(stmt, 3, branch.c_str(), static_cast(branch.size()), SQLITE_TRANSIENT)) - { - sqlite3_finalize(stmt); - return QString(); - } - - if(sqlite3_step(stmt) != SQLITE_ROW) - { - // If there was either an error or no record was found for this combination of url and - // identity, stop here. - sqlite3_finalize(stmt); - return QString(); - } - - // Having come here we can assume that at least some local clone for the given combination of - // url, identity and branch exists. So extract all the information we have on it. - QString local_commit_id = QString::fromUtf8(reinterpret_cast(sqlite3_column_text(stmt, 1))); - QString local_file = QString::fromUtf8(reinterpret_cast(sqlite3_column_text(stmt, 2))); - sqlite3_finalize(stmt); - - // There are three possibilities now: either we didn't get any commit id in the URL in which case we just return the file we got, no matter what. - // Or the requested commit id is the same as the local commit id in which case we return the file we got as well. - // Or the requested commit id differ in which case we return no match. - if(url_commit_id.isNull() || local_commit_id == url_commit_id) - { - // Both commit ids are the same. That's the perfect match, so we can open the local file if it still exists - return localCheckFile(local_file); - } else { - // The commit ids differ. This means we have no match - return QString(); - } -} - -QString RemoteDatabase::localCheckFile(const QString& local_file) -{ - // This function takes the file name of a locally cloned database and checks if this file still exists. If it has been deleted in the meantime it returns - // an empty string and deletes the file from the clone database. If the file still exists, it returns the full path to the file. - - localAssureOpened(); - - // Build the full path to where the file should be - QString full_path = Settings::getValue("remote", "clonedirectory").toString() + "/" + local_file; - - // Check if the database still exists. If so return its path, if not return an empty string to redownload it - if(QFile::exists(full_path)) - { - return full_path; - } else { - // Remove the apparently invalid entry from the local clones database to avoid future lookups and confusions. The file column should - // be unique for the entire table because the files are all in the same directory and their names need to be unique because of this. - localDeleteFile(local_file); - - // Return empty string to indicate a redownload request - return QString(); - } -} - -std::string RemoteDatabase::localLastCommitId(QString identity, const QUrl& url, const std::string& branch) -{ - localAssureOpened(); - - // Query commit id for that file name - QString sql = QString("SELECT commit_id FROM local WHERE identity=? AND url=? AND branch=?"); - sqlite3_stmt* stmt; - if(sqlite3_prepare_v2(m_dbLocal, sql.toUtf8(), -1, &stmt, nullptr) != SQLITE_OK) - return std::string(); - - QFileInfo f(identity); // Remove the path - identity = f.fileName(); - if(sqlite3_bind_text(stmt, 1, identity.toUtf8(), identity.toUtf8().length(), SQLITE_TRANSIENT)) - { - sqlite3_finalize(stmt); - return std::string(); - } - - if(sqlite3_bind_text(stmt, 2, url.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery).toUtf8(), - url.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery).toUtf8().size(), SQLITE_TRANSIENT)) - { - sqlite3_finalize(stmt); - return std::string(); - } - - if(sqlite3_bind_text(stmt, 3, branch.c_str(), static_cast(branch.size()), SQLITE_TRANSIENT)) - { - sqlite3_finalize(stmt); - return std::string(); - } - - if(sqlite3_step(stmt) != SQLITE_ROW) - { - // If there was either an error or no record was found for this file name, stop here. - sqlite3_finalize(stmt); - return std::string(); - } - - // Having come here we can assume that at least some local clone with the given file name - std::string local_commit_id = reinterpret_cast(sqlite3_column_text(stmt, 0)); - sqlite3_finalize(stmt); - - return local_commit_id; -} - -std::vector RemoteDatabase::localGetLocalFiles(QString identity) -{ - localAssureOpened(); - - // Get all rows for this identity - QString sql = QString("SELECT name, url, commit_id, file, branch FROM local WHERE identity=? ORDER BY url"); - sqlite3_stmt* stmt; - if(sqlite3_prepare_v2(m_dbLocal, sql.toUtf8(), -1, &stmt, nullptr) != SQLITE_OK) - return {}; - - QFileInfo f(identity); - identity = f.fileName(); - if(sqlite3_bind_text(stmt, 1, identity.toUtf8(), identity.toUtf8().length(), SQLITE_TRANSIENT)) - { - sqlite3_finalize(stmt); - return {}; - } - - std::vector result; - while(sqlite3_step(stmt) == SQLITE_ROW) - { - result.emplace_back(reinterpret_cast(sqlite3_column_text(stmt, 0)), - reinterpret_cast(sqlite3_column_text(stmt, 1)), - reinterpret_cast(sqlite3_column_text(stmt, 2)), - reinterpret_cast(sqlite3_column_text(stmt, 3)), - reinterpret_cast(sqlite3_column_text(stmt, 4)), - identity.toStdString()); - } - - sqlite3_finalize(stmt); - return result; -} - -RemoteDatabase::LocalFileInfo RemoteDatabase::localGetLocalFileInfo(QString filename) -{ - localAssureOpened(); - - // Find this file in our database - QString sql = QString("SELECT name, url, commit_id, branch, identity FROM local WHERE file=?"); - sqlite3_stmt* stmt; - if(sqlite3_prepare_v2(m_dbLocal, sql.toUtf8(), -1, &stmt, nullptr) != SQLITE_OK) - return {}; - - // Remove the path for querying the file name - filename = QFileInfo(filename).fileName(); - if(sqlite3_bind_text(stmt, 1, filename.toUtf8(), filename.toUtf8().length(), SQLITE_TRANSIENT)) - { - sqlite3_finalize(stmt); - return {}; - } - - if(sqlite3_step(stmt) != SQLITE_ROW) - { - // If there was either an error or no record was found for this file name, stop here. - sqlite3_finalize(stmt); - return {}; - } - - // Retrieve and return all the information we have - RemoteDatabase::LocalFileInfo result(reinterpret_cast(sqlite3_column_text(stmt, 0)), - reinterpret_cast(sqlite3_column_text(stmt, 1)), - reinterpret_cast(sqlite3_column_text(stmt, 2)), - filename.toStdString(), - reinterpret_cast(sqlite3_column_text(stmt, 3)), - reinterpret_cast(sqlite3_column_text(stmt, 4))); - sqlite3_finalize(stmt); - return result; -} - -void RemoteDatabase::localDeleteFile(QString filename) -{ - localAssureOpened(); - - // Remove the file's entry in our database - QString sql = QString("DELETE FROM local WHERE file=?"); - sqlite3_stmt* stmt; - if(sqlite3_prepare_v2(m_dbLocal, sql.toUtf8(), -1, &stmt, nullptr) != SQLITE_OK) - return; - if(sqlite3_bind_text(stmt, 1, filename.toUtf8(), filename.toUtf8().length(), SQLITE_TRANSIENT)) - { - sqlite3_finalize(stmt); - return; - } - if(sqlite3_step(stmt) != SQLITE_DONE) - { - sqlite3_finalize(stmt); - return; - } - sqlite3_finalize(stmt); - - // Delete the actual file on disk - QFile::remove(Settings::getValue("remote", "clonedirectory").toString() + "/" + filename); -} - -QString RemoteDatabase::LocalFileInfo::user_name() const -{ - // Figure out the user name from the URL - - QString path = QUrl(QString::fromStdString(url)).path(); - - if(path.count('/') < 2 || !path.startsWith('/')) - return QString(); - else - return path.mid(1, path.indexOf('/', 1) - 1); -} diff --git a/src/RemoteDatabase.h b/src/RemoteDatabase.h deleted file mode 100644 index c60f4546a9..0000000000 --- a/src/RemoteDatabase.h +++ /dev/null @@ -1,86 +0,0 @@ -#ifndef REMOTEDATABASE_H -#define REMOTEDATABASE_H - -#include -#include - -#include - -struct sqlite3; - -class RemoteDatabase : public QObject -{ - Q_OBJECT - -public: - RemoteDatabase(); - ~RemoteDatabase() override; - - // This class compiles all the information on a lcao database file - class LocalFileInfo - { - public: - LocalFileInfo() - {} - - LocalFileInfo(const std::string& _name, - const std::string& _url, - const std::string& _commit_id, - const std::string& _file, - const std::string& _branch, - const std::string& _identity) : - name(_name), - url(_url), - commit_id(_commit_id), - file(_file), - branch(_branch), - identity(_identity) - {} - - void clear() { name = url = commit_id = file = branch = identity = {}; } - QString user_name() const; - - std::string name; // Database name - std::string url; // URL for cloning - std::string commit_id; // Commit ID at the time of the cloning - std::string file; // Name of the local file on disk - std::string branch; // Cloned branch - std::string identity; // Identity used for cloning - }; - - // Return a list of all checked out databases for a given identity - std::vector localGetLocalFiles(QString identity); - - // Return information on a single file - LocalFileInfo localGetLocalFileInfo(QString filename); - - // Delete a local database clone - void localDeleteFile(QString filename); - - // This function checks if there already is a clone for the given combination of url and identity. It returns the filename - // of this clone if there is or a null string if there isn't a clone yet. The identity needs to be part of this check because - // with the url alone there could be corner cases where different versions or whatever may not be accessible for all users. - // If the URL contains a commit id (optional), this commit id is part of the check. - QString localExists(const QUrl& url, QString identity, const std::string& branch); - - // This function takes a file name and checks with which commit id we had checked out this file or last pushed it. - std::string localLastCommitId(QString clientCert, const QUrl& url, const std::string& branch); - - // This function adds a new local database clone to our internal list. It does so by adding a single - // new record to the remote dbs database. All the fields are extracted from the filename, the identity - // and (most importantly) the url parameters. Note that for the commit id field to be correctly filled we - // require the commit id to be part of the url parameter. Also note that this function doesn't care if the - // database has already been added to the list or not. If you need this information you need to check it before - // calling this function, ideally even before sending out a request to the network. The function returns the full - // path of the newly created/updated file. - QString localAdd(QString filename, QString identity, const QUrl& url, const std::string& new_commit_id, const std::string& branch); - -private: - // Helper functions for managing the list of locally available databases - void localAssureOpened(); - QString localCheckFile(const QString& local_file); - - sqlite3* m_dbLocal; -}; - -#endif diff --git a/src/RemoteDock.cpp b/src/RemoteDock.cpp deleted file mode 100644 index 82352d2d38..0000000000 --- a/src/RemoteDock.cpp +++ /dev/null @@ -1,674 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "RemoteDock.h" -#include "ui_RemoteDock.h" -#include "Settings.h" -#include "RemoteCommitsModel.h" -#include "RemoteDatabase.h" -#include "RemoteLocalFilesModel.h" -#include "RemoteModel.h" -#include "MainWindow.h" -#include "RemotePushDialog.h" -#include "PreferencesDialog.h" - -using json = nlohmann::json; - -RemoteDock::RemoteDock(MainWindow* parent) - : QDialog(parent), - ui(new Ui::RemoteDock), - mainWindow(parent), - remoteModel(new RemoteModel(this)), - remoteLocalFilesModel(new RemoteLocalFilesModel(this, remoteDatabase)), - remoteCommitsModel(new RemoteCommitsModel(this)) -{ - ui->setupUi(this); - - // Set models - ui->treeRemote->setModel(remoteModel); - ui->treeLocal->setModel(remoteLocalFilesModel); - ui->treeDatabaseCommits->setModel(remoteCommitsModel); - - // Set initial column widths for tree views - ui->treeRemote->setColumnWidth(0, 300); // Make name column wider - ui->treeRemote->setColumnWidth(2, 80); // Make size column narrower - ui->treeLocal->setColumnWidth(RemoteLocalFilesModel::ColumnName, 300); // Make name column wider - ui->treeLocal->setColumnWidth(RemoteLocalFilesModel::ColumnSize, 80); // Make size column narrower - ui->treeLocal->setColumnHidden(RemoteLocalFilesModel::ColumnFile, true); // Hide local file name - - // Handle finished uploads and downloads of databases - connect(&RemoteNetwork::get(), &RemoteNetwork::fetchFinished, this, &RemoteDock::fetchFinished); - connect(&RemoteNetwork::get(), &RemoteNetwork::pushFinished, this, &RemoteDock::pushFinished); - - // Whenever a new directory listing has been parsed, check if it was a new root dir and, if so, open the user's directory - connect(remoteModel, &RemoteModel::directoryListingParsed, this, &RemoteDock::newDirectoryNode); - - // When the Preferences link is clicked in the no-certificates-label, open the preferences dialog. For other links than the ones we know, - // just open them in a web browser - connect(ui->labelNoCert, &QLabel::linkActivated, this, [this](const QString& link) { - if(link == "#preferences") - { - PreferencesDialog dialog(mainWindow, PreferencesDialog::TabRemote); - if(dialog.exec()) - mainWindow->reloadSettings(); - } else { - QDesktopServices::openUrl(QUrl(link)); - } - }); - - // When changing the current branch in the branches combo box, update the tree view accordingly - connect(ui->comboDatabaseBranch, static_cast(&QComboBox::currentIndexChanged), this, [this](int /*index*/) { - remoteCommitsModel->refresh(current_commit_json, ui->comboDatabaseBranch->currentData().toString().toStdString(), currently_opened_file_info.commit_id); - ui->treeDatabaseCommits->expandAll(); - }); - - // Fetch latest commit action - connect(ui->actionFetchLatestCommit, &QAction::triggered, this, [this]() { - // Fetch last commit of current branch - // The URL and the branch name is that of the currently opened database file. - // The latest commit id is stored in the data bits of the branch combo box. - QUrl url(QString::fromStdString(currently_opened_file_info.url)); - QUrlQuery query; - query.addQueryItem("branch", QString::fromStdString(currently_opened_file_info.branch)); - query.addQueryItem("commit", ui->comboDatabaseBranch->itemData( - ui->comboDatabaseBranch->findText(QString::fromStdString(currently_opened_file_info.branch))).toString()); - url.setQuery(query); - fetchDatabase(url.toString()); - }); - - // Prepare context menu for list of remote databases - connect(ui->treeRemote->selectionModel(), &QItemSelectionModel::currentChanged, this, [this](const QModelIndex& index, const QModelIndex&) { - // Only enable database actions when a database was selected - bool enable = index.isValid() && - remoteModel->modelIndexToItem(index)->value(RemoteModelColumnType).toString() == "database"; - ui->actionCloneDatabaseDoubleClick->setEnabled(enable); - }); - emit ui->treeRemote->selectionModel()->currentChanged(QModelIndex(), QModelIndex()); // Enable/disable all action initially - connect(ui->actionCloneDatabaseDoubleClick, &QAction::triggered, this, [this]() { - fetchDatabase(ui->treeRemote->currentIndex()); - }); - ui->treeRemote->addAction(ui->actionCloneDatabaseDoubleClick); - - // Prepare context menu for list of local clones - connect(ui->treeLocal->selectionModel(), &QItemSelectionModel::currentChanged, this, [this](const QModelIndex& index, const QModelIndex&) { - // Only enable database actions when a database was selected - bool enable = index.isValid() && - !index.sibling(index.row(), RemoteLocalFilesModel::ColumnFile).data().isNull(); - ui->actionOpenLocalDatabase->setEnabled(enable); - ui->actionPushLocalDatabase->setEnabled(enable); - ui->actionDeleteDatabase->setEnabled(enable); - }); - emit ui->treeLocal->selectionModel()->currentChanged(QModelIndex(), QModelIndex()); // Enable/disable all action initially - connect(ui->actionOpenLocalDatabase, &QAction::triggered, this, [this]() { - openLocalFile(ui->treeLocal->currentIndex()); - }); - connect(ui->actionDeleteDatabase, &QAction::triggered, this, [this]() { - deleteLocalDatabase(ui->treeLocal->currentIndex()); - }); - ui->treeLocal->addAction(ui->actionOpenLocalDatabase); - ui->treeLocal->addAction(ui->actionPushLocalDatabase); - ui->treeLocal->addAction(ui->actionDeleteDatabase); - - // Prepare context menu for list of commits - connect(ui->treeDatabaseCommits->selectionModel(), &QItemSelectionModel::currentChanged, this, [this](const QModelIndex& index, const QModelIndex&) { - // Only enable database actions when a commit was selected - bool enable = index.isValid(); - ui->actionFetchCommit->setEnabled(enable); - ui->actionDownloadCommit->setEnabled(enable); - }); - emit ui->treeDatabaseCommits->selectionModel()->currentChanged(QModelIndex(), QModelIndex()); // Enable/disable all action initially - connect(ui->actionFetchCommit, &QAction::triggered, this, [this]() { - fetchCommit(ui->treeDatabaseCommits->currentIndex()); - }); - connect(ui->actionDownloadCommit, &QAction::triggered, this, [this]() { - fetchCommit(ui->treeDatabaseCommits->currentIndex(), RemoteNetwork::RequestTypeDownload); - }); - ui->treeDatabaseCommits->addAction(ui->actionFetchCommit); - ui->treeDatabaseCommits->addAction(ui->actionDownloadCommit); - - // Initial setup - reloadSettings(); -} - -RemoteDock::~RemoteDock() -{ - delete ui; -} - -void RemoteDock::reloadSettings() -{ - // Clear list of client certificates and add a dummy entry which does nothing except serve as - // an explanation to the user. - ui->comboUser->clear(); - ui->comboUser->addItem(tr("Select an identity to connect"), "dummy"); - - // Load list of client certs - const QStringList client_certs = Settings::getValue("remote", "client_certificates").toStringList(); - for(const QString& file : client_certs) - { - const auto certs = QSslCertificate::fromPath(file); - for(const QSslCertificate& cert : certs) - ui->comboUser->addItem(cert.subjectInfo(QSslCertificate::CommonName).at(0), file); - } - - // Add public certificate for anonymous read-only access to dbhub.io - ui->comboUser->addItem(tr("Public"), ":/user_certs/public.cert.pem"); -} - -void RemoteDock::setNewIdentity(const QString& identity) -{ - // Do nothing if the dummy entry was selected - if(ui->comboUser->currentData() == "dummy") - return; - - // Check if the dummy item is still there and remove it if it is - if(ui->comboUser->itemData(0) == "dummy") - { - ui->comboUser->blockSignals(true); - ui->comboUser->removeItem(0); - ui->comboUser->blockSignals(false); - } - - // Get certificate file name - QString cert = ui->comboUser->itemData(ui->comboUser->findText(identity), Qt::UserRole).toString(); - if(cert.isEmpty()) - return; - - // Open root directory. Get host name from client cert - remoteModel->setNewRootDir(RemoteNetwork::get().getInfoFromClientCert(cert, RemoteNetwork::CertInfoServer), cert); - - // Reset list of local checkouts - remoteLocalFilesModel->setIdentity(cert); - refreshLocalFileList(); - - // Enable buttons if necessary - enableButtons(); -} - -void RemoteDock::fetchDatabase(const QModelIndex& idx) -{ - if(!idx.isValid()) - return; - - // Get item - const RemoteModelItem* item = remoteModel->modelIndexToItem(idx); - - // Only open database file - if(item->value(RemoteModelColumnType).toString() == "database") - fetchDatabase(item->value(RemoteModelColumnUrl).toString()); -} - -void RemoteDock::fetchDatabase(QString url_string, RemoteNetwork::RequestType request_type) -{ - // If no URL was provided ask the user. Default to the current clipboard contents - if(url_string.isEmpty()) - { - url_string = QInputDialog::getText(this, - qApp->applicationName(), - tr("This downloads a database from a remote server for local editing.\n" - "Please enter the URL to clone from. You can generate this URL by\n" - "clicking the 'Clone Database in DB4S' button on the web page\n" - "of the database."), - QLineEdit::Normal, - QApplication::clipboard()->text()); - } - - if(url_string.isEmpty()) - return; - - // Check the URL - QUrl url(url_string); - if(url.authority() != QUrl(RemoteNetwork::get().getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteNetwork::CertInfoServer)).authority()) - { - QMessageBox::warning(this, qApp->applicationName(), tr("Invalid URL: The host name does not match the host name of the current identity.")); - return; - } - if(!QUrlQuery(url).hasQueryItem("branch")) - { - QMessageBox::warning(this, qApp->applicationName(), tr("Invalid URL: No branch name specified.")); - return; - } - if(!QUrlQuery(url).hasQueryItem("commit")) - { - QMessageBox::warning(this, qApp->applicationName(), tr("Invalid URL: No commit ID specified.")); - return; - } - - // For the user name, take the path, remove the database name and the initial slash - QString username = url.path().remove("/" + url.fileName()).mid(1); - - // There is a chance that we've already cloned that database. So check for that first - QString exists = remoteDatabase.localExists(url, remoteModel->currentClientCertificate(), QUrlQuery(url).queryItemValue("branch").toStdString()); - if(!exists.isEmpty() && request_type == RemoteNetwork::RequestTypeDatabase) - { - // Check for modifications - bool modified = isLocalDatabaseModified(exists, username, url.fileName(), remoteModel->currentClientCertificate(), - QUrlQuery(url).queryItemValue("commit").toStdString()); - - // Database has already been cloned! So open the local file instead of fetching the one from the - // server again. If the local file has been modified don't open it but try to download the last known - // commit again. - if(!modified) - { - emit openFile(exists); - return; - } - } - - // Check if we already have a clone of this database branch and, if so, figure out its local file name. - // For this we do not care about the currently checked out commit id because we only have one file per - // database and branch on the disk. - QUrl url_without_commit_id(url); - QUrlQuery url_without_commit_id_query(url_without_commit_id); - url_without_commit_id_query.removeQueryItem("commit"); - url_without_commit_id.setQuery(url_without_commit_id_query); - QString local_file = remoteDatabase.localExists(url_without_commit_id, remoteModel->currentClientCertificate(), QUrlQuery(url).queryItemValue("branch").toStdString()); - if(request_type == RemoteNetwork::RequestTypeDatabase && !local_file.isEmpty()) - { - // If there is a local clone of this dtabase and branch, figure out if the local file has been modified - - // Get the last local commit id - std::string last_commit_id = remoteDatabase.localLastCommitId(remoteModel->currentClientCertificate(), url, QUrlQuery(url).queryItemValue("branch").toStdString()); - - // Check for modifications - bool modified = isLocalDatabaseModified(local_file, username, url.fileName(), remoteModel->currentClientCertificate(), last_commit_id); - - // Only if the local file has been modified show a warning that checking out this commit overrides local changes - if(modified) - { - if(QMessageBox::warning(nullptr, - QApplication::applicationName(), - tr("You have modified the local clone of the database. Fetching this commit overrides these local changes.\n" - "Are you sure you want to proceed?"), - QMessageBox::Yes | QMessageBox::Cancel, - QMessageBox::Cancel) == QMessageBox::Cancel) - { - return; - } - } - } - - // Clone the database - RemoteNetwork::get().fetch(url.toString(), request_type, remoteModel->currentClientCertificate()); -} - -void RemoteDock::fetchCommit(const QModelIndex& idx, RemoteNetwork::RequestType request_type) -{ - // Fetch selected commit - QUrl url(QString::fromStdString(currently_opened_file_info.url)); - QUrlQuery query; - query.addQueryItem("branch", ui->comboDatabaseBranch->currentText()); - query.addQueryItem("commit", idx.sibling(idx.row(), RemoteCommitsModel::ColumnCommitId).data().toString()); - url.setQuery(query); - fetchDatabase(url.toString(), request_type); -} - -void RemoteDock::enableButtons() -{ - bool db_opened = mainWindow->getDb().isOpen() && mainWindow->getDb().currentFile() != ":memory:"; - bool logged_in = !remoteModel->currentClientCertificate().isEmpty(); - - ui->buttonPushDatabase->setEnabled(db_opened && logged_in); - ui->actionRefresh->setEnabled(logged_in); - ui->actionCloneDatabaseLink->setEnabled(logged_in); - ui->actionDatabaseOpenBrowser->setEnabled(db_opened && logged_in); - ui->actionFetchLatestCommit->setEnabled(db_opened && logged_in); -} - -void RemoteDock::pushCurrentlyOpenedDatabase() -{ - // Show a warning when trying to push a database with unsaved changes - if(mainWindow->getDb().getDirty()) - { - if(QMessageBox::warning(this, - QApplication::applicationName(), - tr("The database has unsaved changes. Are you sure you want to push it before saving?"), - QMessageBox::Yes | QMessageBox::Cancel, - QMessageBox::Cancel) == QMessageBox::Cancel) - return; - } - - // Push currently opened file - pushDatabase(mainWindow->getDb().currentFile(), QString::fromStdString(currently_opened_file_info.branch)); -} - -void RemoteDock::pushSelectedLocalDatabase() -{ - // Return if no file is selected - if(!ui->treeLocal->currentIndex().isValid()) - return; - - const int row = ui->treeLocal->currentIndex().row(); - const QString filename = ui->treeLocal->currentIndex().sibling(row, RemoteLocalFilesModel::ColumnFile).data().toString(); - if(filename.isEmpty()) - return; - - // Push selected file - const QString branch = ui->treeLocal->currentIndex().sibling(row, RemoteLocalFilesModel::ColumnBranch).data().toString(); - pushDatabase(Settings::getValue("remote", "clonedirectory").toString() + "/" + filename, branch); -} - -void RemoteDock::pushDatabase(const QString& path, const QString& branch) -{ - // If the currently active identity is the read-only public access to dbhub.io, don't show the Push Database dialog because it won't work anyway. - // Instead switch to an explanation offering some advice to create and import a proper certificate. - if(remoteModel->currentClientCertificate() == ":/user_certs/public.cert.pem") - { - ui->stack->setCurrentIndex(1); - return; - } - - // The default suggestion for a database name is the local file name. If it is a remote file (like when it initially was fetched using DB4S), - // the extra bit of information at the end of the name gets removed first. - QString name = QFileInfo(path).fileName(); - name = name.remove(QRegularExpression("_[0-9]+.remotedb$")); - - // Show the user a dialog for setting all the commit details - QString host = RemoteNetwork::get().getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteNetwork::CertInfoServer); - RemotePushDialog pushDialog(this, host, remoteModel->currentClientCertificate(), name, branch); - if(pushDialog.exec() != QDialog::Accepted) - return; - - // Build push URL - QString url = host; - url.append(pushDialog.user()); - url.append("/"); - url.append(pushDialog.name()); - - // Check if we are pushing a cloned database. Only in this case we provide the last known commit id. - // For this check we use the branch name which was used for cloning this database rather than the - // branch name which was selected in the push dialog. This is because we want to figure out the - // current commit id of we are currently looking at rather than some other commit id or none at all - // when creating a new branch. - QString commit_id; - if(path.startsWith(Settings::getValue("remote", "clonedirectory").toString())) - commit_id = QString::fromStdString(remoteDatabase.localLastCommitId(remoteModel->currentClientCertificate(), url, branch.toStdString())); - - // Push database - RemoteNetwork::get().push(path, url, remoteModel->currentClientCertificate(), pushDialog.name(), - pushDialog.commitMessage(), pushDialog.licence(), pushDialog.isPublic(), pushDialog.branch(), - pushDialog.forcePush(), commit_id); -} - -void RemoteDock::newDirectoryNode(const QModelIndex& parent) -{ - // Was this a new root dir? - if(!parent.isValid()) - { - // Then check if there is a directory with the current user name - - // Get current user name - QString user = RemoteNetwork::get().getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteNetwork::CertInfoUser); - - for(int i=0;irowCount();i++) - { - QModelIndex child = remoteModel->index(i, RemoteModelColumnName); - if(child.data().toString() == user) - { - ui->treeRemote->expand(child); - break; - } - } - } -} - -void RemoteDock::reject() -{ - // We override this, to ensure the Escape key doesn't make this dialog - // dock go away - return; -} - -void RemoteDock::switchToMainView() -{ - ui->stack->setCurrentIndex(0); -} - -void RemoteDock::refreshLocalFileList() -{ - remoteLocalFilesModel->refresh(); - - // Expand node for current user - QString user = RemoteNetwork::get().getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteNetwork::CertInfoUser); - for(int i=0;irowCount();i++) - { - QModelIndex child = remoteLocalFilesModel->index(i, RemoteLocalFilesModel::ColumnName); - if(child.data().toString() == user) - { - ui->treeLocal->expand(child); - break; - } - } -} - -void RemoteDock::openLocalFile(const QModelIndex& idx) -{ - if(!idx.isValid()) - return; - - QString file = idx.sibling(idx.row(), RemoteLocalFilesModel::ColumnFile).data().toString(); - if(!file.isEmpty()) - emit openFile(Settings::getValue("remote", "clonedirectory").toString() + "/" + file); -} - -void RemoteDock::fileOpened(const QString& filename) -{ - // Clear data first - currently_opened_file_info.clear(); - remoteCommitsModel->clear(); - ui->comboDatabaseBranch->clear(); - ui->editDatabaseUser->clear(); - ui->editDatabaseFile->clear(); - ui->editDatabaseBranch->clear(); - - // Do nothing if the file name is empty (indicating a closed database) or this is an in-memory database - if(filename.isEmpty() || filename == ":memory:") - return; - - // Check if it is a tracked remote database file and retrieve the information we have on it - if(filename.startsWith(Settings::getValue("remote", "clonedirectory").toString())) - currently_opened_file_info = remoteDatabase.localGetLocalFileInfo(filename); - - // Is this actually a clone of a remote database? - if(!currently_opened_file_info.file.empty()) - { - // Copy information to view - ui->editDatabaseUser->setText(currently_opened_file_info.user_name()); - ui->editDatabaseFile->setText(QString::fromStdString(currently_opened_file_info.name)); - ui->editDatabaseBranch->setText(QString::fromStdString(currently_opened_file_info.branch)); - - // Make sure the current identity matches the identity used to clone this file in the first place. - // A mismatch is possible when the local database file has been opened using a recent files menu item or some similar technique. - if(QString::fromStdString(currently_opened_file_info.identity) != QFileInfo(remoteModel->currentClientCertificate()).fileName()) - ui->comboUser->setCurrentIndex(ui->comboUser->findData("/" + QString::fromStdString(currently_opened_file_info.identity), Qt::UserRole, Qt::MatchEndsWith)); - - // Query more information on database from server - refreshMetadata(currently_opened_file_info.user_name(), QString::fromStdString(currently_opened_file_info.name)); - - // Switch to "Current Database" tab - ui->tabs->setCurrentIndex(2); - } -} - -void RemoteDock::refreshMetadata(const QString& username, const QString& dbname) -{ - // Make request for meta data - QUrl url(RemoteNetwork::get().getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteNetwork::CertInfoServer) + "/metadata/get"); - QUrlQuery query; - query.addQueryItem("username", username); - query.addQueryItem("folder", "/"); - query.addQueryItem("dbname", dbname); - url.setQuery(query); - RemoteNetwork::get().fetch(url.toString(), RemoteNetwork::RequestTypeCustom, remoteModel->currentClientCertificate(), [this](const QByteArray& reply) { - // Read and check results - json obj = json::parse(reply, nullptr, false); - if(obj.is_discarded() || !obj.is_object()) - return; - - // Store all the commit information as-is - json obj_commits = obj["commits"]; - current_commit_json = obj_commits.dump(); - - // Store the link to the web page in the action for opening that link in a browser - ui->actionDatabaseOpenBrowser->setData(QString::fromStdString(obj["web_page"])); - - // Fill branches combo box - json obj_branches = obj["branches"]; - ui->comboDatabaseBranch->clear(); - for(auto it=obj_branches.cbegin();it!=obj_branches.cend();++it) - ui->comboDatabaseBranch->addItem(QString::fromStdString(it.key()), QString::fromStdString(it.value()["commit"])); - ui->comboDatabaseBranch->setCurrentIndex(ui->comboDatabaseBranch->findText(ui->editDatabaseBranch->text())); - }); -} - -void RemoteDock::deleteLocalDatabase(const QModelIndex& index) -{ - if(!index.isValid()) - return; - - QString filename = index.sibling(index.row(), RemoteLocalFilesModel::ColumnFile).data().toString(); - QString path = Settings::getValue("remote", "clonedirectory").toString() + "/" + filename; - - // Warn when trying to delete a currently opened database file - if(mainWindow->getDb().currentFile() == path) - { - QMessageBox::warning(this, QApplication::applicationName(), tr("The database you are trying to delete is currently opened. " - "Please close it before deleting.")); - return; - } - - // Let user confirm deleting the database - if(QMessageBox::warning(this, QApplication::applicationName(), tr("This deletes the local version of this database with all the " - "changes you have not committed yet. Are you sure you want to " - "delete this database?"), - QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel) == QMessageBox::Cancel) - { - return; - } - - // Delete the file - remoteLocalFilesModel->removeRow(index.row(), index.parent()); -} - -void RemoteDock::openCurrentDatabaseInBrowser() const -{ - QDesktopServices::openUrl(ui->actionDatabaseOpenBrowser->data().toUrl()); -} - -void RemoteDock::refresh() -{ - // Refresh Remote tab - remoteModel->refresh(); - - // Refresh Local tab - refreshLocalFileList(); - - // Refresh Current Database tab - if(!currently_opened_file_info.file.empty()) - refreshMetadata(currently_opened_file_info.user_name(), QString::fromStdString(currently_opened_file_info.name)); -} - -void RemoteDock::pushFinished(const QString& filename, const QString& identity, const QUrl& url, const std::string& new_commit_id, - const std::string& branch, const QString& source_file) -{ - // Create or update the record in our local checkout database - QString saveFileAs = remoteDatabase.localAdd(filename, identity, url, new_commit_id, branch); - - // If the name of the source file and the name we're saving as differ, we're doing an initial push. In this case, copy the source file to - // the destination path to avoid redownloading it when it's first used. - if(saveFileAs != source_file) - QFile::copy(source_file, saveFileAs); - - // Update info on currently opened file - if(currently_opened_file_info.file == QFileInfo(saveFileAs).fileName().toStdString()) - currently_opened_file_info = remoteDatabase.localGetLocalFileInfo(saveFileAs); - - // Refresh view - refresh(); -} - -void RemoteDock::fetchFinished(const QString& filename, const QString& identity, const QUrl& url, const std::string& new_commit_id, - const std::string& branch, const QDateTime& last_modified, QIODevice* device) -{ - // Add cloned database to list of local databases - QString saveFileAs = remoteDatabase.localAdd(filename, identity, url, new_commit_id, branch); - - // Save the downloaded data under the generated file name - QFile file(saveFileAs); - file.open(QIODevice::WriteOnly); - file.write(device->readAll()); - - // Set last modified data of the new file to the one provided by the server - // Before version 5.10, Qt didn't offer any option to set this attribute, so we're not setting it at the moment -#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) - file.setFileTime(last_modified, QFileDevice::FileModificationTime); -#endif - - // Close file - file.close(); - - // Update info on currently opened file - currently_opened_file_info = remoteDatabase.localGetLocalFileInfo(saveFileAs); - - // Refresh data - refreshLocalFileList(); - - // Tell the application to open this file - emit openFile(saveFileAs); -} - -bool RemoteDock::isLocalDatabaseModified(const QString& local_file, const QString& username, const QString& dbname, const QString& identity, const std::string& commit_id) -{ - // Fetch metadata on database - QUrl url(RemoteNetwork::get().getInfoFromClientCert(identity, RemoteNetwork::CertInfoServer) + "/metadata/get"); - QUrlQuery query; - query.addQueryItem("username", username); - query.addQueryItem("folder", "/"); - query.addQueryItem("dbname", dbname); - url.setQuery(query); - - bool modified = true; // By default we assume the database has been modified - RemoteNetwork::get().fetch(url, RemoteNetwork::RequestTypeCustom, identity, [commit_id, dbname, local_file, &modified](const QByteArray& reply) { - // Read and check results - json obj = json::parse(reply, nullptr, false); - if(obj.is_discarded() || !obj.is_object()) - return; - - // Search tree entry for currently checked out commit - json tree = obj["commits"][commit_id]["tree"]["entries"]; - for(const auto& it : tree) - { - if(it["entry_type"] == "db" && it["name"] == dbname.toStdString()) - { - // When we have found it, check if the current file matches the remote file by first checking if the - // file size is equal and then comparing their SHA256 hashes - if(QFileInfo(local_file).size() == it["size"]) - { - QFile file(local_file); - if(!file.open(QFile::ReadOnly)) - return; - - QCryptographicHash hash(QCryptographicHash::Sha256); - hash.addData(&file); - if(hash.result().toHex().toStdString() == it["sha256"]) - { - modified = false; - return; - } - } - - break; - } - } - }, true); - - return modified; -} diff --git a/src/RemoteDock.h b/src/RemoteDock.h deleted file mode 100644 index b064448c8d..0000000000 --- a/src/RemoteDock.h +++ /dev/null @@ -1,80 +0,0 @@ -#ifndef REMOTEDOCK_H -#define REMOTEDOCK_H - -#include - -#include "RemoteDatabase.h" -#include "RemoteNetwork.h" - -class RemoteCommitsModel; -class RemoteLocalFilesModel; -class RemoteModel; -class MainWindow; - -namespace Ui { -class RemoteDock; -} - -class RemoteDock : public QDialog -{ - Q_OBJECT - -public: - explicit RemoteDock(MainWindow* parent); - ~RemoteDock() override; - - void reloadSettings(); - void enableButtons(); - - // This function should be called whenever a database file is opened. - // It checks whether the file is a checkout of a tracked remote database - // and updates some of the fields in the remote dock if it is. - // Call it with an empty file name if the database is closed. - void fileOpened(const QString& filename); - -public slots: - void reject() override; - -private slots: - void setNewIdentity(const QString& identity); - void fetchDatabase(const QModelIndex& idx); - void fetchDatabase(QString url = QString(), RemoteNetwork::RequestType request_type = RemoteNetwork::RequestTypeDatabase); - void fetchCommit(const QModelIndex& idx, RemoteNetwork::RequestType request_type = RemoteNetwork::RequestTypeDatabase); - void pushCurrentlyOpenedDatabase(); - void pushSelectedLocalDatabase(); - void newDirectoryNode(const QModelIndex& parent); - void switchToMainView(); - void openLocalFile(const QModelIndex& idx); - void deleteLocalDatabase(const QModelIndex& index); - void openCurrentDatabaseInBrowser() const; - void refresh(); - void pushFinished(const QString& filename, const QString& identity, const QUrl& url, const std::string& new_commit_id, - const std::string& branch, const QString& source_file); - void fetchFinished(const QString& filename, const QString& identity, const QUrl& url, const std::string& new_commit_id, - const std::string& branch, const QDateTime& last_modified, QIODevice* device); - -signals: - void openFile(QString file); - -private: - Ui::RemoteDock* ui; - - MainWindow* mainWindow; - - RemoteDatabase remoteDatabase; - RemoteModel* remoteModel; - RemoteLocalFilesModel* remoteLocalFilesModel; - RemoteCommitsModel* remoteCommitsModel; - - std::string current_commit_json; - RemoteDatabase::LocalFileInfo currently_opened_file_info; - - void refreshLocalFileList(); - void refreshMetadata(const QString& username, const QString& dbname); - - bool isLocalDatabaseModified(const QString& local_file, const QString& username, const QString& dbname, const QString& identity, const std::string& commit_id); - - void pushDatabase(const QString& path, const QString& branch); -}; - -#endif diff --git a/src/RemoteDock.ui b/src/RemoteDock.ui deleted file mode 100644 index fedf983126..0000000000 --- a/src/RemoteDock.ui +++ /dev/null @@ -1,754 +0,0 @@ - - - RemoteDock - - - - 0 - 0 - 534 - 387 - - - - Remote - - - - 2 - - - 0 - - - 2 - - - 0 - - - - - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - Identity - - - comboUser - - - - - - - QComboBox::AdjustToContents - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Push currently opened database to server - - - Upload - - - - :/icons/push_database:/icons/push_database - - - Qt::ToolButtonTextBesideIcon - - - - - - - - - 0 - - - - DBHub.io - - - - 2 - - - 0 - - - 2 - - - 0 - - - - - false - - - false - - - - - - - - - Qt::ActionsContextMenu - - - <html><head/><body><p>In this pane, remote databases from dbhub.io website can be added to DB Browser for SQLite. First you need an identity:</p><ol style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Login to the dbhub.io website (use your GitHub credentials or whatever you want)</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Click the button to &quot;Generate client certificate&quot; (that's your identity). That'll give you a certificate file (save it to your local disk).</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Go to the Remote tab in DB Browser for SQLite Preferences. Click the button to add a new certificate to DB Browser for SQLite and choose the just downloaded certificate file.</li></ol><p>Now the Remote panel shows your identity and you can add remote databases.</p></body></html> - - - - - - - - Local - - - - 2 - - - 0 - - - 2 - - - 0 - - - - - Qt::ActionsContextMenu - - - - - - - - Current Database - - - - 2 - - - 0 - - - 2 - - - 0 - - - - - true - - - - - 0 - 0 - 520 - 319 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - false - - - false - - - - - - - - - - Clone - - - - 0 - - - - - &User - - - editDatabaseUser - - - - - - - true - - - - - - - &Database - - - editDatabaseFile - - - - - - - true - - - - - - - Branch - - - editDatabaseBranch - - - - - - - true - - - - - - - - - - Commits - - - - 2 - - - 0 - - - 2 - - - 0 - - - - - - - Commits for - - - - - - - - - - - - Qt::ActionsContextMenu - - - - - - - - - - - - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Vertical - - - - 20 - 85 - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - <html><head/><body><p>You are currently using a built-in, read-only identity. For uploading your database, you need to configure and use your DBHub.io account.</p><p>No DBHub.io account yet? <a href="https://dbhub.io/"><span style=" text-decoration: underline; color:#007af4;">Create one now</span></a> and import your certificate <a href="#preferences"><span style=" text-decoration: underline; color:#007af4;">here</span></a> to share your databases.</p><p>For online help visit <a href="https://dbhub.io/about"><span style=" text-decoration: underline; color:#007af4;">here</span></a>.</p></body></html> - - - true - - - - - - - Back - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Qt::Vertical - - - - 20 - 85 - - - - - - - - - - - - - :/icons/close:/icons/close - - - Delete Database - - - Delete the local clone of this database - - - - - - :/icons/browser_open:/icons/browser_open - - - Open in Web Browser - - - Open the web page for the current database in your browser - - - - - - :/icons/clone_database:/icons/clone_database - - - Clone from Link - - - Use this to download a remote database for local editing using a URL as provided on the web page of the database. - - - - - - :/icons/refresh:/icons/refresh - - - Refresh - - - Reload all data and update the views - - - - - - :/icons/clone_database:/icons/clone_database - - - Clone Database - - - - 75 - true - - - - - - - :/icons/db_open:/icons/db_open - - - Open Database - - - Open the local copy of this database - - - - 75 - true - - - - - - Check out Commit - - - Download and open this specific commit - - - - 75 - true - - - - - - - :/icons/db_revert:/icons/db_revert - - - Check out Latest Commit - - - Check out the latest commit of the current branch - - - - - Save Revision to File - - - Saves the selected revision of the database to another file - - - - - - :/icons/push_database:/icons/push_database - - - Upload Database - - - Upload this database as a new commit - - - - - comboUser - buttonPushDatabase - tabs - treeRemote - treeLocal - comboDatabaseBranch - treeDatabaseCommits - buttonNoCertBack - - - - - - - comboUser - currentTextChanged(QString) - RemoteDock - setNewIdentity(QString) - - - 134 - 25 - - - 419 - 24 - - - - - treeRemote - doubleClicked(QModelIndex) - RemoteDock - fetchDatabase(QModelIndex) - - - 215 - 148 - - - 204 - 37 - - - - - buttonPushDatabase - clicked() - RemoteDock - pushCurrentlyOpenedDatabase() - - - 530 - 25 - - - 287 - 154 - - - - - buttonNoCertBack - clicked() - RemoteDock - switchToMainView() - - - 93 - 23 - - - 266 - 148 - - - - - treeLocal - doubleClicked(QModelIndex) - RemoteDock - openLocalFile(QModelIndex) - - - 266 - 179 - - - 266 - 148 - - - - - actionCloneDatabaseLink - triggered() - RemoteDock - fetchDatabase() - - - -1 - -1 - - - 266 - 178 - - - - - actionDatabaseOpenBrowser - triggered() - RemoteDock - openCurrentDatabaseInBrowser() - - - -1 - -1 - - - 266 - 189 - - - - - actionRefresh - triggered() - RemoteDock - refresh() - - - -1 - -1 - - - 266 - 193 - - - - - treeDatabaseCommits - doubleClicked(QModelIndex) - RemoteDock - fetchCommit(QModelIndex) - - - 266 - 337 - - - 266 - 193 - - - - - actionPushLocalDatabase - triggered() - RemoteDock - pushSelectedLocalDatabase() - - - -1 - -1 - - - 266 - 193 - - - - - - setNewIdentity(QString) - fetchDatabase(QModelIndex) - pushCurrentlyOpenedDatabase() - pushSelectedLocalDatabase() - switchToMainView() - openLocalFile(QModelIndex) - fetchDatabase() - openCurrentDatabaseInBrowser() - refresh() - fetchCommit(QModelIndex) - - diff --git a/src/RemoteLocalFilesModel.cpp b/src/RemoteLocalFilesModel.cpp deleted file mode 100644 index 0c7a34208f..0000000000 --- a/src/RemoteLocalFilesModel.cpp +++ /dev/null @@ -1,191 +0,0 @@ -#include -#include -#include -#include -#include - -#include "Data.h" -#include "RemoteDatabase.h" -#include "RemoteLocalFilesModel.h" -#include "RemoteNetwork.h" -#include "Settings.h" - -using json = nlohmann::json; - -RemoteLocalFilesModel::RemoteLocalFilesModel(QObject* parent, RemoteDatabase& remote) : - QAbstractItemModel(parent), - remoteDatabase(remote) -{ - QStringList header; - header << tr("Name") << tr("Branch") << tr("Last modified") << tr("Size") << tr("Commit") << tr("File"); - rootItem = new QTreeWidgetItem(header); -} - -RemoteLocalFilesModel::~RemoteLocalFilesModel() -{ - delete rootItem; -} - -void RemoteLocalFilesModel::setIdentity(const QString& cert_filename) -{ - current_cert_filename = cert_filename; - current_user_name = RemoteNetwork::get().getInfoFromClientCert(cert_filename, RemoteNetwork::CertInfoUser); - refresh(); -} - -void RemoteLocalFilesModel::refresh() -{ - beginResetModel(); - - // Remove all data except for the root item - while(rootItem->childCount()) - delete rootItem->child(0); - - // Get list of locally checked out databases - auto files = remoteDatabase.localGetLocalFiles(current_cert_filename); - - // Loop through that list - for(const auto& file : files) - { - QString user_name = file.user_name(); - - // Check if there is already a node for this user - QTreeWidgetItem* user_node = nullptr; - for(int i=0;ichildCount();i++) - { - if(rootItem->child(i)->text(ColumnName) == user_name) - { - user_node = rootItem->child(i); - break; - } - } - - // If there is no node for this user yet create one - if(user_node == nullptr) - { - user_node = new QTreeWidgetItem(rootItem); - user_node->setText(ColumnName, user_name); - user_node->setIcon(ColumnName, QIcon(user_name == current_user_name ? ":/icons/folder_user" : ":/icons/folder")); - } - - // Get file information - QFile file_info(Settings::getValue("remote", "clonedirectory").toString() + "/" + QString::fromStdString(file.file)); - - // Add file to user node - QTreeWidgetItem* file_node = new QTreeWidgetItem(user_node); - file_node->setText(ColumnName, QString::fromStdString(file.name)); - file_node->setIcon(ColumnName, QIcon(":/icons/database")); - file_node->setText(ColumnBranch, QString::fromStdString(file.branch)); - file_node->setText(ColumnLastModified, QLocale::system().toString(QFileInfo(file_info).lastModified().toLocalTime(), QLocale::ShortFormat)); - file_node->setText(ColumnSize, humanReadableSize(static_cast(file_info.size()))); - file_node->setText(ColumnCommit, QString::fromStdString(file.commit_id)); - file_node->setText(ColumnFile, QString::fromStdString(file.file)); - } - - // Refresh the view - endResetModel(); -} - -QModelIndex RemoteLocalFilesModel::index(int row, int column, const QModelIndex& parent) const -{ - if(!hasIndex(row, column, parent)) - return QModelIndex(); - - QTreeWidgetItem *parentItem; - if(!parent.isValid()) - parentItem = rootItem; - else - parentItem = static_cast(parent.internalPointer()); - - QTreeWidgetItem* childItem = parentItem->child(row); - if(childItem) - return createIndex(row, column, childItem); - else - return QModelIndex(); -} - -QModelIndex RemoteLocalFilesModel::parent(const QModelIndex& index) const -{ - if(!index.isValid()) - return QModelIndex(); - - QTreeWidgetItem* childItem = static_cast(index.internalPointer()); - QTreeWidgetItem* parentItem = childItem->parent(); - - if(parentItem == rootItem) - return QModelIndex(); - else - return createIndex(0, 0, parentItem); -} - -QVariant RemoteLocalFilesModel::data(const QModelIndex& index, int role) const -{ - if(!index.isValid()) - return QVariant(); - - // Get the item the index points at - QTreeWidgetItem* item = static_cast(index.internalPointer()); - - // Return data depending on the role - switch(role) - { - case Qt::DisplayRole: - case Qt::EditRole: - return item->text(index.column()); - case Qt::DecorationRole: - return item->icon(index.column()); - default: - return QVariant(); - } -} - -QVariant RemoteLocalFilesModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - // Get the header string from the root item - if(orientation == Qt::Horizontal && role == Qt::DisplayRole) - return rootItem->data(section, role); - - return QVariant(); -} - -int RemoteLocalFilesModel::rowCount(const QModelIndex& parent) const -{ - if(parent.column() > 0) - return 0; - - if(!parent.isValid()) - return rootItem->childCount(); - else - return static_cast(parent.internalPointer())->childCount(); -} - -int RemoteLocalFilesModel::columnCount(const QModelIndex& /*parent*/) const -{ - return rootItem->columnCount(); -} - -bool RemoteLocalFilesModel::removeRows(int row, int count, const QModelIndex& parent) -{ - for(int i=0;i=0;i--) - { - auto item = static_cast(index(row + i, 0, parent).internalPointer()); - item->parent()->removeChild(item); - } - endRemoveRows(); - - // If parent node is empty, remove that one too. Make sure to not remove the root node - if(parent.isValid() && !index(0, 0, parent).isValid()) - { - beginRemoveRows(parent.parent(), 0, 0); - auto item = static_cast(parent.internalPointer()); - item->parent()->removeChild(item); - endRemoveRows(); - } - - return true; -} diff --git a/src/RemoteLocalFilesModel.h b/src/RemoteLocalFilesModel.h deleted file mode 100644 index ee6e4786ae..0000000000 --- a/src/RemoteLocalFilesModel.h +++ /dev/null @@ -1,56 +0,0 @@ -#ifndef REMOTELOCALFILESMODEL_H -#define REMOTELOCALFILESMODEL_H - -#include - -#include - -class RemoteDatabase; - -class QTreeWidgetItem; - -class RemoteLocalFilesModel : public QAbstractItemModel -{ - Q_OBJECT - -public: - explicit RemoteLocalFilesModel(QObject* parent, RemoteDatabase& remote); - ~RemoteLocalFilesModel() override; - - void setIdentity(const QString& cert_filename); - void refresh(); - - QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; - QModelIndex parent(const QModelIndex& index) const override; - - QVariant data(const QModelIndex& index, int role) const override; - QVariant headerData(int section, Qt::Orientation orientation, int role) const override; - - int rowCount(const QModelIndex& parent = QModelIndex()) const override; - int columnCount(const QModelIndex& parent = QModelIndex()) const override; - - bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override; - - enum Columns - { - ColumnName, - ColumnBranch, - ColumnLastModified, - ColumnSize, - ColumnCommit, - ColumnFile, - }; - -private: - // Pointer to the root item. This contains all the actual item data. - QTreeWidgetItem* rootItem; - - // Reference to the remote database object which is stored somewhere in the main window. - RemoteDatabase& remoteDatabase; - - // This stores the currently used network identity for further requests - QString current_cert_filename; - QString current_user_name; -}; - -#endif diff --git a/src/RemoteModel.cpp b/src/RemoteModel.cpp deleted file mode 100644 index 19597761c4..0000000000 --- a/src/RemoteModel.cpp +++ /dev/null @@ -1,344 +0,0 @@ -#include - -#include "Data.h" -#include "RemoteModel.h" -#include "RemoteNetwork.h" - -using json = nlohmann::json; - -RemoteModelItem::RemoteModelItem(RemoteModelItem* parent) : - m_parent(parent), - m_fetchedDirectoryList(false) -{ -} - -RemoteModelItem::~RemoteModelItem() -{ - qDeleteAll(m_children); -} - -QVariant RemoteModelItem::value(RemoteModelColumns column) const -{ - return m_values[column]; -} - -void RemoteModelItem::setValue(RemoteModelColumns column, QVariant value) -{ - m_values[column] = value; -} - -void RemoteModelItem::appendChild(RemoteModelItem *item) -{ - m_children.push_back(item); -} - -RemoteModelItem* RemoteModelItem::child(int row) const -{ - return m_children[static_cast(row)]; -} - -RemoteModelItem* RemoteModelItem::parent() const -{ - return m_parent; -} - -int RemoteModelItem::childCount() const -{ - return static_cast(m_children.size()); -} - -int RemoteModelItem::row() const -{ - if(m_parent) - { - auto f = std::find( m_parent->m_children.begin(), m_parent->m_children.end(), const_cast(this)); - if(f == m_parent->m_children.end()) - return -1; - else - return static_cast(std::distance(m_parent->m_children.begin(), f)); - } - - return 0; -} - -bool RemoteModelItem::fetchedDirectoryList() const -{ - return m_fetchedDirectoryList; -} - -void RemoteModelItem::setFetchedDirectoryList(bool fetched) -{ - m_fetchedDirectoryList = fetched; -} - -std::vector RemoteModelItem::loadArray(const json& array, RemoteModelItem* parent) -{ - std::vector items; - - // Loop through all directory items - for(const auto& elem : array) - { - // Create a new model item with the specified parent - RemoteModelItem* item = new RemoteModelItem(parent); - - // Save all relevant values. Some of the values are only available for databases. - item->setValue(RemoteModelColumnName, QString::fromStdString(elem["name"])); - item->setValue(RemoteModelColumnType, QString::fromStdString(elem["type"])); - item->setValue(RemoteModelColumnUrl, QString::fromStdString(elem["url"])); - item->setValue(RemoteModelColumnLastModified, isoDateTimeStringToLocalDateTimeString(QString::fromStdString(elem["last_modified"]))); - if(item->value(RemoteModelColumnType).toString() == "database") - { - item->setValue(RemoteModelColumnCommitId, QString::fromStdString(elem["commit_id"])); - item->setValue(RemoteModelColumnSize, QString::number(static_cast(elem["size"]))); - item->setValue(RemoteModelColumnDefaultBranch, QString::fromStdString(elem["default_branch"])); - item->setValue(RemoteModelColumnLicence, QString::fromStdString(elem["licence"])); - item->setValue(RemoteModelColumnOneLineDescription, QString::fromStdString(elem["one_line_description"])); - item->setValue(RemoteModelColumnPublic, static_cast(elem["public"])); - item->setValue(RemoteModelColumnSha256, QString::fromStdString(elem["sha256"])); - item->setValue(RemoteModelColumnRepoModified, isoDateTimeStringToLocalDateTimeString(QString::fromStdString(elem["repo_modified"]))); - } - - items.push_back(item); - } - - return items; -} - -RemoteModel::RemoteModel(QObject* parent) : - QAbstractItemModel(parent), - headerList({tr("Name"), tr("Last modified"), tr("Size"), tr("Commit")}), - rootItem(new RemoteModelItem()) -{ -} - -RemoteModel::~RemoteModel() -{ - delete rootItem; -} - -void RemoteModel::setNewRootDir(const QString& url, const QString& cert) -{ - // Get user name from client cert - currentUserName = RemoteNetwork::get().getInfoFromClientCert(cert, RemoteNetwork::CertInfoUser); - - // Save settings - currentRootDirectory = url; - currentClientCert = cert; - - // Fetch root directory - refresh(); -} - -void RemoteModel::refresh() -{ - // Fetch root directory and put the reply data under the root item - RemoteNetwork::get().fetch(currentRootDirectory, RemoteNetwork::RequestTypeCustom, currentClientCert, [this](const QByteArray& reply) { - parseDirectoryListing(reply, QModelIndex()); - }); -} - -void RemoteModel::parseDirectoryListing(const QString& text, QModelIndex parent) -{ - // Load new JSON root document assuming it's an array - json array = json::parse(text.toStdString(), nullptr, false); - if(array.is_discarded() || !array.is_array()) - return; - - // Get model index to store the new data under - RemoteModelItem* parentItem = const_cast(modelIndexToItem(parent)); - - // An invalid model index indicates that this is a new root item. This means the old one needs to be entirely deleted first. - if(!parent.isValid()) - { - // Clear root item - beginResetModel(); - delete rootItem; - rootItem = new RemoteModelItem(); - endResetModel(); - - // Set parent model index and parent item to the new values - parent = QModelIndex(); - parentItem = rootItem; - } - - // Insert data - std::vector items = RemoteModelItem::loadArray(array, parentItem); - beginInsertRows(parent, 0, static_cast(items.size() - 1)); - for(RemoteModelItem* item : items) - parentItem->appendChild(item); - endInsertRows(); - - // Emit directory listing parsed signal - emit directoryListingParsed(parent); -} - -QModelIndex RemoteModel::index(int row, int column, const QModelIndex& parent) const -{ - if(!hasIndex(row, column, parent)) - return QModelIndex(); - - const RemoteModelItem* parentItem = modelIndexToItem(parent); - RemoteModelItem* childItem = parentItem->child(row); - - if(childItem) - return createIndex(row, column, childItem); - else - return QModelIndex(); -} - -QModelIndex RemoteModel::parent(const QModelIndex& index) const -{ - if(!index.isValid()) - return QModelIndex(); - - const RemoteModelItem* childItem = modelIndexToItem(index); - RemoteModelItem* parentItem = childItem->parent(); - - if(parentItem == rootItem) - return QModelIndex(); - - return createIndex(parentItem->row(), 0, parentItem); -} - -QVariant RemoteModel::data(const QModelIndex& index, int role) const -{ - // Don't return data for invalid indices - if(!index.isValid()) - return QVariant(); - - // Type of item - const RemoteModelItem* item = modelIndexToItem(index); - QString type = item->value(RemoteModelColumnType).toString(); - - // Decoration role? Only for first column! - if(role == Qt::DecorationRole && index.column() == 0) - { - // Use different icons depending on item type - if(type == "folder" && index.parent() == QModelIndex() && item->value(RemoteModelColumnName) == currentUserName) - return QImage(":/icons/folder_user"); - else if(type == "folder") - return QImage(":/icons/folder"); - else if(type == "database") - return QImage(":/icons/database"); - } else if(role == Qt::ToolTipRole) { - if(type == "database") - { - // Use URL to generate user name and database name. This avoids using the name of the parent item which - // might not contain the user name when the server sends a different directory structure. - QString result = "" + item->value(RemoteModelColumnUrl).toUrl().path().mid(1) + ""; - if(!item->value(RemoteModelColumnOneLineDescription).toString().isEmpty()) - result += "
" + item->value(RemoteModelColumnOneLineDescription).toString(); - result += "
" + tr("Size: ") + humanReadableSize(item->value(RemoteModelColumnSize).toULongLong()); - result += "
" + tr("Last Modified: ") + item->value(RemoteModelColumnLastModified).toString(); - result += "
" + tr("Licence: ") + item->value(RemoteModelColumnLicence).toString(); - result += "
" + tr("Default Branch: ") + item->value(RemoteModelColumnDefaultBranch).toString(); - return result; - } - } else if(role == Qt::DisplayRole) { - // Display role? - - // Return different value depending on column - switch(index.column()) - { - case 0: - { - return item->value(RemoteModelColumnName); - } - case 1: - { - return item->value(RemoteModelColumnLastModified); - } - case 2: - { - // Folders don't have a size - if(type == "folder") - return QVariant(); - - // Convert size to human readable format - unsigned int size = item->value(RemoteModelColumnSize).toUInt(); - return humanReadableSize(size); - } - case 3: - { - if(type == "folder") - return QVariant(); - return item->value(RemoteModelColumnCommitId); - } - } - } - - return QVariant(); -} - -QVariant RemoteModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - // Call default implementation for vertical headers and for non-display roles - if(role != Qt::DisplayRole || orientation != Qt::Horizontal) - return QAbstractItemModel::headerData(section, orientation, role); - - // Return header string depending on column - return headerList.at(static_cast(section)); -} - -int RemoteModel::rowCount(const QModelIndex& parent) const -{ - if(parent.column() > 0) - return 0; - - const RemoteModelItem* parentItem = modelIndexToItem(parent); - return parentItem->childCount(); -} - -int RemoteModel::columnCount(const QModelIndex& /*parent*/) const -{ - return static_cast(headerList.size()); -} - -bool RemoteModel::hasChildren(const QModelIndex& parent) const -{ - if(!parent.isValid()) - return true; - - // If the item actually has children or is of type "folder" (and may have no children yet), we say that it actually has children - const RemoteModelItem* item = modelIndexToItem(parent); - return item->childCount() || item->value(RemoteModelColumnType) == "folder"; -} - -bool RemoteModel::canFetchMore(const QModelIndex& parent) const -{ - if(!parent.isValid()) - return false; - - // If the item is of type "folder" and we haven't tried fetching a directory listing yet, we indicate that there might be more data to load - const RemoteModelItem* item = modelIndexToItem(parent); - return item->value(RemoteModelColumnType) == "folder" && !item->fetchedDirectoryList(); -} - -void RemoteModel::fetchMore(const QModelIndex& parent) -{ - // Can we even fetch more data? - if(!canFetchMore(parent)) - return; - - // Get parent item - RemoteModelItem* item = static_cast(parent.internalPointer()); - - // Fetch item URL - item->setFetchedDirectoryList(true); - RemoteNetwork::get().fetch(item->value(RemoteModelColumnUrl).toUrl(), RemoteNetwork::RequestTypeCustom, currentClientCert, [this, parent](const QByteArray& reply) { - parseDirectoryListing(reply, parent); - }); -} - -const QString& RemoteModel::currentClientCertificate() const -{ - return currentClientCert; -} - -const RemoteModelItem* RemoteModel::modelIndexToItem(const QModelIndex& idx) const -{ - if(!idx.isValid()) - return rootItem; - else - return static_cast(idx.internalPointer()); -} diff --git a/src/RemoteModel.h b/src/RemoteModel.h deleted file mode 100644 index dacabeb80f..0000000000 --- a/src/RemoteModel.h +++ /dev/null @@ -1,118 +0,0 @@ -#ifndef REMOTEMODEL_H -#define REMOTEMODEL_H - -#include -#include - -#include - -// List of fields stored in the JSON data -enum RemoteModelColumns -{ - RemoteModelColumnName, - RemoteModelColumnType, - RemoteModelColumnUrl, - RemoteModelColumnCommitId, - RemoteModelColumnSize, - RemoteModelColumnLastModified, - RemoteModelColumnDefaultBranch, - RemoteModelColumnLicence, - RemoteModelColumnOneLineDescription, - RemoteModelColumnPublic, - RemoteModelColumnRepoModified, - RemoteModelColumnSha256, - - RemoteModelColumnCount -}; - -class RemoteModelItem -{ -public: - explicit RemoteModelItem(RemoteModelItem* parent = nullptr); - ~RemoteModelItem(); - - QVariant value(RemoteModelColumns column) const; - void setValue(RemoteModelColumns column, QVariant value); - - bool fetchedDirectoryList() const; - void setFetchedDirectoryList(bool fetched); - - void appendChild(RemoteModelItem* item); - RemoteModelItem* child(int row) const; - RemoteModelItem* parent() const; - int childCount() const; - int row() const; - - // This function assumes the JSON value it's getting passed is an array ("[{...}, {...}, {...}, ...]"). It returns a list of model items, one - // per array entry and each with the specified parent set. - static std::vector loadArray(const nlohmann::json& array, RemoteModelItem* parent = nullptr); - -private: - // These are just the fields from the json objects returned by the dbhub.io server - QVariant m_values[RemoteModelColumnCount]; - - // Child items and parent item - std::vector m_children; - RemoteModelItem* m_parent; - - // Indicates whether we already tried fetching a directory listing for this item. This serves two purposes: - // 1) When having an empty directory this allows us to remove the expandable flag for this item. - // 2) Between sending a network request and getting the reply this flag is already set, avoiding a second or third request being sent in the meantime. - bool m_fetchedDirectoryList; -}; - -class RemoteModel : public QAbstractItemModel -{ - Q_OBJECT - -public: - explicit RemoteModel(QObject* parent); - ~RemoteModel() override; - - void setNewRootDir(const QString& url, const QString& cert); - void refresh(); - - QModelIndex index(int row, int column,const QModelIndex& parent = QModelIndex()) const override; - QModelIndex parent(const QModelIndex& index) const override; - - QVariant data(const QModelIndex& index, int role) const override; - QVariant headerData(int section, Qt::Orientation orientation, int role) const override; - - int rowCount(const QModelIndex& parent = QModelIndex()) const override; - int columnCount(const QModelIndex& parent = QModelIndex()) const override; - bool hasChildren(const QModelIndex& parent) const override; - - bool canFetchMore(const QModelIndex& parent) const override; - void fetchMore(const QModelIndex& parent) override; - - // This helper function takes a model index and returns the according model item. An invalid model index is used to indicate the - // root item, so if the index is invalid the root item is returned. This means that if you need to check for actual invalid indices - // this needs to be done prior to calling this function. - const RemoteModelItem* modelIndexToItem(const QModelIndex& idx) const; - - // Returns the current client certificate - const QString& currentClientCertificate() const; - -signals: - // This signal is emitted whenever a directory listing has been received and parsed - void directoryListingParsed(QModelIndex parent); - -private slots: - // This is called whenever a network reply containing a directory listing arrives - void parseDirectoryListing(const QString& text, QModelIndex parent); - -private: - // The header list is a list of column titles - const std::vector headerList; - - // Pointer to the root item. This contains all the actual item data. - RemoteModelItem* rootItem; - - // This stores the currently used network identity so it can be used for further requests, e.g. for - // lazy population. - QUrl currentRootDirectory; - QString currentClientCert; - QString currentUserName; -}; - -#endif diff --git a/src/RemoteNetwork.cpp b/src/RemoteNetwork.cpp index 7c9ede8914..01aa5eb3e8 100644 --- a/src/RemoteNetwork.cpp +++ b/src/RemoteNetwork.cpp @@ -2,82 +2,29 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include - -#include "FileDialog.h" #include "RemoteNetwork.h" #include "Settings.h" -#include "sqlite.h" #include "version.h" -using json = nlohmann::json; - RemoteNetwork::RemoteNetwork() : - m_manager(new QNetworkAccessManager), - m_progress(nullptr), - m_sslConfiguration(QSslConfiguration::defaultConfiguration()) + m_manager(new QNetworkAccessManager) { - // Set up SSL configuration - m_sslConfiguration.setPeerVerifyMode(QSslSocket::VerifyPeer); - - // Load CA certs from resource file - QDir dirCaCerts(":/certs"); - const QStringList caCertsList = dirCaCerts.entryList(); - QList caCerts; - for(const QString& caCertName : caCertsList) - caCerts += QSslCertificate::fromPath(":/certs/" + caCertName); - m_sslConfiguration.setCaCertificates(caCerts); - // Load settings and set up some more stuff while doing so reloadSettings(); // Set up signals - connect(m_manager, &QNetworkAccessManager::encrypted, this, &RemoteNetwork::gotEncrypted); connect(m_manager, &QNetworkAccessManager::sslErrors, this, &RemoteNetwork::gotError); } RemoteNetwork::~RemoteNetwork() { delete m_manager; - delete m_progress; } void RemoteNetwork::reloadSettings() { - // Load all configured client certificates - m_clientCertFiles.clear(); - const auto client_certs = Settings::getValue("remote", "client_certificates").toStringList(); - for(const QString& path : client_certs) - { - QFile file(path); - file.open(QFile::ReadOnly); - QSslCertificate cert(&file); - file.close(); - m_clientCertFiles.insert({path, cert}); - } - - // Always add the default certificate for anonymous access to dbhub.io - { - QFile file(":/user_certs/public.cert.pem"); - file.open(QFile::ReadOnly); - QSslCertificate cert(&file); - file.close(); - m_clientCertFiles.insert({":/user_certs/public.cert.pem", cert}); - } - // Configure proxy to use { QString type = Settings::getValue("proxy", "type").toString(); @@ -87,8 +34,8 @@ void RemoteNetwork::reloadSettings() { // For system settings we have to get the system-wide proxy and use that - // Get list of proxies for accessing dbhub.io via HTTPS and use the first one - auto list = QNetworkProxyFactory::systemProxyForQuery(QNetworkProxyQuery(QUrl("https://db4s.dbhub.io/"))); + // Get list of proxies for accessing sqlitebrowser.org via HTTPS and use the first one + auto list = QNetworkProxyFactory::systemProxyForQuery(QNetworkProxyQuery(QUrl("https://sqlitebrowser.org/"))); proxy = list.front(); } else { // For any other type we have to set up our own proxy configuration @@ -124,102 +71,14 @@ void RemoteNetwork::reloadSettings() } } -void RemoteNetwork::gotEncrypted(QNetworkReply* reply) -{ -#ifdef Q_OS_MAC - // Temporary workaround for now, as Qt 5.8 and below doesn't support - // verifying certificates on OSX: https://bugreports.qt.io/browse/QTBUG-56973 - // Hopefully this is fixed in Qt 5.9 - return; -#else - // Verify the server's certificate using our CA certs - auto verificationErrors = reply->sslConfiguration().peerCertificate().verify(m_sslConfiguration.caCertificates()); - bool good = false; - if(verificationErrors.size() == 0) - { - good = true; - } else if(verificationErrors.size() == 1) { - // Ignore any self signed certificate errors - if(verificationErrors.at(0).error() == QSslError::SelfSignedCertificate || verificationErrors.at(0).error() == QSslError::SelfSignedCertificateInChain) - good = true; - } - - // If the server certificate didn't turn out to be good, abort the reply here - if(!good) - reply->abort(); -#endif -} - void RemoteNetwork::gotReply(QNetworkReply* reply) { // What type of data is this? RequestType type = static_cast(reply->property("type").toInt()); - // Hide progress dialog before opening a file dialog to make sure the progress dialog doesn't interfer with the file dialog - if(type == RequestTypeDatabase || type == RequestTypePush) - m_progress->reset(); - // Handle the reply data switch(type) { - case RequestTypeDatabase: - { - // It's a database file. - - // Get last modified date as provided by the server - QDateTime last_modified; - QString content_disposition = reply->rawHeader("Content-Disposition"); - const static QRegularExpression regex("^.*modification-date=\"(.+)\";.*$", QRegularExpression::InvertedGreedinessOption); - const QRegularExpressionMatch match = regex.match(content_disposition); - if(match.hasMatch()) - last_modified = QDateTime::fromString(match.captured(1), Qt::ISODate); - - // Extract all other information from reply and send it to slots - emit fetchFinished(reply->url().fileName(), - reply->property("certfile").toString(), - reply->url(), - QUrlQuery(reply->url()).queryItemValue("commit").toStdString(), - QUrlQuery(reply->url()).queryItemValue("branch").toStdString(), - last_modified, - reply); - } - break; - case RequestTypePush: - { - // Read and check results - json obj = json::parse(reply->readAll(), nullptr, false); - if(obj.is_discarded() || !obj.is_object()) - break; - - // Extract all information from reply and send it to slots - emit pushFinished(reply->url().fileName(), - reply->property("certfile").toString(), - QString::fromStdString(obj["url"]), - obj["commit_id"], - QUrlQuery(QUrl(QString::fromStdString(obj["url"]))).queryItemValue("branch").toStdString(), - reply->property("source_file").toString()); - break; - } - case RequestTypeDownload: - { - // It's a download - - // Where should we save it? - QString path = FileDialog::getSaveFileName(FileDialogTypes::CreateDatabaseFile, - nullptr, - tr("Choose a location to save the file"), - QString(), - reply->url().fileName() + "_" + QUrlQuery(reply->url()).queryItemValue("commit") + ".db"); - if(path.isEmpty()) - break; - - // Save the downloaded data in that file - QFile file(path); - file.open(QIODevice::WriteOnly); - file.write(reply->readAll()); - file.close(); - } - break; case RequestTypeCustom: break; } @@ -228,158 +87,7 @@ void RemoteNetwork::gotReply(QNetworkReply* reply) reply->deleteLater(); } -void RemoteNetwork::gotError(QNetworkReply* reply, const QList& errors) -{ - // Are there any errors in here that aren't about self-signed certificates and non-matching hostnames? - bool serious_errors = std::any_of(errors.begin(), errors.end(), [](const QSslError& error) { return error.error() != QSslError::SelfSignedCertificate; }); - - // Just stop the error checking here and accept the reply if there were no 'serious' errors - if(!serious_errors) - { - reply->ignoreSslErrors(errors); - return; - } - - // Build an error message and short it to the user - QString message = tr("Error opening remote file at %1.\n%2").arg(reply->url().toString(), errors.at(0).errorString()); - QMessageBox::warning(nullptr, qApp->applicationName(), message); - - // Delete reply later, i.e. after returning from this slot function - if(m_progress) - m_progress->reset(); - reply->deleteLater(); -} - -void RemoteNetwork::updateProgress(qint64 bytesTransmitted, qint64 bytesTotal) -{ - // Find out to which pending reply this progress update belongs - QNetworkReply* reply = qobject_cast(QObject::sender()); - - // Update progress dialog - if(bytesTotal == -1) - { - // We don't know anything about the current progress, but it's still downloading - m_progress->setMinimum(0); - m_progress->setMaximum(0); - m_progress->setValue(0); - } else if(bytesTransmitted == bytesTotal) { - // The download has finished - m_progress->reset(); - } else { - // It's still downloading and we know the current progress - - // Were using a range 0 to 10000 here, the progress dialog will calculate 0% to 100% values from that. The reason we're not using - // the byte counts as-is is that they're 64bit wide while the progress dialog takes only 32bit values, so for large files the values - // would lose precision. The reason why we're not using a range 0 to 100 is that our range increases the precision a bit and this way - // we're prepared if the progress dialog will show decimal numbers one day on one platform. - m_progress->setMinimum(0); - m_progress->setMaximum(10000); - m_progress->setValue(static_cast((static_cast(bytesTransmitted) / static_cast(bytesTotal)) * 10000.0f)); - } - - // Check if the Cancel button has been pressed - if(reply && m_progress->wasCanceled()) - { - reply->abort(); - m_progress->reset(); - } -} - -const QList& RemoteNetwork::caCertificates() const -{ - static QList certs = m_sslConfiguration.caCertificates(); - return certs; -} - -QString RemoteNetwork::getInfoFromClientCert(const QString& cert, CertInfo info) const -{ - // Get the common name of the certificate and split it into user name and server address - QString cn = m_clientCertFiles.at(cert).subjectInfo(QSslCertificate::CommonName).at(0); - QStringList cn_parts = cn.split("@"); - if(cn_parts.size() < 2) - return QString(); - - // Return requested part of the CN - if(info == CertInfoUser) - { - return cn_parts.first(); - } else if(info == CertInfoServer) { - // Assemble the full URL from the host name. We use port 443 by default but for - // local development purposes we use 5550 instead. - QString host = cn_parts.last(); - host = QString("https://%1%2/").arg(host, host.contains("docker-dev") ? ":5550" : ""); - return host; - } - - return QString(); -} - -bool RemoteNetwork::prepareSsl(QNetworkRequest* request, const QString& clientCert) -{ - // Check if client cert exists - const QSslCertificate& cert = m_clientCertFiles[clientCert]; - if(cert.isNull()) - { - QMessageBox::warning(nullptr, qApp->applicationName(), tr("Error: Invalid client certificate specified.")); - return false; - } - - // Load private key for the client certificate - QFile fileClientCert(clientCert); - fileClientCert.open(QFile::ReadOnly); - QSslKey clientKey(&fileClientCert, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey); - while(clientKey.isNull()) - { - // If the private key couldn't be read, we assume it's password protected. So ask the user for the correct password and try reading it - // again. If the user cancels the password dialog, abort the whole process. - QString password = QInputDialog::getText(nullptr, qApp->applicationName(), tr("Please enter the passphrase for this client certificate in order to authenticate.")); - if(password.isEmpty()) - return false; - clientKey = QSslKey(&fileClientCert, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, password.toUtf8()); - } - fileClientCert.close(); - - // Set client certificate (from the cache) and private key (just loaded) - m_sslConfiguration.setLocalCertificate(cert); - m_sslConfiguration.setPrivateKey(clientKey); - - // Apply SSL configuration - request->setSslConfiguration(m_sslConfiguration); - - return true; -} - -void RemoteNetwork::prepareProgressDialog(QNetworkReply* reply, bool upload, const QUrl& url) -{ - // Instantiate progress dialog and apply some basic settings - if(!m_progress) - m_progress = new QProgressDialog(); - m_progress->reset(); - // Disable context help button on Windows - m_progress->setWindowFlags(m_progress->windowFlags() - & ~Qt::WindowContextHelpButtonHint); - m_progress->setWindowModality(Qt::NonModal); - m_progress->setCancelButtonText(tr("Cancel")); - - // Set dialog text - QString url_for_display = url.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery); - if(upload) - m_progress->setLabelText(tr("Uploading remote database to\n%1").arg(url_for_display)); - else - m_progress->setLabelText(tr("Downloading remote database from\n%1").arg(url_for_display)); - - // Show dialog - m_progress->show(); - - // Make sure the dialog is updated - if(upload) - connect(reply, &QNetworkReply::uploadProgress, this, &RemoteNetwork::updateProgress); - else - connect(reply, &QNetworkReply::downloadProgress, this, &RemoteNetwork::updateProgress); -} - -void RemoteNetwork::fetch(const QUrl& url, RequestType type, const QString& clientCert, - std::function when_finished, bool synchronous, bool ignore_errors) +void RemoteNetwork::fetch(const QUrl& url, RequestType type, std::function when_finished, bool synchronous, bool ignore_errors) { // Build network request QNetworkRequest request; @@ -391,23 +99,9 @@ void RemoteNetwork::fetch(const QUrl& url, RequestType type, const QString& clie request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); #endif - // Set SSL configuration when trying to access a file via the HTTPS protocol. - // Skip this step when no client certificate was specified. In this case the default HTTPS configuration is used. - bool https = url.scheme().compare("https", Qt::CaseInsensitive) == 0; - if(https && !clientCert.isNull()) - { - // If configuring the SSL connection fails, abort the request here - if(!prepareSsl(&request, clientCert)) - return; - } - - // Clear access cache if necessary - clearAccessCache(clientCert); - // Fetch database and prepare pending reply for future processing QNetworkReply* reply = m_manager->get(request); reply->setProperty("type", type); - reply->setProperty("certfile", clientCert); reply->setProperty("ignore_errors", ignore_errors); // Hook up custom handler when there is one and global handler otherwise @@ -431,109 +125,26 @@ void RemoteNetwork::fetch(const QUrl& url, RequestType type, const QString& clie connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); loop.exec(); } - - // Initialise the progress dialog for this request, but only if this is a database file or a download. - // Directory listing and similar are small enough to be loaded without progress dialog. - if(type == RequestTypeDatabase || type == RequestTypeDownload) - prepareProgressDialog(reply, false, url); } -void RemoteNetwork::push(const QString& filename, const QUrl& url, const QString& clientCert, const QString& remotename, - const QString& commitMessage, const QString& licence, bool isPublic, const QString& branch, - bool forcePush, const QString& last_commit) +void RemoteNetwork::gotError(QNetworkReply* reply, const QList& errors) { - // Open the file to send and check if it exists - QFile* file = new QFile(filename); - if(!file->open(QFile::ReadOnly)) - { - delete file; - QMessageBox::warning(nullptr, qApp->applicationName(), tr("Error: Cannot open the file for sending.")); - return; - } - - // Build network request - QNetworkRequest request; - request.setUrl(url); - request.setRawHeader("User-Agent", QString("%1 %2").arg(qApp->organizationName(), APP_VERSION).toUtf8()); - - // Get the last modified date of the file and prepare it for conversion into the ISO date format - QDateTime last_modified = QFileInfo(filename).lastModified().toOffsetFromUtc(0); - - // Prepare HTTP multi part data containing all the information about the commit we're about to push - QHttpMultiPart* multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType); - addPart(multipart, "file", file, remotename); - addPart(multipart, "commitmsg", commitMessage); - addPart(multipart, "licence", licence); - addPart(multipart, "public", isPublic ? "true" : "false"); - addPart(multipart, "branch", branch); - addPart(multipart, "force", forcePush ? "true" : "false"); - addPart(multipart, "lastmodified", last_modified.toString("yyyy-MM-dd'T'HH:mm:ss'Z'")); - - // Only add commit id if one was provided - if(!last_commit.isEmpty()) - addPart(multipart, "commit", last_commit); + // Are there any errors in here that aren't about self-signed certificates and non-matching hostnames? + bool serious_errors = std::any_of(errors.begin(), errors.end(), [](const QSslError& error) { return error.error() != QSslError::SelfSignedCertificate; }); - // Set SSL configuration when trying to access a file via the HTTPS protocol - bool https = url.scheme().compare("https", Qt::CaseInsensitive) == 0; - if(https) + // Just stop the error checking here and accept the reply if there were no 'serious' errors + if(!serious_errors) { - // If configuring the SSL connection fails, abort the request here - if(!prepareSsl(&request, clientCert)) - { - delete file; - return; - } + reply->ignoreSslErrors(errors); + return; } - // Clear access cache if necessary - clearAccessCache(clientCert); - - // Put database to remote server and save pending reply for future processing - QNetworkReply* reply = m_manager->post(request, multipart); - reply->setProperty("type", RequestTypePush); - reply->setProperty("certfile", clientCert); - reply->setProperty("source_file", filename); - multipart->setParent(reply); // Delete the multi-part object along with the reply - - // Connect reply handler - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - if(handleReply(reply)) - gotReply(reply); - }); - - // Initialise the progress dialog for this request - prepareProgressDialog(reply, true, url); -} - -void RemoteNetwork::addPart(QHttpMultiPart* multipart, const QString& name, const QString& value) const -{ - QHttpPart part; - part.setHeader(QNetworkRequest::ContentDispositionHeader, QString("form-data; name=\"%1\"").arg(name)); - part.setBody(value.toUtf8()); - - multipart->append(part); -} - -void RemoteNetwork::addPart(QHttpMultiPart* multipart, const QString& name, QFile* file, const QString& filename) const -{ - QHttpPart part; - part.setHeader(QNetworkRequest::ContentDispositionHeader, QString("form-data; name=\"%1\"; filename=\"%2\"").arg(name, filename)); - part.setBodyDevice(file); - file->setParent(multipart); // Close the file and delete the file object as soon as the multi-part object is destroyed - - multipart->append(part); -} + // Build an error message and show it to the user + QString message = tr("Error opening remote file at %1.\n%2").arg(reply->url().toString(), errors.at(0).errorString()); + QMessageBox::warning(nullptr, qApp->applicationName(), message); -void RemoteNetwork::clearAccessCache(const QString& clientCert) -{ - // When the client certificate is different from the one before, clear the access and authentication cache. - // Otherwise Qt might use the old certificate again. - static QString lastClientCert; - if(lastClientCert != clientCert) - { - lastClientCert = clientCert; - m_manager->clearAccessCache(); - } + // Delete reply later, i.e. after returning from this slot function + reply->deleteLater(); } bool RemoteNetwork::handleReply(QNetworkReply* reply) diff --git a/src/RemoteNetwork.h b/src/RemoteNetwork.h index 0ece572a0e..28dd654e41 100644 --- a/src/RemoteNetwork.h +++ b/src/RemoteNetwork.h @@ -4,15 +4,8 @@ #include #include -#include -#include - class QNetworkAccessManager; class QNetworkReply; -class QProgressDialog; -class QNetworkRequest; -class QHttpMultiPart; -class QFile; class RemoteNetwork : public QObject { @@ -27,65 +20,25 @@ class RemoteNetwork : public QObject void reloadSettings(); - enum CertInfo - { - CertInfoUser, - CertInfoServer, - }; - - const QList& caCertificates() const; - const std::map& clientCertificates() const { return m_clientCertFiles; } - QString getInfoFromClientCert(const QString& cert, CertInfo info) const; - enum RequestType { RequestTypeCustom, - RequestTypeDatabase, - RequestTypePush, - RequestTypeDownload, }; - void fetch(const QUrl& url, RequestType type, const QString& clientCert = QString(), - std::function when_finished = {}, bool synchronous = false, bool ignore_errors = false); - void push(const QString& filename, const QUrl& url, const QString& clientCert, const QString& remotename, - const QString& commitMessage = QString(), const QString& licence = QString(), bool isPublic = false, - const QString& branch = QString("main"), bool forcePush = false, const QString& last_commit = QString()); - -signals: - // The fetchFinished() signal is emitted when a fetch() call for a database is finished - void fetchFinished(QString filename, QString identity, const QUrl& url, std::string new_commit_id, std::string branch, - QDateTime last_modified, QIODevice* device); - - // The pushFinished() signal is emitted when a push() call is finished, i.e. a database upload has completed. - void pushFinished(QString filename, QString identity, const QUrl& url, std::string new_commit_id, std::string branch, QString source_file); + void fetch(const QUrl& url, RequestType type, std::function when_finished = {}, bool synchronous = false, bool ignore_errors = false); private: RemoteNetwork(); ~RemoteNetwork() override; - void gotEncrypted(QNetworkReply* reply); void gotReply(QNetworkReply* reply); void gotError(QNetworkReply* reply, const QList& errors); - void updateProgress(qint64 bytesTransmitted, qint64 bytesTotal); - bool prepareSsl(QNetworkRequest* request, const QString& clientCert); - void prepareProgressDialog(QNetworkReply* reply, bool upload, const QUrl& url); - - // Helper functions for building multi-part HTTP requests - void addPart(QHttpMultiPart* multipart, const QString& name, const QString& value) const; - void addPart(QHttpMultiPart* multipart, const QString& name, QFile* file, const QString& filename) const; - - // Before using a new client certificate we need to clear the access and authentication cache of the network manager - // object. Otherwise Qt might reuse the old certificate if the requested URL has been used before. - void clearAccessCache(const QString& clientCert); // This function is called for all network replies we get whether they are handled globally or individually. // It mainly does some error checking and returns true if the actual handler should be called. bool handleReply(QNetworkReply* reply); QNetworkAccessManager* m_manager; - QProgressDialog* m_progress; - QSslConfiguration m_sslConfiguration; - std::map m_clientCertFiles; }; #endif diff --git a/src/RemotePushDialog.cpp b/src/RemotePushDialog.cpp deleted file mode 100644 index f40dc874f6..0000000000 --- a/src/RemotePushDialog.cpp +++ /dev/null @@ -1,175 +0,0 @@ -#include -#include - -#include - -#include - -#include "RemotePushDialog.h" -#include "ui_RemotePushDialog.h" -#include "RemoteNetwork.h" - -using json = nlohmann::json; - -RemotePushDialog::RemotePushDialog(QWidget* parent, const QString& host, const QString& clientCert, - const QString& name, const QString& branch, const QString& user) : - QDialog(parent), - ui(new Ui::RemotePushDialog), - m_host(host), - m_clientCert(clientCert), - m_nameValidator(new QRegularExpressionValidator(QRegularExpression("^[a-z,A-Z,0-9,\\.,\\-,\\_,\\(,\\),\\+,\\ ]+$"), this)), - m_branchValidator(new QRegularExpressionValidator(QRegularExpression("^[a-z,A-Z,0-9,\\^,\\.,\\-,\\_,\\/,\\(,\\),\\:,\\&,\\ )]+$"), this)) -{ - // Create UI - ui->setupUi(this); - ui->editName->setValidator(m_nameValidator); - ui->comboBranch->setValidator(m_branchValidator); - ui->comboUser->setValidator(m_nameValidator); - - // Set start values - ui->editName->setText(name); - - // Fill in usernames - ui->comboUser->addItem(RemoteNetwork::get().getInfoFromClientCert(m_clientCert, RemoteNetwork::CertInfoUser)); - if(!user.isEmpty()) - ui->comboUser->addItem(user); - - // Enable/disable accept button - checkInput(); - - // Fetch list of available licences - RemoteNetwork::get().fetch(host + "licence/list", RemoteNetwork::RequestTypeCustom, clientCert, [this](const QByteArray& reply) { - // Clear licence list - ui->comboLicence->clear(); - - // Read and check results - json obj = json::parse(reply, nullptr, false); - if(obj.is_discarded() || !obj.is_object()) - return; - - // Parse data and build ordered licence map: order -> (short name, long name) - std::map> licences; - for(auto it=obj.cbegin();it!=obj.cend();++it) - licences.insert({it.value()["order"], {it.key(), it.value()["full_name"]}}); - - // Parse licence list and fill combo box. Show the full name to the user and use the short name as user data. - for(auto it=licences.begin();it!=licences.end();++it) - ui->comboLicence->addItem(QString::fromStdString(it->second.second), QString::fromStdString(it->second.first)); - }); - - // Fetch list of existing branches - reloadBranchList(branch); -} - -RemotePushDialog::~RemotePushDialog() -{ - delete ui; -} - -void RemotePushDialog::checkInput() -{ - // Update public/private check box text - if(ui->checkPublic->isChecked()) - ui->checkPublic->setText(tr("Database will be public. Everyone has read access to it.")); - else - ui->checkPublic->setText(tr("Database will be private. Only you have access to it.")); - - // Update the foce push check box text - if(ui->checkForce->isChecked()) - ui->checkForce->setText(tr("Use with care. This can cause remote commits to be deleted.")); - else - ui->checkForce->setText(" "); // The space character here is required to avoid annoying resizes when toggling the checkbox - - // Check input - bool valid = true; - - if(ui->editName->text().trimmed().isEmpty()) - valid = false; - - if(ui->editCommitMessage->toPlainText().size() > 1024) - valid = false; - - if(ui->comboBranch->currentText().size() < 1 || ui->comboBranch->currentText().size() > 32) - valid = false; - - if(ui->comboUser->currentText().trimmed().isEmpty()) - valid = false; - - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(valid); -} - -void RemotePushDialog::accept() -{ - QDialog::accept(); -} - -QString RemotePushDialog::name() const -{ - return ui->editName->text().trimmed(); -} - -QString RemotePushDialog::commitMessage() const -{ - return ui->editCommitMessage->toPlainText().trimmed(); -} - -QString RemotePushDialog::licence() const -{ - return ui->comboLicence->currentData(Qt::UserRole).toString(); -} - -bool RemotePushDialog::isPublic() const -{ - return ui->checkPublic->isChecked(); -} - -QString RemotePushDialog::branch() const -{ - return ui->comboBranch->currentText(); -} - -QString RemotePushDialog::user() const -{ - return ui->comboUser->currentText(); -} - -bool RemotePushDialog::forcePush() const -{ - return ui->checkForce->isChecked(); -} - -void RemotePushDialog::reloadBranchList(const QString& select_branch) -{ - QUrl url(m_host + "branch/list"); - QUrlQuery query; - query.addQueryItem("username", ui->comboUser->currentText()); - query.addQueryItem("folder", "/"); - query.addQueryItem("dbname", ui->editName->text()); - url.setQuery(query); - RemoteNetwork::get().fetch(url.toString(), RemoteNetwork::RequestTypeCustom, m_clientCert, [this, select_branch](const QByteArray& reply) { - // Read and check results - json obj = json::parse(reply, nullptr, false); - if(obj.is_discarded() || !obj.is_object()) - return; - json obj_branches = obj["branches"]; - - // Get default branch - std::string default_branch = (obj.contains("default_branch") && !obj["default_branch"].empty()) ? obj["default_branch"] : "main"; - - // Clear branch list and add the default branch - ui->comboBranch->clear(); - ui->comboBranch->addItem(QString::fromStdString(default_branch)); - - // Parse data and assemble branch list - std::vector branches; - for(auto it=obj_branches.cbegin();it!=obj_branches.cend();++it) - { - if(it.key() != default_branch) - ui->comboBranch->addItem(QString::fromStdString(it.key())); - } - - // If a branch was suggested, select it now - if(!select_branch.isEmpty()) - ui->comboBranch->setCurrentIndex(ui->comboBranch->findText(select_branch)); - }, false, true); -} diff --git a/src/RemotePushDialog.h b/src/RemotePushDialog.h deleted file mode 100644 index 1b2033df2c..0000000000 --- a/src/RemotePushDialog.h +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef REMOTEPUSHDIALOG_H -#define REMOTEPUSHDIALOG_H - -#include - -class QRegularExpressionValidator; - -namespace Ui { -class RemotePushDialog; -} - -class RemotePushDialog : public QDialog -{ - Q_OBJECT - -public: - explicit RemotePushDialog(QWidget* parent, const QString& host, const QString& clientCert, - const QString& name = QString(), const QString& branch = QString(), const QString& user = QString()); - ~RemotePushDialog() override; - - QString name() const; - QString commitMessage() const; - QString licence() const; - bool isPublic() const; - QString branch() const; - QString user() const; - bool forcePush() const; - -private: - Ui::RemotePushDialog* ui; - - // Connection details - QString m_host; - QString m_clientCert; - - // Validators - QRegularExpressionValidator* m_nameValidator; - QRegularExpressionValidator* m_branchValidator; - -protected slots: - void checkInput(); - void reloadBranchList(const QString& select_branch = QString()); - void accept() override; -}; - -#endif diff --git a/src/RemotePushDialog.ui b/src/RemotePushDialog.ui deleted file mode 100644 index 49dab0c223..0000000000 --- a/src/RemotePushDialog.ui +++ /dev/null @@ -1,333 +0,0 @@ - - - RemotePushDialog - - - - 0 - 0 - 583 - 315 - - - - Push database - - - - - - - - Database na&me to push to - - - editName - - - - - - - 256 - - - - - - - Commit message - - - editCommitMessage - - - - - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Noto Sans'; font-size:10pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> - - - false - - - - - - - Database licence - - - comboLicence - - - - - - - - 0 - 0 - - - - - - - - Public - - - checkPublic - - - - - - - - - - Branch - - - comboBranch - - - - - - - true - - - true - - - - - - - Force push - - - checkForce - - - - - - - - - - Username - - - comboUser - - - - - - - true - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - editName - editCommitMessage - comboBranch - checkPublic - comboLicence - comboUser - checkForce - - - - - buttonBox - accepted() - RemotePushDialog - accept() - - - 257 - 305 - - - 157 - 274 - - - - - buttonBox - rejected() - RemotePushDialog - reject() - - - 325 - 305 - - - 286 - 274 - - - - - editName - textChanged(QString) - RemotePushDialog - checkInput() - - - 201 - 25 - - - 217 - 3 - - - - - checkPublic - toggled(bool) - RemotePushDialog - checkInput() - - - 345 - 181 - - - 210 - 175 - - - - - editCommitMessage - textChanged() - RemotePushDialog - checkInput() - - - 175 - 113 - - - 91 - 111 - - - - - editName - textChanged(QString) - RemotePushDialog - reloadBranchList() - - - 176 - 25 - - - 77 - 3 - - - - - comboBranch - currentTextChanged(QString) - RemotePushDialog - checkInput() - - - 346 - 160 - - - 33 - 151 - - - - - checkForce - toggled(bool) - RemotePushDialog - checkInput() - - - 342 - 269 - - - 62 - 229 - - - - - comboUser - currentTextChanged(QString) - RemotePushDialog - reloadBranchList() - - - 373 - 235 - - - 291 - 157 - - - - - comboUser - currentTextChanged(QString) - RemotePushDialog - checkInput() - - - 373 - 235 - - - 291 - 157 - - - - - - checkInput() - reloadBranchList() - - diff --git a/src/RowLoader.cpp b/src/RowLoader.cpp index 2fa5c72139..fde19b88f2 100644 --- a/src/RowLoader.cpp +++ b/src/RowLoader.cpp @@ -235,7 +235,8 @@ void RowLoader::process (Task & t) auto row = t.row_begin; if(sqlite3_prepare_v2(pDb.get(), utf8Query, utf8Query.size(), &stmt, nullptr) == SQLITE_OK) { - while(!t.cancel && sqlite3_step(stmt) == SQLITE_ROW) + int step_result = SQLITE_OK; + while(!t.cancel && (step_result = sqlite3_step(stmt)) == SQLITE_ROW) { size_t num_columns = static_cast(sqlite3_data_count(stmt)); @@ -257,6 +258,11 @@ void RowLoader::process (Task & t) cache_data.set(row++, std::move(rowdata)); } + if (step_result != SQLITE_ROW && step_result != SQLITE_DONE && step_result != SQLITE_OK && !t.cancel) { + QString errorMsg = QString::fromUtf8(sqlite3_errmsg(pDb.get())); + emit error(t.token, errorMsg); + } + sqlite3_finalize(stmt); // Query the total row count if and only if: diff --git a/src/RowLoader.h b/src/RowLoader.h index b3224bcf13..cba38221a0 100644 --- a/src/RowLoader.h +++ b/src/RowLoader.h @@ -65,6 +65,7 @@ class RowLoader : public QThread signals: void fetched(int token, size_t row_begin, size_t row_end); void rowCountComplete(int token, int num_rows); + void error(int token, const QString& errMsg); private: const std::function()> db_getter; diff --git a/src/RunSql.cpp b/src/RunSql.cpp index 31cada67d0..29c25f3a66 100644 --- a/src/RunSql.cpp +++ b/src/RunSql.cpp @@ -104,6 +104,22 @@ bool RunSql::executeNextStatement() auto time_end_prepare = std::chrono::high_resolution_clock::now(); auto time_for_prepare_in_ms = std::chrono::duration_cast(time_end_prepare - time_start); + // If there was no actual statement (e.g. empty line or comments), SQLite returns SQLITE_OK but vm is nullptr. + // We can safely skip execution of this block. + if (sql3status == SQLITE_OK && !vm) + { + lk.lock(); + releaseDbAccess(); + + execute_current_position = end_of_current_statement_position; + if(execute_current_position >= execute_to_position) + { + stopExecution(); + return false; + } + return true; + } + // Execute prepared statement QString error; if (sql3status == SQLITE_OK) @@ -117,7 +133,8 @@ bool RunSql::executeNextStatement() query_type == DropStatement || query_type == RollbackStatement || query_type == AttachStatement || - query_type == DetachStatement)) + query_type == DetachStatement || + query_type == AnalyzeStatement)) structure_updated = true; // Check whether this is trying to set a pragma or to vacuum the database @@ -166,23 +183,24 @@ bool RunSql::executeNextStatement() // Start measuring time from here again time_start = std::chrono::high_resolution_clock::now(); - // Check if this statement returned any data. We skip this check if this is an ALTER TABLE statement which, for some reason, are reported to return one column. - if(query_type != AlterStatement && sqlite3_column_count(vm)) - { - // It did. So it is definitely some SELECT statement or similar and we don't need to actually execute it here - sql3status = SQLITE_ROW; - } else { - // It did not. So it's probably some modifying SQL statement and we want to execute it here. If for some reason - // it turns out to return data after all, we just change the status - sql3status = sqlite3_step(vm); - - // SQLite returns SQLITE_DONE when a valid SELECT statement was executed but returned no results. To run into the branch that updates - // the status message and the table view anyway manipulate the status value here. This is also done for PRAGMA statements as they (sometimes) - // return rows just like SELECT statements, too. - if((query_type == SelectStatement || query_type == PragmaStatement) && sql3status == SQLITE_DONE) + if(sql3status == SQLITE_OK || sql3status == SQLITE_ROW) { + // Check if this statement returned any data. We skip this check if this is an ALTER TABLE statement which, for some reason, are reported to return one column. + if(query_type != AlterStatement && sqlite3_column_count(vm)) + { + // It did. So it is definitely some SELECT statement or similar and we don't need to actually execute it here sql3status = SQLITE_ROW; + } else { + // It did not. So it's probably some modifying SQL statement and we want to execute it here. If for some reason + // it turns out to return data after all, we just change the status + sql3status = sqlite3_step(vm); + + // SQLite returns SQLITE_DONE when a valid SELECT statement was executed but returned no results. To run into the branch that updates + // the status message and the table view anyway manipulate the status value here. This is also done for PRAGMA statements as they (sometimes) + // return rows just like SELECT statements, too. + if((query_type == SelectStatement || query_type == PragmaStatement) && sql3status == SQLITE_DONE) + sql3status = SQLITE_ROW; + } } - // Destroy statement sqlite3_finalize(vm); @@ -245,8 +263,6 @@ bool RunSql::executeNextStatement() lk.unlock(); break; } - case SQLITE_MISUSE: - break; default: error = QString::fromUtf8(sqlite3_errmsg(pDb.get())); } @@ -319,6 +335,7 @@ RunSql::StatementType RunSql::getQueryType(const QString& query) if(query.startsWith("CREATE", Qt::CaseInsensitive)) return CreateStatement; if(query.startsWith("ATTACH", Qt::CaseInsensitive)) return AttachStatement; if(query.startsWith("DETACH", Qt::CaseInsensitive)) return DetachStatement; + if(query.startsWith("ANALYZE", Qt::CaseInsensitive)) return AnalyzeStatement; return OtherStatement; } diff --git a/src/RunSql.h b/src/RunSql.h index 15eb615e85..6a9f13c2bf 100644 --- a/src/RunSql.h +++ b/src/RunSql.h @@ -41,6 +41,7 @@ class RunSql : public QThread CreateStatement, AttachStatement, DetachStatement, + AnalyzeStatement, OtherStatement, }; diff --git a/src/Settings.cpp b/src/Settings.cpp index bb1e31c4d8..6fe1b5d7f3 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -159,6 +159,10 @@ QVariant Settings::getDefaultValue(const std::string& group, const std::string& if(group == "db" && name == "fontsize") return 10; + // db/watcher? + if(group == "db" && name == "watcher") + return false; + // exportcsv/firstrowheader? if(group == "exportcsv" && name == "firstrowheader") return true; @@ -303,8 +307,12 @@ QVariant Settings::getDefaultValue(const std::string& group, const std::string& return 1000; if(name == "image_preview") return false; + if(name == "cell_word_wrap") + return true; if(name == "indent_compact") return false; + if (name == "sort_keys") + return true; if(name == "auto_switch_mode") return true; if(name == "editor_word_wrap") @@ -433,6 +441,9 @@ QVariant Settings::getDefaultValue(const std::string& group, const std::string& if(name == "dropSelectQuery") return true; + if(name == "dropInsert") + return false; + if(name == "dropQualifiedNames") return false; @@ -440,22 +451,6 @@ QVariant Settings::getDefaultValue(const std::string& group, const std::string& return true; } - // Remote settings? - if(group == "remote") - { - // Enable the File → Remote menu by default - if(name == "active") - return true; - - // Clone directory - if(name == "clonedirectory") -#if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0) - return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); -#else - return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); -#endif - } - // Proxy settings if(group == "proxy") { diff --git a/src/SqlUiLexer.cpp b/src/SqlUiLexer.cpp index 314a73bcab..face122596 100644 --- a/src/SqlUiLexer.cpp +++ b/src/SqlUiLexer.cpp @@ -80,6 +80,9 @@ void SqlUiLexer::setupAutoCompletion() {"changes", tr("() The changes() function returns the number of database rows that were changed or inserted or deleted by the most recently completed INSERT, DELETE, or UPDATE statement.")}, {"char", tr("(X1,X2,...) The char(X1,X2,...,XN) function returns a string composed of characters having the unicode code point values of integers X1 through XN, respectively. ")}, {"coalesce", tr("(X,Y,...) The coalesce() function returns a copy of its first non-NULL argument, or NULL if all arguments are NULL")}, + {"concat", tr("(X,...) The concat(...) function returns a string which is the concatenation of the string representation of all of its non-NULL arguments. If all arguments are NULL, then concat() returns an empty string.")}, + {"concat_ws", tr("(SEP,X,...) The concat_ws(SEP,...) function returns a string that is the concatenation of all non-null arguments beyond the first argument, using the text value of the first argument as a separator.")}, + {"format", tr("(FORMAT,...) The format(FORMAT,...) SQL function works like the sqlite3_mprintf() C-language function and the printf() function from the standard C library.")}, {"glob", tr("(X,Y) The glob(X,Y) function is equivalent to the expression \"Y GLOB X\".")}, {"ifnull", tr("(X,Y) The ifnull() function returns a copy of its first non-NULL argument, or NULL if both arguments are NULL.")}, {"instr", tr("(X,Y) The instr(X,Y) function finds the first occurrence of string Y within string X and returns the number of prior characters plus 1, or 0 if Y is nowhere found within X.")}, @@ -89,6 +92,8 @@ void SqlUiLexer::setupAutoCompletion() {"length", tr("(X) For a string value X, the length(X) function returns the number of characters (not bytes) in X prior to the first NUL character.")}, {"like", tr("(X,Y) The like() function is used to implement the \"Y LIKE X\" expression.")}, {"like", tr("(X,Y,Z) The like() function is used to implement the \"Y LIKE X ESCAPE Z\" expression.")}, + {"likelihood", tr("(X,Y) The purpose of the likelihood(X,Y) function is to provide a hint to the query planner that the argument X is a boolean that is true with a probability of approximately Y.")}, + {"likely", tr("(X) The purpose of the likely(X) function is to provide a hint to the query planner that the argument X is a boolean value that is usually true.")}, {"load_extension", tr("(X) The load_extension(X) function loads SQLite extensions out of the shared library file named X.\nUse of this function must be authorized from Preferences.")}, {"load_extension", tr("(X,Y) The load_extension(X) function loads SQLite extensions out of the shared library file named X using the entry point Y.\nUse of this function must be authorized from Preferences.")}, {"lower", tr("(X) The lower(X) function returns a copy of string X with all ASCII characters converted to lower case.")}, @@ -97,6 +102,7 @@ void SqlUiLexer::setupAutoCompletion() {"max", tr("(X,Y,...) The multi-argument max() function returns the argument with the maximum value, or return NULL if any argument is NULL.")}, {"min", tr("(X,Y,...) The multi-argument min() function returns the argument with the minimum value.")}, {"nullif", tr("(X,Y) The nullif(X,Y) function returns its first argument if the arguments are different and NULL if the arguments are the same.")}, + {"octet_length", tr("(X) The octet_length(X) function returns the number of bytes in the encoding of X.")}, {"printf", tr("(FORMAT,...) The printf(FORMAT,...) SQL function works like the sqlite3_mprintf() C-language function and the printf() function from the standard C library.")}, {"quote", tr("(X) The quote(X) function returns the text of an SQL literal which is the value of its argument suitable for inclusion into an SQL statement.")}, {"random", tr("() The random() function returns a pseudo-random integer between -9223372036854775808 and +9223372036854775807.")}, @@ -113,7 +119,10 @@ void SqlUiLexer::setupAutoCompletion() {"trim", tr("(X) trim(X) removes spaces from both ends of X.")}, {"trim", tr("(X,Y) The trim(X,Y) function returns a string formed by removing any and all characters that appear in Y from both ends of X.")}, {"typeof", tr("(X) The typeof(X) function returns a string that indicates the datatype of the expression X.")}, + {"unhex", tr("(X) The unhex(X) function returns a BLOB value which is the decoding of the hexadecimal string X. X must be a pure hexadecimal string.")}, + {"unhex", tr("(X,Y) The unhex(X,Y) function returns a BLOB value which is the decoding of the hexadecimal string X. If X contains any characters that are not hexadecimal digits and which are not in Y, then unhex(X,Y) returns NULL.")}, {"unicode", tr("(X) The unicode(X) function returns the numeric unicode code point corresponding to the first character of the string X.")}, + {"unlikely", tr("(X) The purpose of the unlikely(X) function is to provide a hint to the query planner that the argument X is a boolean value that is usually not true.")}, {"upper", tr("(X) The upper(X) function returns a copy of input string X in which all lower-case ASCII characters are converted to their upper-case equivalent.")}, {"zeroblob", tr("(N) The zeroblob(N) function returns a BLOB consisting of N bytes of 0x00.")}, // Date and time functions @@ -127,6 +136,7 @@ void SqlUiLexer::setupAutoCompletion() {"count", tr("(X) The count(X) function returns a count of the number of times that X is not NULL in a group.")}, {"group_concat", tr("(X) The group_concat() function returns a string which is the concatenation of all non-NULL values of X.")}, {"group_concat", tr("(X,Y) The group_concat() function returns a string which is the concatenation of all non-NULL values of X. If parameter Y is present then it is used as the separator between instances of X.")}, + {"string_agg", tr("(X,Y) string_agg(X,Y) function is an alias for group_concat(X,Y). String_agg() is compatible with PostgreSQL and SQL-Server and group_concat() is compatible with MySQL.")}, {"max", tr("(X) The max() aggregate function returns the maximum value of all values in the group.")}, {"min", tr("(X) The min() aggregate function returns the minimum non-NULL value of all values in the group.")}, {"sum", tr("(X) The sum() and total() aggregate functions return sum of all non-NULL values in the group.")}, diff --git a/src/TableBrowser.cpp b/src/TableBrowser.cpp index 5156be5d65..6dc3e8f2ee 100644 --- a/src/TableBrowser.cpp +++ b/src/TableBrowser.cpp @@ -1724,11 +1724,13 @@ void TableBrowser::fetchedData() return; m_columnsResized = true; - // Set column widths according to their contents but make sure they don't exceed a certain size - ui->dataTable->resizeColumnsToContents(); - for(int i = 0; i < m_model->columnCount(); i++) - { - if(ui->dataTable->columnWidth(i) > 300) - ui->dataTable->setColumnWidth(i, 300); + if (m_settings[currentlyBrowsedTableName()].columnWidths.empty()) { + // Set column widths according to their contents but make sure they don't exceed a certain size + ui->dataTable->resizeColumnsToContents(); + for(int i = 0; i < m_model->columnCount(); i++) + { + if(ui->dataTable->columnWidth(i) > 300) + ui->dataTable->setColumnWidth(i, 300); + } } } diff --git a/src/certs/CaCerts.qrc b/src/certs/CaCerts.qrc deleted file mode 100644 index 43826f1c78..0000000000 --- a/src/certs/CaCerts.qrc +++ /dev/null @@ -1,9 +0,0 @@ - - - ca-chain-development.crt - ca-chain-docker.crt - - - public.cert.pem - - diff --git a/src/certs/ca-chain-development.crt b/src/certs/ca-chain-development.crt deleted file mode 100644 index cec20f99ac..0000000000 --- a/src/certs/ca-chain-development.crt +++ /dev/null @@ -1,68 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIF7DCCA9SgAwIBAgIJAKtqE1Cz9EmfMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD -VQQGEwJHQjEQMA4GA1UECAwHRW5nbGFuZDERMA8GA1UECgwIREJIdWIuaW8xJzAl -BgNVBAsMHkRCSHViLmlvIENlcnRpZmljYXRlIEF1dGhvcml0eTElMCMGA1UEAwwc -REJIdWIuaW8gREVWRUxPUE1FTlQgUm9vdCBDQTAeFw0xNzAzMDQxMDE1MzBaFw0z -NzAzMDQxMDE1MzBaMIGCMQswCQYDVQQGEwJHQjEQMA4GA1UECAwHRW5nbGFuZDER -MA8GA1UECgwIREJIdWIuaW8xJzAlBgNVBAsMHkRCSHViLmlvIENlcnRpZmljYXRl -IEF1dGhvcml0eTElMCMGA1UEAwwcREJIdWIuaW8gREVWRUxPUE1FTlQgUm9vdCBD -QTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMlg1CFBIRwv87u2A8Gl -IqIJgGBlEIF9EREF6UyIylHuvwEV7efOUwwaAoF2N1V4w7MlHPeW7o7eKMt0LFI/ -fDBRb6xz3bxnR8Mxr9p4zn77qocDU/AJDAk/ZMMRi4urIQg6tFBp1gsbSaWgsVFm -wgewTyXSUu51PAgRtXPhiiMKwabjOjxZJGZY7vCP1vl6bL5Dp9pvoShSD/BcCB1a -b2FiPBSTfl45Ovs+oW7enKOik/jKXqqGMtDCyjLl71wObTyn3Sv8uKuuMc0bY7ui -747sNmUWQFwP27BsXtHY27Q1dgC7oR1o3uyE9nLlOHrycLoVz/WuS5+UwbVlH+9x -ixxuW8fhAHXO1hUG8ZNsUVqiBKaVryMsWgM76kCiRRHbwLXsSKu/zwo+HcHhnqku -tq/+ibV9R0u1reSZ46rVhmLCuD1BWO5OEQRujlpGBAQPu0ajm7Ym+vG3MHTOeI3p -LqAM+0QnLKhDDC6kwVpFmZkcvTsKBtIFRw26H6pGXmapzxrcuAzM6MKIQGiJaduf -Vn8RvrTzSxGqWHb10DLAosfV2dBAT7qUpGw9yAtpjpKudjuZ6WDcAGUrPwozxfqo -cWjamTMML8r2Lsm/DBtmMHcpxqLw17FzWSzkP8HVvLMJYkkkO6uixdUMnSTm5H7V -VzZZSpbtRtcrVMFR9sqB+y41AgMBAAGjYzBhMB0GA1UdDgQWBBSHZI71PcSJIxOh -4sspAvBhGwivJTAfBgNVHSMEGDAWgBSHZI71PcSJIxOh4sspAvBhGwivJTAPBgNV -HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEA -Uxjm+WdsvoQwZFQfcYAJDB4hLKe2257jBcfRJdC8kuU2hlNKLwJIoepd5ByMtFMz -pYkcHvOVYPiYXBT+b6ReluvnNun3bELWa+XvhYdDIkgEqYnnIhnVKKjONOQOo1mh -pvuA5iW7G9zxYHrdu3WFCPTIQU/YdRnN1X9uqS8AdPWIsuOqHl6+mivjSAgLDq1J -5mYatCKRIC1OxZDPqXRgyd0LwD9sU0ImBb/icLDa0bAWt/gXid75rZV08zOOzd8f -4CmDO679o7D0S4opf3JSpyjWg5ALncmygcX83Uk6AaNvlEKvwKTp5vCNsiHml7IO -KuPXRqJJxAFErpjbaboon+WX3zpOh0w4DRS6UB7mmWeZ8/rbSG5KyHUvCy6gAEoW -i1EKfrt0T+7xx2jERdSTX0Vy3+G5CrS74MRgdDz+QYFVevY1zGc34U/+3l32VtMZ -x7I5jLXb+LiZ5JQrzfPf62xO9jJogczt9DuHQ4BOAtqAFTzZvMRnyCgxD49IfsZ6 -YZ/CNNlqdoPJkXmR/BHiUk/k6ZC3vYa/Am3tniQp7RxeV6s4hTB8XS6CN2Aq8cnS -a5dtZbFSXNMB4QfBKGDg/gUwxU3j6VyoQJnmKt2QVgQqG6Sz1Px+YfmQXOMoGiFM -IhGSBh8DEnh/aNtGXBF3OaYjAfSg9zfq9ATUmSzjzxY= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIF8DCCA9igAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNVBAYTAkdC -MRAwDgYDVQQIDAdFbmdsYW5kMREwDwYDVQQKDAhEQkh1Yi5pbzEnMCUGA1UECwwe -REJIdWIuaW8gQ2VydGlmaWNhdGUgQXV0aG9yaXR5MSUwIwYDVQQDDBxEQkh1Yi5p -byBERVZFTE9QTUVOVCBSb290IENBMB4XDTE3MDMwNDEwNTI0NloXDTI3MDMwNTEw -NTI0NlowgYoxCzAJBgNVBAYTAkdCMRAwDgYDVQQIDAdFbmdsYW5kMREwDwYDVQQK -DAhEQkh1Yi5pbzEnMCUGA1UECwweREJIdWIuaW8gQ2VydGlmaWNhdGUgQXV0aG9y -aXR5MS0wKwYDVQQDDCREQkh1Yi5pbyBERVZFTE9QTUVOVCBJbnRlcm1lZGlhdGUg -Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKOkhcRjOBkxQdzDie -S6dfXIiQ9Qk2iCqfBVGPCFMjNrzUywjLvi0ZVbo7CaR4gveaXwcH94C8sxaqmm+7 -SB3FimcgRfq1fwPS78wKyeAtb37TGu+Xwu/+4l320BwdmaCLx10kjT2pOf29t7MH -qBLF3p4+7Jza5IPL3Ddq4O8iyUEvd3QfHZ2RgzY1M2APezG51DLRUHX7s5d8Bbe8 -HcHmsrWCbJpbPzGCj2C4UiaT5ZCMPLFW+pbUnnZemVriDHekPBSNgo8AdSnnUypm -YgdidlUFdZ8LAMqJYouY6On32+huXWK8QXnWz/TuGnP2mV4KgVPIkM89gymLakAL -x8UX44csekodLsq4xmHnBZq9eDddjRKK/A6iG95GqYW4g3QbnjU56UCe4NlzRRj1 -jATjLvJ3KHG45YyW8qjlr3vvVDNLiYPUs3ZDo4N8qChYFKbGKUljay6JpHjek16g -6Lmy6+8NUopKh+Q7vIH2LLpgh5xNlCRvHHOpE7TR2XtmCOvp2FHCSUqym+inOuvN -CoSwVpooi5y4yvnVmSa9gxAp5AoptGzo/ucEsRvvf7giu7JnWIImYv1xvFOnqN2G -GxRpXujAvrgpH79zdaj3VNUNebUw9PLeEksj4SuoAt3CxPrPnv+QecMXzRjLCgS4 -PAygRceqJfsUl/dRhZTpoZr5ywIDAQABo2YwZDAdBgNVHQ4EFgQUzl/NZNsCDfwl -RFDAZBt/2KfiH+UwHwYDVR0jBBgwFoAUh2SO9T3EiSMToeLLKQLwYRsIryUwEgYD -VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD -ggIBABKx6uDYuvYPrJtfyNpW63ELWukVVNMHojipg4M2dlV+phgooRGqJwE1Nx8s -dQ3lDIIpIqi7eVfkm2SSYsMN3AUHhLnX9VeyWe4ffKs2mHCaQ6nIk+niE01zZC4z -bGHNZwJNkgKa8s2E+iK1Z/QB1QicS9PiQoJHHkLbnS7v0YqowdXgMniU1yqIjmf7 -aWhK0Gt51iXFgVcz0lXHsJkdgl85JW6nN/EB1rtZ+tWDgfBpPL8JObVmr1qMUsNe -nWaAf91DA+3DDWVCCMdtgvTIRc7srjYl7rpBW36Ztijm8fTvEWdB6zVT4BUl4lh9 -mECVV/Gx39oMyCMLq6X0jQ49tAuTjlCYRtQ1vRhKpfO+hJma01WBKPSrtJ3Yiiyk -DFyi3Rs0w8GDN5FXRTfachExt5A1DAsUnxFz3JkdEJnmEgzzipa+FDAOC1JZvgxP -I0GCJaQT60YuSyUsM+IPuedy2izRUImFLWXocn9y0Kbsqn7GY8pUYn4cXO7s2X/2 -PsOQyFHLmAiTrhTISO/58NUzLsudMY9d1V5ymVHXYBDwgRMMVoTQqniU3ArCIRWZ -XgqjSf6psZaXS4/9wf4y6c+/WgkfAMbCGGj7mKLu2a9Lo5zfdCB73ZhEATJbMFwk -KTYOLQO4zJEK2h+hmyqMaKv7EDB0BjLmbnFCcuvWL27aB5FL ------END CERTIFICATE----- diff --git a/src/certs/ca-chain-docker.crt b/src/certs/ca-chain-docker.crt deleted file mode 100644 index ec11f01dbe..0000000000 --- a/src/certs/ca-chain-docker.crt +++ /dev/null @@ -1,72 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIGSjCCBDKgAwIBAgICEAMwDQYJKoZIhvcNAQELBQAwga8xCzAJBgNVBAYTAkdC -MRAwDgYDVQQIDAdFbmdsYW5kMREwDwYDVQQKDAhEQkh1Yi5pbzEnMCUGA1UECwwe -REJIdWIuaW8gQ2VydGlmaWNhdGUgQXV0aG9yaXR5MSwwKgYDVQQDDCNEQkh1Yi5p -byBEb2NrZXIgRGV2ZWxvcG1lbnQgUm9vdCBDQTEkMCIGCSqGSIb3DQEJARYVanVz -dGluQHBvc3RncmVzcWwub3JnMB4XDTIxMDUwMjExMjkxM1oXDTMxMDUwMzExMjkx -M1owgbcxCzAJBgNVBAYTAkdCMRAwDgYDVQQIDAdFbmdsYW5kMREwDwYDVQQKDAhE -Qkh1Yi5pbzEnMCUGA1UECwweREJIdWIuaW8gQ2VydGlmaWNhdGUgQXV0aG9yaXR5 -MTQwMgYDVQQDDCtEQkh1Yi5pbyBEb2NrZXIgRGV2ZWxvcG1lbnQgSW50ZXJtZWRp -YXRlIENBMSQwIgYJKoZIhvcNAQkBFhVqdXN0aW5AcG9zdGdyZXNxbC5vcmcwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDGwn/4kHEK3wBol8/fTLZI15Ie -HH88Vk8ks++S7FTg7GeN3ITNHvvEFQ9qlV3OZRMckvP/UBVbqupnewQjOQO3f6H+ -FW7Pfn9bynZmCb5uaWhzxdqLJye9jSEw57tAkAwXEY2RSFJTYr4UVU3Lmow+Iqj/ -sAOXsPTIKkIIUSzC+khla6eXyZzeK0/uroQQYHGIJRLuihP33xQ520GRpVdLDeKr -JJIw85YitMpdm0RfH1kEDPrQZVtC8XMjpA3G+4BrYcJazO8s+txwQQxgT5SOkvT1 -XNUlGSLMKFvY2Ufy5J1mouGU4H90b82tf30cfyHDRjQlVMHAhN+AJA1jdj/aJBl5 -HUNOB0tpL2OSYymEgyqnDyt1crMKjZU8PEM7LgbeRTHj8p2NeFPMJXOIhsN6gw0W -8+lbau70RPmPyFki0umM45PFomwKdYAO3GnC4vDWyoU1aPA82lzbTKTlbNQj5idM -vrPZW0LCRZ0rnwHyCLqYTItYUlSItVBJnVJ77z1xVi1bNNHBBy8/1etM+LKMWLMu -HmTKZK1dcpOqVZ9oIg3fvL3/8kdkaFVuloj6sU0SpNwDiF839O0sv5a0URrX+23K -wjmtLNmXfZtz6ikSfc87mCZePHRqVejv1lQJYf9a5aw9Xm3FTSJDLPThNttsjCKg -jVDRnvlT22ywtLNOlQIDAQABo2YwZDAdBgNVHQ4EFgQU/sG8zdpEuNy3LzZho0MA -bWZ/qoowHwYDVR0jBBgwFoAUQRp62y8gTl4irm8Q2gz7NNf9WSAwEgYDVR0TAQH/ -BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBAJkN -0y0PnBdC6OwKHEIFXVDauiKvVTdm5/1AGtvE7C1t8cnVb22Jygzaw7TyekYqTaTV -sVW8zXCgNMNvTZOfB17A1jn7zxXjHYi3/IZw5NAzz02SPutxWgetuas4EdDwD9iT -thHxkq2c6/1LaY/ZVHuQvnrIIfec01ZK6LzAlQyD8/v6CIoBTBqEIerVo9YNTimd -l/UF4DVnX17jyZKWJuKqyL8HCC42QqC9smGPGvnE8mVdo0ed40+Dsx82n2vWLNVr -nltNTCww4ryRcmtsuEsdRv1b+MJLJfFVEm9nevXZplAs0XwjEtETvlFWvPQz+zOH -hm6LglP3LXLYIIzHnwSV9e9Qwr4yXAReBZxnfVdUzw0JDZdVUniA7sUSMlRQIg1F -KXU42sT1AcCOuWUaD72MF29xpfOt5pWOM5R76y3xC6bhuRwkneUX9Nf5iHAkwG23 -MwpurAVZiO5VR/LCYuL1vPP2XOmC5a10qbCQzP5hACCTXr5P8xXVtB4tyMZ46Vnw -wnGxZ9bxhiXU8OO/FISWKmb9XFbbiNcJyRFQSjVU1prypvRpbbmwVg9bL1JgemH+ -VQK3XIKr3GHWqAwRVGcNYD4LVLO0HX5kKkeIB2NzfvR8Bxn1WIjJPPZFyc4gAoLg -v57ad/lF+jA0wlW68dB8AGV0HKjxq6b9jAeR+W2h ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIGUTCCBDmgAwIBAgIUROzwAIHBoAT1lP4XR65lSC/C+uEwDQYJKoZIhvcNAQEL -BQAwga8xCzAJBgNVBAYTAkdCMRAwDgYDVQQIDAdFbmdsYW5kMREwDwYDVQQKDAhE -Qkh1Yi5pbzEnMCUGA1UECwweREJIdWIuaW8gQ2VydGlmaWNhdGUgQXV0aG9yaXR5 -MSwwKgYDVQQDDCNEQkh1Yi5pbyBEb2NrZXIgRGV2ZWxvcG1lbnQgUm9vdCBDQTEk -MCIGCSqGSIb3DQEJARYVanVzdGluQHBvc3RncmVzcWwub3JnMB4XDTIxMDUwMjEx -MTk0MVoXDTMxMDUwMzExMTk0MVowga8xCzAJBgNVBAYTAkdCMRAwDgYDVQQIDAdF -bmdsYW5kMREwDwYDVQQKDAhEQkh1Yi5pbzEnMCUGA1UECwweREJIdWIuaW8gQ2Vy -dGlmaWNhdGUgQXV0aG9yaXR5MSwwKgYDVQQDDCNEQkh1Yi5pbyBEb2NrZXIgRGV2 -ZWxvcG1lbnQgUm9vdCBDQTEkMCIGCSqGSIb3DQEJARYVanVzdGluQHBvc3RncmVz -cWwub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAx45dv4Ue1YZ8 -7NxY26n3ccMhmtifRatdyCV0XWIIe8zBYQ3R1qSGIDmrNjNSA2i+DVIcktEsWO+B -xPPGneI0NPv1sKnhWBpC93hVm0q8cCh07ud37BsYZEOu+j04Z3hZmJ+LBKhC9kJA -p8bsZPAJokF1ThHmfMCD8Cr0EntnZdRsY+uVpw0AsHvC2PHmQvs5f2K49R4SwVD6 -ehkKaM5qjb5e+TkeuIg16qKeIwyz0qiL41Z07+THw5JBbkVuThLrGQ721iydpxPR -3CJbRtJemNDdrZCEyvcgq8w6jRe519McmCUCEuYZy1rbodng5MOybUyvnLi5AwzA -EeDsHk18yF0P9JhVmPGg8bSswoscYQk3p821IMZpL4DFVLdA0nHdwZeZoD7fw0TV -p5h4oD5t0tsFR/N9cBEUvunwJ4P1WU1MstLrfQf4YVWzYOoZgl8nBf7zqoGwYN9t -NkTgr4fnWZM8rZTzm93Mpr2nZ2r/7hMzadfrQFPRgR4BYFrZzMutUisLqMG5qMdJ -4/z3qRP6+Gwr8cZjRJw7Pv6dDJes/uq1PHdJ8c4pYR+GGmpT2rM8jHX2n8C2vJFG -x19BEjNaqhY34zonTAA/WrFYcrGamzojqIyRBEcc572E0HWShC053HlMmjMHSukh -6uwQ3U6x2ABAqP76qGaj89skELSi6RkCAwEAAaNjMGEwHQYDVR0OBBYEFEEaetsv -IE5eIq5vENoM+zTX/VkgMB8GA1UdIwQYMBaAFEEaetsvIE5eIq5vENoM+zTX/Vkg -MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUA -A4ICAQAjZKP5sTXPEXdzv65XCBsEj8kVR6SDs3WTVCU/gU2Lylqe/gB0kKRh2C9K -/DafSfsEYx0qwHXt4qRlTU7ih87CY+QnCw5IBaye0wVCofc1FqjP6T7RC7QNhni8 -mUzeAl9eOf6Idex1wzEhgk+lulrJ3igHczLfR+layr8RLY7De9pO23xPIQoV23Yo -/kYJicKBKOs6U/tM4nzoZsiB9yEUIdnOkF8SGWBbYVMOkxt8CYtChHGqJFeEveuc -5uY5Ot1iPrAPVP30JeVpGu6TQmFWNjXo18eqj/lmw5G1iSAAxSfIeHb1njFlw/++ -irpAnICe6ggSSi8IgEeuwlZfNtTAGjXpqU3xFKvv6vg9Y7D4UlA1ln/Xd2P1Ea7t -Yy0UhBs6a4SbEm7Au4m7SwEMA+ImGcbUacgiLX/EDLqUjM02WtwNeXq3dK4bIbo5 -DhvKvYg11LQT1A1XJQopMFRMo6YNnsWXxy3MWq3l6GAqxmet+vslTAuOUdorS/5t -0yhQo1JtjcxZV7m+7TcwvFs+oeseaBXiLHkHM9P5TKsTJ9vKQuohEc0o1+YS+TqA -Oie7rmViMy8+IKE9pFGNjo9KBtRshqDpz9C5z8X4WDpM7lB+EcFKIMbz0P4vPIcZ -KDeX5COGCBbVnqBR7+MD+jU5k0376ShF+BAM3Ob9qF09aw0vMg== ------END CERTIFICATE----- diff --git a/src/certs/public.cert.pem b/src/certs/public.cert.pem deleted file mode 100644 index c27afb5138..0000000000 --- a/src/certs/public.cert.pem +++ /dev/null @@ -1,54 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEmjCCAoKgAwIBAgIQRKcXCIHx/VM6mM4J+w/BnTANBgkqhkiG9w0BAQsFADCB -ijELMAkGA1UEBhMCR0IxEDAOBgNVBAgMB0VuZ2xhbmQxETAPBgNVBAoMCERCSHVi -LmlvMScwJQYDVQQLDB5EQkh1Yi5pbyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxLTAr -BgNVBAMMJERCSHViLmlvIERFVkVMT1BNRU5UIEludGVybWVkaWF0ZSBDQTAeFw0y -MTA1MDIxMzM0MjFaFw0yNjA1MDIxMzM0MjFaMD8xHjAcBgNVBAoTFURCIEJyb3dz -ZXIgZm9yIFNRTGl0ZTEdMBsGA1UEAwwUcHVibGljQGRiNHMuZGJodWIuaW8wggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtS1Ik94JMX6bZJArL6xPddEZv -yhAyv5BFKSJwm+NjvGQNR0guAZSeWRBVn/pQLvfGQnY+DkX7CQ5Mp6jJARRMjM8+ -MQXkbsoZ3kyFTrMjoezEIoiNI7ndyAwauk1Hh2VfjQ6Ce2FMfFGYZk48dd3m4bvr -nPgdcDnnBFIQ6Np/W76bHSUhkNRivcFyGgWWBCKYlLmMBviN7an9D4zBex7LhL37 -FFCrGgo9vs0qLtX8m5Chtdlvj18CVOvYgSGvqRIj5y2peF+jAgbtyTtWmnT9iqK3 -cBudd2PiWk1l4SvEcR1+ULsyw4LReSin4aHrpZ45WLLBsQICTtcPlqSVrr+tAgMB -AAGjRjBEMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHwYDVR0j -BBgwFoAUzl/NZNsCDfwlRFDAZBt/2KfiH+UwDQYJKoZIhvcNAQELBQADggIBAA16 -XYqgzJFZjcrYC2lhpOEgmwlavBMwEP4e/ly37+PbsirIak4bNVyY0zw6tEZCF55r -Rij2LNgaxFJnrZrWdP7yFua9wzf4wQOd+dLLHE9K8E4mAAz8wuof+C0J9nljmD66 -6p3HH6X5TrDaMBILQB4moWrjgGBfey2DufFd8w05M1vPIWisZiUN6gkrAImhGLBt -aXEnCKY5Kby4+SHv2ks+yk7U/cDIOOSwQU2Sgc2gaUM3joofsab5JsChyQrQbEDd -yIM1PtpvZCvyTjlrBb1ic+TlRgDtRCDOEBL7uHgaHJp1s+XZ0axLx9U+qYuBS0P9 -qDTxpewB3JSLZfUu4noeW5Oe9MhiRky7tPTAADehjeuYYWiCBtL1F73x21MUoey9 -+yCnHuWOrr6iMlG7R2Joa+KPZZ1RnMdaH42/DxYzB5rEymqvNQQvwsQJ1KIb3gVp -72AHYXiIbcw0EtDU2Js6K6KfNd14wQmBcyIhfY6NT3GUrFuVamWLiEQZh8H6fBn+ -iSv3QeDF1TJxJ8G88BJbwm+gAsQTL4oyVzZSAFvuGwE1ODmNLm8J52jzsuduTWeb -30/dBeFXL/TC6xw8BsXMjP92CDTI9RM/rpe+b0m+StEOp27k8i6qg/ozTCpdf4dH -XOjJ0KVw3giAvF8+p6WiZnm2UJPqS9hVP9QDBsZR ------END CERTIFICATE----- ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEA7UtSJPeCTF+m2SQKy+sT3XRGb8oQMr+QRSkicJvjY7xkDUdI -LgGUnlkQVZ/6UC73xkJ2Pg5F+wkOTKeoyQEUTIzPPjEF5G7KGd5MhU6zI6HsxCKI -jSO53cgMGrpNR4dlX40OgnthTHxRmGZOPHXd5uG765z4HXA55wRSEOjaf1u+mx0l -IZDUYr3BchoFlgQimJS5jAb4je2p/Q+MwXsey4S9+xRQqxoKPb7NKi7V/JuQobXZ -b49fAlTr2IEhr6kSI+ctqXhfowIG7ck7Vpp0/Yqit3AbnXdj4lpNZeErxHEdflC7 -MsOC0Xkop+Gh66WeOViywbECAk7XD5akla6/rQIDAQABAoIBABz90dhwYPwBx7nJ -3IPTgcbRuPVZg6cfnnnEr8+ATETYNIUmy1vLl6PND+DWPdfkFSOk5Rtp1QT5s0SM -S2PbfOZpYbygcq7ZFloYvdKfreVRmELSUUqlLcU59rihQGXX1fvZmQc6GcYlfjaX -cUGTyPu3YqVDtVCrcHeYIIquWFus1p/MdQg1kDcr/vRAm3A5Evn4zv3D+jN0SX/h -ZO0D3PejDy/TBFEIQnn0c8XKRkVuVAeA8ZRB+lLsOXDzTpDf2QNVOscS/icQ0cXR -1jnGRlr3zATgO1fSh17cWCZgNVNxtijJOE1avHXSGxZMPjkSC8cK4IzfcteplEex -G5kOfCECgYEA9U2NhIyomrUpYb6pnCdHQHTV/Ew2s8ud3wBKAtrmAW9lIHn1j1kX -/I/C0sNuyAaU5Zh81gHcx3KcuwHooTWUVf56FNKqxElafpwCTQTXF0dF3uycVopq -Mt60fntD0LqQ1EhEpnYjIOuu1f4+2Y3o2URUovRVIwbajQ06UFKqi5kCgYEA96Rc -zBMEiffgNRszzYKsBxkKk/ysXXWovZXz998TZ51o1jn50q35HfJPj8yEAlUn1yF9 -htvrWxLRuYuIkDOJmKbG94O3F14p0tuWx5TrnVxLYikCQAl++UZJWEb4a+YhVGra -VGgfioQpYPD0dF2Z1CxGSe9w6u+s0UYxYaceQTUCgYAZR67H9D8EApuLKT0NjLa5 -G9FZuPkCOn/AlaUK+kgt6a8AU9FMvp/MNXycf+uQzazWpIpo+7QnEda5Jw80XfM1 -kY4/Sx6yL+UVmzpZeaA5E+1NdhD+kjEzoOP1DPsGLdPzLKd2iEJCiEdyYktT3F4c -1f/q80c63t/zHWPfF0XgMQKBgHQwpO2HGsEVERg5rCOHZOlroV/v1HuBQeu52J77 -BmK+IzsoNoPX5qNbVmMUxPdHNwskBn5o3tN7T/Vrd2aZF70MuxvUq+oF3z+0kdkQ -kT/i1ue2b/zVt8KDbNRDcDlH32l5PPkPZYUbH0MBquCSLiOzpkL4WhWQ4JfMBE1a -GMLlAoGAOOYr6tCW+MP9PYWWqmoIxwX4q3a72mPkf9Yk2JzMWb0LFuJltXwOx8az -K0c1sviqvzTK75KEHxYYK0ISCsFcEqD/Jf3EYs2v3ORuDvEykSAc4pj3LS4EgoVP -XvP1QYG8f8W5uSB8O3HzoTXVYtAeVtsdvdipzDcnWH+wSi/3EPQ= ------END RSA PRIVATE KEY----- diff --git a/src/extensions/extension-formats.c b/src/extensions/extension-formats.c index 53306b12f8..3a7bddbfe5 100644 --- a/src/extensions/extension-formats.c +++ b/src/extensions/extension-formats.c @@ -69,7 +69,7 @@ typedef struct CONFIG { unsigned char *outputBuffer; } CONFIG; -int createObject(int type, int length, int extra, OBJECT **ptr2); +int createObject(int type, int length, size_t extra, OBJECT **ptr2); int readObject(CONFIG *cfg, long offset, OBJECT **ptr2); static int indent = 2; @@ -157,16 +157,40 @@ int readTrailer(CONFIG *cfg) return ERROR_NONE; } -int createObject(int type, int length, int extra, OBJECT **ptr2) -{ - *ptr2 = NULL; - OBJECT *ptr = (OBJECT *)malloc(sizeof(OBJECT) + (size_t)extra); - if (ptr == NULL) - return ERROR_INSUFFICIENT_MEMORY; - ptr->type = type; - ptr->length = length; - *ptr2 = ptr; - return ERROR_NONE; +int createObject(int type, int length, size_t extra, OBJECT **ptr2) { + OBJECT *ptr; + size_t totalSize; + + // Calculate proper extra size based on type + size_t actualExtra; + switch (type) { + case 0x06: // UTF16 string + actualExtra = 2 * (length + 1); // 2 bytes per char + null terminator + break; + case 0x05: // Regular string + actualExtra = length + 1; // bytes + null terminator + break; + case 0x0A: // Array + actualExtra = length * sizeof(OBJECT*); + break; + case 0x0D: // Dictionary + actualExtra = (length - 1) * sizeof(KEY); + break; + default: + actualExtra = extra; + break; + } + + totalSize = sizeof(OBJECT) + actualExtra; + + ptr = malloc(totalSize); + if (ptr == NULL) + return ERROR_INSUFFICIENT_MEMORY; + + ptr->type = type; + ptr->length = length; + *ptr2 = ptr; + return ERROR_NONE; } long readInteger(unsigned char *ptr, int desc) @@ -336,14 +360,22 @@ int readObject(CONFIG *cfg, long offset, OBJECT **ptr2) obj->data.text[length] = '\0'; break; case 0x06: // UTF16 string - err = createObject(type, length, 2 * length, &obj); - if (err != ERROR_NONE) - return err; - for (i=0; i < length; i++) { - short int d = *(ptr++); - obj->data.utf16[i] = (short int)((d << 8) | *(ptr++)); + { + size_t bytesNeeded = 2 * ((size_t)length + 1); + err = createObject(type, length, bytesNeeded, &obj); + if (err != ERROR_NONE) + return err; + + memset(obj->data.utf16, 0, bytesNeeded); + + for (i = 0; i < length; i++) { + unsigned char byte1 = *(ptr++); + unsigned char byte2 = *(ptr++); + obj->data.utf16[i] = (byte1 << 8) | byte2; + } + + obj->data.utf16[length] = 0; } - obj->data.utf16[length] = '\0'; break; case 0x08: // UID err = createObject(type, length, 0, &obj); @@ -517,6 +549,8 @@ int displayObject(CONFIG *cfg, OBJECT *obj, int raw) int releaseObject(OBJECT *obj) { + if (!obj) return ERROR_NONE; // Guard against NULL + int i; switch (obj->type) { case 0x00: // Singleton @@ -529,16 +563,29 @@ int releaseObject(OBJECT *obj) case 0x08: // UID break; case 0x0A: // Array - for (i=0; i < obj->length; i++) - releaseObject(obj->data.objects[i]); + for (i=0; i < obj->length; i++) { + if (obj->data.objects[i]) { + releaseObject(obj->data.objects[i]); + obj->data.objects[i] = NULL; + } + } break; case 0x0D: // Dictionary for (i=0; i < obj->length; i++) { - releaseObject(obj->data.keys[i].key); - releaseObject(obj->data.keys[i].value); + if (obj->data.keys[i].key) { + releaseObject(obj->data.keys[i].key); + obj->data.keys[i].key = NULL; + } + if (obj->data.keys[i].value) { + releaseObject(obj->data.keys[i].value); + obj->data.keys[i].value = NULL; + } } break; + default: + break; } + free(obj); return ERROR_NONE; } @@ -767,72 +814,81 @@ void freeResult(void *ptr) return; } -static void plistFunc(sqlite3_context *context, int argc, sqlite3_value **argv){ - int resultLength; - int errorCode = 0; - char *result = NULL; - assert( argc==1 ); - switch( sqlite3_value_type(argv[0]) ){ - case SQLITE_TEXT: - case SQLITE_BLOB: { - const char *data = sqlite3_value_text(argv[0]); - const int dataLength = sqlite3_value_bytes(argv[0]); - errorCode = parsePlist(&result, data, dataLength); - if (errorCode == ERROR_NONE) { - resultLength = strlen(result); - sqlite3_result_text(context, result, resultLength, &freeResult); - } else { - if (sqlite3_value_type(argv[0]) == SQLITE_TEXT) - sqlite3_result_text(context, data, dataLength, NULL); - else - sqlite3_result_blob(context, data, dataLength, NULL); - } - break; +static void plistFunc(sqlite3_context *context, int argc, sqlite3_value **argv) { + int errorCode = 0; + char *result = NULL; + assert(argc==1); + + if (sqlite3_value_type(argv[0]) == SQLITE_NULL) { + sqlite3_result_null(context); + return; } - default: { - sqlite3_result_null(context); - break; + + const unsigned char *rawData = sqlite3_value_blob(argv[0]); + const int dataLength = sqlite3_value_bytes(argv[0]); + + // Check for valid plist header + if (!rawData || dataLength < 8 || memcmp(rawData, "bplist00", 8) != 0) { + // Not a plist, return original value + if (sqlite3_value_type(argv[0]) == SQLITE_TEXT) { + sqlite3_result_text(context, (const char *)rawData, dataLength, NULL); + } else { + sqlite3_result_blob(context, rawData, dataLength, NULL); + } + return; } - } -} -static void encodeBase64Func(sqlite3_context *context, int argc, sqlite3_value **argv){ - int resultLength; - int errorCode = 0; - int dataLength; - const unsigned char *data; - char *result = NULL; - assert( argc==1 ); - switch( sqlite3_value_type(argv[0]) ){ - case SQLITE_BLOB: - data = sqlite3_value_blob(argv[0]); - dataLength = sqlite3_value_bytes(argv[0]); - errorCode = encodeBase64(&result, data, dataLength); - if (errorCode == ERROR_NONE) { - resultLength = strlen(result); + errorCode = parsePlist(&result, (const char *)rawData, dataLength); + if (errorCode == ERROR_NONE) { + int resultLength = strlen(result); sqlite3_result_text(context, result, resultLength, &freeResult); - } else { - sqlite3_result_blob(context, data, dataLength, NULL); - } - break; - case SQLITE_TEXT: { - data = sqlite3_value_text(argv[0]); - dataLength = sqlite3_value_bytes(argv[0]); - sqlite3_result_text(context, data, dataLength, NULL); - errorCode = encodeBase64(&result, data, dataLength); - if (errorCode == ERROR_NONE) { - resultLength = strlen(result); - sqlite3_result_text(context, result, resultLength, &freeResult); - } else { - sqlite3_result_text(context, data, dataLength, NULL); - } - break; + } else { + // On error, return original value + if (sqlite3_value_type(argv[0]) == SQLITE_TEXT) { + sqlite3_result_text(context, (const char *)rawData, dataLength, NULL); + } else { + sqlite3_result_blob(context, rawData, dataLength, NULL); + } } - default: { - sqlite3_result_null(context); - break; +} + +static void encodeBase64Func(sqlite3_context *context, int argc, sqlite3_value **argv) { + int resultLength; + int errorCode = 0; + int dataLength; + const unsigned char *rawData; + char *result = NULL; + assert(argc==1); + switch(sqlite3_value_type(argv[0])) { + case SQLITE_BLOB: + rawData = sqlite3_value_blob(argv[0]); + dataLength = sqlite3_value_bytes(argv[0]); + errorCode = encodeBase64(&result, rawData, dataLength); + if (errorCode == ERROR_NONE) { + resultLength = strlen(result); + sqlite3_result_text(context, result, resultLength, &freeResult); + } else { + sqlite3_result_blob(context, rawData, dataLength, NULL); + } + break; + case SQLITE_TEXT: { + rawData = sqlite3_value_text(argv[0]); + dataLength = sqlite3_value_bytes(argv[0]); + sqlite3_result_text(context, (const char *)rawData, dataLength, NULL); + errorCode = encodeBase64(&result, rawData, dataLength); + if (errorCode == ERROR_NONE) { + resultLength = strlen(result); + sqlite3_result_text(context, result, resultLength, &freeResult); + } else { + sqlite3_result_text(context, (const char *)rawData, dataLength, NULL); + } + break; + } + default: { + sqlite3_result_null(context); + break; + } } - } } @@ -917,17 +973,17 @@ static void decodeBase64Func(sqlite3_context *context, int argc, sqlite3_value * switch( sqlite3_value_type(argv[0]) ){ case SQLITE_BLOB: case SQLITE_TEXT: { - const char *data = sqlite3_value_text(argv[0]); + const char *data = (const char *)sqlite3_value_text(argv[0]); int dataLength = sqlite3_value_bytes(argv[0]); errorCode = decodeBase64(&result, &resultLength, data, dataLength); if (errorCode == ERROR_NONE) { if (isUTF8(result, resultLength)) - sqlite3_result_text(context, result, resultLength, &freeResult); + sqlite3_result_text(context, (const char *)result, resultLength, &freeResult); else - sqlite3_result_blob(context, result, resultLength, &freeResult); + sqlite3_result_blob(context, result, resultLength, &freeResult); } else { if (sqlite3_value_type(argv[0]) == SQLITE_TEXT) - sqlite3_result_text(context, data, dataLength, NULL); + sqlite3_result_text(context, (const char *)data, dataLength, NULL); else sqlite3_result_blob(context, data, dataLength, NULL); } @@ -940,16 +996,713 @@ static void decodeBase64Func(sqlite3_context *context, int argc, sqlite3_value * } } +int isJsonString(const char *text) { + if (!text) return 0; + int len = strlen(text); + if (len < 2) return 0; + + // Check if it starts with [ or { or " + return (text[0] == '[' || text[0] == '{' || (text[0] == '"' && text[1] != '{')); +} + +int outputJsonEscapedText(CONFIG *cfg, const char *text) { + int err = ERROR_NONE; + + // Check if text is a JSON string that starts with { + if (text && (text[0] == '{' || (text[0] == '"' && text[1] != '{'))) { + // Try to unescape the JSON string + char *unescaped = malloc(strlen(text) + 1); + if (!unescaped) return ERROR_INSUFFICIENT_MEMORY; + + const char *src = text; + char *dst = unescaped; + + while (*src) { + if (*src == '\\' && *(src + 1) == '"') { + *dst++ = '"'; + src += 2; + } else { + *dst++ = *src++; + } + } + *dst = '\0'; + + // Output the unescaped JSON directly + err = outputText(cfg, unescaped); + free(unescaped); + return err; + } + + // Original escaping logic for regular strings + while (*text && err == ERROR_NONE) { + switch (*text) { + case '"': + err = outputText(cfg, "\\\""); + break; + case '\\': + err = outputText(cfg, "\\\\"); + break; + case '\b': + err = outputText(cfg, "\\b"); + break; + case '\f': + err = outputText(cfg, "\\f"); + break; + case '\n': + err = outputText(cfg, "\\n"); + break; + case '\r': + err = outputText(cfg, "\\r"); + break; + case '\t': + err = outputText(cfg, "\\t"); + break; + default: + if ((unsigned char)*text < 32) { + char escaped[8]; + sprintf(escaped, "\\u%04x", (unsigned char)*text); + err = outputText(cfg, escaped); + } else { + char c[2] = {*text, 0}; + err = outputText(cfg, c); + } + } + text++; + } + return err; +} + +char* utf16_to_utf8(const short int* utf16, int length) { + char* utf8 = malloc(length * 3 + 1); // Max 3 bytes per char + null terminator + if (!utf8) return NULL; + + int j = 0; + for (int i = 0; i < length; i++) { + unsigned short uc = utf16[i]; + + if (uc < 0x80) { + utf8[j++] = (char)uc; + } else if (uc < 0x800) { + utf8[j++] = (char)(0xC0 | (uc >> 6)); + utf8[j++] = (char)(0x80 | (uc & 0x3F)); + } else { + utf8[j++] = (char)(0xE0 | (uc >> 12)); + utf8[j++] = (char)(0x80 | ((uc >> 6) & 0x3F)); + utf8[j++] = (char)(0x80 | (uc & 0x3F)); + } + } + + utf8[j] = '\0'; + return utf8; +} + +// Add new display function for JSON format +int displayObjectAsJson(CONFIG *cfg, OBJECT *obj, int raw) { + char text[32]; + switch (obj->type) { + case 0x00: // Singleton + if (strcmp(obj->data.text, "") == 0) + outputText(cfg, "true"); + else if (strcmp(obj->data.text, "") == 0) + outputText(cfg, "false"); + else if (strcmp(obj->data.text, "") == 0) + outputText(cfg, "null"); + else + outputText(cfg, "null"); + break; + case 0x01: // Integer + sprintf(text, "%ld", obj->data.integer); + outputText(cfg, text); + break; + case 0x02: // Float + sprintf(text, "%f", obj->data.real); + outputText(cfg, text); + break; + case 0x03: // Date + outputText(cfg, "\""); + outputText(cfg, ctime(&(obj->data.date))); + if (cfg->outputBuffer[cfg->outputBufferIn-1] == '\n') + cfg->outputBufferIn--; // Remove trailing newline + outputText(cfg, "\""); + break; + case 0x04: // Binary data + outputText(cfg, "\"\""); + break; + case 0x05: // String + if (isJsonString(obj->data.text)) { + // If it's already JSON, output it directly + outputText(cfg, obj->data.text); + } else { + // Otherwise treat as regular string + outputText(cfg, "\""); + outputJsonEscapedText(cfg, obj->data.text); + outputText(cfg, "\""); + } + break; + case 0x06: // UTF16 string + { + char* utf8 = utf16_to_utf8(obj->data.utf16, obj->length); + if (utf8) { + if (isJsonString(utf8)) { + // If it's already JSON, output directly + outputText(cfg, utf8); + } else { + // Otherwise wrap in quotes and escape + outputText(cfg, "\""); + outputJsonEscapedText(cfg, utf8); + outputText(cfg, "\""); + } + free(utf8); + } else { + outputText(cfg, "\"\""); + } + } + break; + case 0x08: // UID + sprintf(text, "%lu", obj->data.uid); + outputText(cfg, text); + break; + case 0x0A: // Array + outputText(cfg, "["); + for (int i=0; i < obj->length; i++) { + displayObjectAsJson(cfg, obj->data.objects[i], 0); + if (i < obj->length - 1) + outputText(cfg, ","); + } + outputText(cfg, "]"); + break; + case 0x0D: // Dictionary + outputText(cfg, "{"); + for (int i=0; i < obj->length; i++) { + // Check if key is already JSON + if (!isJsonString(obj->data.keys[i].key->data.text)) { + outputText(cfg, "\""); + displayObject(cfg, obj->data.keys[i].key, 1); + outputText(cfg, "\""); + } else { + displayObject(cfg, obj->data.keys[i].key, 1); + } + outputText(cfg, ":"); + displayObjectAsJson(cfg, obj->data.keys[i].value, 0); + if (i < obj->length - 1) + outputText(cfg, ","); + } + outputText(cfg, "}"); + break; + default: + sprintf(text, "\"\"", obj->type); + outputText(cfg, text); + break; + } + return ERROR_NONE; +} + +// Add new function to parse plist to JSON +int parsePlistToJson(char **result, const char *data, int dataLength) { + CONFIG cfg; + char *ptr; + OBJECT *obj; + int err = ERROR_NONE; + long length; + + // Determine the file size and save + cfg.buffer = (unsigned char *)data; + cfg.bufferLength = dataLength; + + // Preset the output buffer + cfg.outputBufferLength = 0; + cfg.outputBufferIn = 0; + cfg.outputBuffer = NULL; + + // Read the header + err = readHeader(&cfg); + if (err != ERROR_NONE) + return err; + + // Read the trailer + err = readTrailer(&cfg); + if (err != ERROR_NONE) + return err; + + // Locate and read the root object + long offset = cfg.offsetTable[cfg.rootObjectReference]; + err = readObject(&cfg, offset, &obj); + + // If no error display the root object and hence the whole object tree + if (err != ERROR_NONE) + return err; + + displayObjectAsJson(&cfg, obj, 0); + + // Create return data + length = strlen((const char *)(cfg.outputBuffer)); + ptr = malloc(length + 1); + *result = ptr; + if (ptr != NULL) { + for (int i=0; i < length; i++) + *(ptr++) = cfg.outputBuffer[i]; + *ptr = '\0'; + } + else + err = ERROR_INSUFFICIENT_MEMORY; + + // Release assigned memory + releaseObject(obj); + free(cfg.offsetTable); + free(cfg.outputBuffer); + return err; +} + +// Add the new function to SQLite +static void plistToJsonFunc(sqlite3_context *context, int argc, sqlite3_value **argv) { + int errorCode = 0; + char *result = NULL; + assert(argc==1); + + if (sqlite3_value_type(argv[0]) == SQLITE_NULL) { + sqlite3_result_null(context); + return; + } + + const unsigned char *rawData = sqlite3_value_blob(argv[0]); + const int dataLength = sqlite3_value_bytes(argv[0]); + + // Check for valid plist header + if (!rawData || dataLength < 8 || memcmp(rawData, "bplist00", 8) != 0) { + // Not a plist, return original value + if (sqlite3_value_type(argv[0]) == SQLITE_TEXT) { + sqlite3_result_text(context, (const char *)rawData, dataLength, NULL); + } else { + sqlite3_result_blob(context, rawData, dataLength, NULL); + } + return; + } + + errorCode = parsePlistToJson(&result, (const char *)rawData, dataLength); + if (errorCode == ERROR_NONE) { + int resultLength = strlen(result); + sqlite3_result_text(context, result, resultLength, &freeResult); + } else { + // On error, return original value + if (sqlite3_value_type(argv[0]) == SQLITE_TEXT) { + sqlite3_result_text(context, (const char *)rawData, dataLength, NULL); + } else { + sqlite3_result_blob(context, rawData, dataLength, NULL); + } + } +} + +// Add these helper functions for NSKeyedArchiver deserialization +int isNSKeyedArchiver(OBJECT *obj) { + if (obj->type != 0x0D) return 0; + + for (int i = 0; i < obj->length; i++) { + OBJECT *key = obj->data.keys[i].key; + if (key->type == 0x05 && strcmp(key->data.text, "$archiver") == 0) { + OBJECT *value = obj->data.keys[i].value; + if (value->type == 0x05 && strcmp(value->data.text, "NSKeyedArchiver") == 0) { + return 1; + } + } + } + return 0; +} + +OBJECT* findObjectByUID(CONFIG *cfg, int uid) { + if (uid >= cfg->objectCount) + return NULL; + long offset = cfg->offsetTable[uid]; + OBJECT *obj; + if (readObject(cfg, offset, &obj) != ERROR_NONE) + return NULL; + return obj; +} + +// Add this helper function to get actual object from reference +OBJECT* resolveReference(CONFIG *cfg, OBJECT *objects, OBJECT *ref) { + if (!ref || !objects) return NULL; + + if (ref->type == 0x01) { // Direct integer reference + int index = ref->data.integer; + if (index >= 0 && index < objects->length) { + return objects->data.objects[index]; + } + } + return ref; // Return original if not a reference +} + +// Add this helper function to get the actual value from an index +OBJECT* getObjectAtIndex(OBJECT *objects, int index) { + if (!objects || objects->type != 0x0A || index < 0 || index >= objects->length) { + return NULL; + } + return objects->data.objects[index]; +} + +// Helper function to get object from $objects array by index +OBJECT* getObjectByIndex(OBJECT *objects, int index) { + if (!objects || objects->type != 0x0A || index < 0 || index >= objects->length) { + return NULL; + } + return objects->data.objects[index]; +} + +// Helper function to get value from dictionary by key +OBJECT* getDictValue(OBJECT *dict, const char *key) { + if (!dict || dict->type != 0x0D || !key) return NULL; + + for (int i = 0; i < dict->length; i++) { + OBJECT *k = dict->data.keys[i].key; + if (k && k->type == 0x05 && strcmp(k->data.text, key) == 0) { + return dict->data.keys[i].value; + } + } + return NULL; +} + +// Helper function to get integer value from object +int getIntegerValue(OBJECT *obj) { + if (!obj) return -1; + + switch (obj->type) { + case 0x01: // Integer + return obj->data.integer; + case 0x08: // UID + return (int)obj->data.uid; + default: + return -1; + } +} + +OBJECT* resolveObject(OBJECT *objects, int index) { + if (!objects || index < 0 || index >= objects->length) { + return NULL; + } + + OBJECT *obj = getObjectByIndex(objects, index); + if (!obj) return NULL; + + // If it's a dictionary with NS.keys and NS.objects, resolve it + if (obj->type == 0x0D) { + OBJECT *nsKeys = getDictValue(obj, "NS.keys"); + OBJECT *nsObjects = getDictValue(obj, "NS.objects"); + + if (nsKeys && nsObjects && + nsKeys->type == 0x0A && nsObjects->type == 0x0A && + nsKeys->length == nsObjects->length && + nsKeys->length > 0) { + + OBJECT *result; + int err = createObject(0x0D, nsKeys->length, (nsKeys->length - 1) * sizeof(KEY), &result); + if (err != ERROR_NONE || !result) { + return NULL; + } + + for (int i = 0; i < nsKeys->length; i++) { + int keyIndex = getIntegerValue(nsKeys->data.objects[i]); + int valueIndex = getIntegerValue(nsObjects->data.objects[i]); + + if (keyIndex >= 0 && keyIndex < objects->length && + valueIndex >= 0 && valueIndex < objects->length) { + + OBJECT *key = getObjectByIndex(objects, keyIndex); + if (key) { + size_t extraSize = 0; + switch (key->type) { + case 0x06: // UTF16 string + extraSize = key->length * 2; // 2 bytes per character + break; + case 0x05: // Regular string + extraSize = key->length + 1; // Include null terminator + break; + case 0x0A: // Array + extraSize = key->length * sizeof(OBJECT*); + break; + case 0x0D: // Dictionary + extraSize = key->length * sizeof(KEY); + break; + default: + extraSize = key->length; + break; + } + + err = createObject(key->type, key->length, extraSize, &result->data.keys[i].key); + if (err == ERROR_NONE && result->data.keys[i].key) { + if (key->type == 0x06) { + memcpy(result->data.keys[i].key->data.utf16, key->data.utf16, key->length * 2); + } else { + memcpy(&result->data.keys[i].key->data, &key->data, extraSize); + } + } + + result->data.keys[i].value = resolveObject(objects, valueIndex); + } + } + } + return result; + } + } + + OBJECT *copy; + size_t extraSize = 0; + + switch (obj->type) { + case 0x06: // UTF16 string + extraSize = obj->length * 2; // 2 bytes per character + break; + case 0x05: // Regular string + extraSize = obj->length + 1; // Include null terminator + break; + case 0x0A: // Array + extraSize = obj->length * sizeof(OBJECT*); + break; + case 0x0D: // Dictionary + extraSize = obj->length * sizeof(KEY); + break; + default: + extraSize = obj->length; + break; + } + + int err = createObject(obj->type, obj->length, extraSize, ©); + if (err == ERROR_NONE && copy) { + if (obj->type == 0x06) { + memcpy(copy->data.utf16, obj->data.utf16, obj->length * 2); + } else { + memcpy(©->data, &obj->data, extraSize); + } + } + return copy; +} + +OBJECT* convertNSKeyedArchiver(OBJECT *archiver) { + if (!archiver || archiver->type != 0x0D) { + return NULL; + } + + // Get $objects array + OBJECT *objects = getDictValue(archiver, "$objects"); + if (!objects || objects->type != 0x0A || objects->length == 0) { + return NULL; + } + + // Get root object reference from $top + OBJECT *top = getDictValue(archiver, "$top"); + if (!top || top->type != 0x0D) { + return NULL; + } + + OBJECT *rootRef = getDictValue(top, "root"); + if (!rootRef) { + return NULL; + } + + int rootIndex = getIntegerValue(rootRef); + if (rootIndex < 0 || rootIndex >= objects->length) { + return NULL; + } + + // Get actual root object + OBJECT *root = getObjectByIndex(objects, rootIndex); + if (!root) { + return NULL; + } + + // If root is a dictionary with NS.keys and NS.objects, create new dict + if (root->type == 0x0D) { + OBJECT *nsKeys = getDictValue(root, "NS.keys"); + OBJECT *nsObjects = getDictValue(root, "NS.objects"); + + if (nsKeys && nsObjects && + nsKeys->type == 0x0A && nsObjects->type == 0x0A && + nsKeys->length == nsObjects->length && + nsKeys->length > 0) { + + OBJECT *result; + int err = createObject(0x0D, nsKeys->length, (nsKeys->length - 1) * sizeof(KEY), &result); + if (err != ERROR_NONE || !result) { + return NULL; + } + + for (int i = 0; i < nsKeys->length; i++) { + int keyIndex = getIntegerValue(nsKeys->data.objects[i]); + int valueIndex = getIntegerValue(nsObjects->data.objects[i]); + + if (keyIndex >= 0 && keyIndex < objects->length && + valueIndex >= 0 && valueIndex < objects->length) { + + OBJECT *key = getObjectByIndex(objects, keyIndex); + if (key) { + size_t extraSize = 0; + switch (key->type) { + case 0x06: // UTF16 string + extraSize = key->length * 2; // 2 bytes per character + break; + case 0x05: // Regular string + extraSize = key->length + 1; // Include null terminator + break; + case 0x0A: // Array + extraSize = key->length * sizeof(OBJECT*); + break; + case 0x0D: // Dictionary + extraSize = key->length * sizeof(KEY); + break; + default: + extraSize = key->length; + break; + } + + err = createObject(key->type, key->length, extraSize, &result->data.keys[i].key); + if (err == ERROR_NONE && result->data.keys[i].key) { + if (key->type == 0x06) { + memcpy(result->data.keys[i].key->data.utf16, key->data.utf16, key->length * 2); + } else { + memcpy(&result->data.keys[i].key->data, &key->data, extraSize); + } + } + + result->data.keys[i].value = resolveObject(objects, valueIndex); + } + } + } + return result; + } + } + + return resolveObject(objects, rootIndex); +} + +// Add new function for deserialized JSON conversion +int parsePlistDeserializedToJson(char **result, const char *data, int dataLength) { + CONFIG cfg; + char *ptr; + OBJECT *obj; + int err = ERROR_NONE; + long length; + + // Determine the file size and save + cfg.buffer = (unsigned char *)data; + cfg.bufferLength = dataLength; + + // Preset the output buffer + cfg.outputBufferLength = 0; + cfg.outputBufferIn = 0; + cfg.outputBuffer = NULL; + + // Read the header + err = readHeader(&cfg); + if (err != ERROR_NONE) + return err; + + // Read the trailer + err = readTrailer(&cfg); + if (err != ERROR_NONE) + return err; + + // Locate and read the root object + long offset = cfg.offsetTable[cfg.rootObjectReference]; + err = readObject(&cfg, offset, &obj); + + // If no error display the root object and hence the whole object tree + if (err != ERROR_NONE) + return err; + + // Check if this is an NSKeyedArchiver plist + if (isNSKeyedArchiver(obj)) { + OBJECT *deserializedObj = convertNSKeyedArchiver(obj); + if (deserializedObj) { + // Free original object and use deserialized version + releaseObject(obj); + obj = deserializedObj; + } + } + + displayObjectAsJson(&cfg, obj, 0); + + // Create return data + length = strlen((const char *)(cfg.outputBuffer)); + ptr = malloc(length + 1); + *result = ptr; + if (ptr != NULL) { + for (int i=0; i < length; i++) + *(ptr++) = cfg.outputBuffer[i]; + *ptr = '\0'; + } + else + err = ERROR_INSUFFICIENT_MEMORY; + + // Release assigned memory + releaseObject(obj); + free(cfg.offsetTable); + free(cfg.outputBuffer); + return err; +} + +// Add the new function to SQLite +static void plistDeserializedToJsonFunc(sqlite3_context *context, int argc, sqlite3_value **argv) { + int errorCode = 0; + char *result = NULL; + assert(argc==1); + + if (sqlite3_value_type(argv[0]) == SQLITE_NULL) { + sqlite3_result_null(context); + return; + } + + const unsigned char *rawData = sqlite3_value_blob(argv[0]); + const int dataLength = sqlite3_value_bytes(argv[0]); + + // Check for valid plist header + if (!rawData || dataLength < 8 || memcmp(rawData, "bplist00", 8) != 0) { + // Not a plist, return original value + if (sqlite3_value_type(argv[0]) == SQLITE_TEXT) { + sqlite3_result_text(context, (const char *)rawData, dataLength, NULL); + } else { + sqlite3_result_blob(context, rawData, dataLength, NULL); + } + return; + } + + errorCode = parsePlistDeserializedToJson(&result, (const char *)rawData, dataLength); + if (errorCode == ERROR_NONE) { + int resultLength = strlen(result); + sqlite3_result_text(context, result, resultLength, &freeResult); + } else { + // On error, return original value + if (sqlite3_value_type(argv[0]) == SQLITE_TEXT) { + sqlite3_result_text(context, (const char *)rawData, dataLength, NULL); + } else { + sqlite3_result_blob(context, rawData, dataLength, NULL); + } + } +} + +static void load_formats_builtin(sqlite3_context *context, int argc, sqlite3_value **argv) { + // Return format: "Display Name|internal_name|function_name" + const char *result = "Plist as JSON|plistjson|plistToJson\n" + "Plist as JSON (Deserialized)|plistdesjson|plistDeserializedToJson\n" + "Plist as XML|plistxml|plist\n" + "Base64 Decode|base64|unBase64\n" + "Base64 Encode|base64|toBase64"; + sqlite3_result_text(context, result, -1, SQLITE_TRANSIENT); +} + /** RegisterExtensionFormats * * Register the parsing functions with sqlite */ -int RegisterExtensionFormats(sqlite3 *db) -{ - sqlite3_create_function(db, "plist", 1, 0, db, plistFunc, 0, 0); - sqlite3_create_function(db, "unBase64", 1, 0, db, decodeBase64Func, 0, 0); - sqlite3_create_function(db, "toBase64", 1, 0, db, encodeBase64Func, 0, 0); +int RegisterExtensionFormats(sqlite3 *db) { + sqlite3_create_function(db, "plist", 1, 0, db, plistFunc, 0, 0); + sqlite3_create_function(db, "plistToJson", 1, 0, db, plistToJsonFunc, 0, 0); + sqlite3_create_function(db, "plistDeserializedToJson", 1, 0, db, plistDeserializedToJsonFunc, 0, 0); + sqlite3_create_function(db, "unBase64", 1, 0, db, decodeBase64Func, 0, 0); + sqlite3_create_function(db, "toBase64", 1, 0, db, encodeBase64Func, 0, 0); + + // Register the display format + sqlite3_create_function(db, "load_formats_builtin", 0, SQLITE_UTF8, NULL, load_formats_builtin, NULL, NULL); + + return ERROR_NONE; } #ifdef COMPILE_SQLITE_EXTENSIONS_AS_LOADABLE_MODULE diff --git a/src/icons/icons.qrc b/src/icons/icons.qrc index e34fe0a831..222f6d38e5 100644 --- a/src/icons/icons.qrc +++ b/src/icons/icons.qrc @@ -98,6 +98,7 @@ text_bold.svg text_italic.svg text_padding_left.svg + text_sort_ascending.svg text_padding_top.svg color_wheel.svg find_edit.svg diff --git a/src/icons/text_sort_ascending.svg b/src/icons/text_sort_ascending.svg new file mode 100644 index 0000000000..347fda5610 --- /dev/null +++ b/src/icons/text_sort_ascending.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + Layer 1 + + + + + + + + + + \ No newline at end of file diff --git a/src/sql/sqlitetypes.cpp b/src/sql/sqlitetypes.cpp index dda41de206..aa00f58756 100644 --- a/src/sql/sqlitetypes.cpp +++ b/src/sql/sqlitetypes.cpp @@ -453,7 +453,7 @@ void Table::removeConstraint(std::shared_ptr constraint) void Table::addKeyToConstraint(std::shared_ptr constraint, const std::string& key) { // Search for matching constraint - for(auto it=m_indexConstraints.begin();it!=m_indexConstraints.end();++it) + for(auto it=m_indexConstraints.begin();it!=m_indexConstraints.end();) { if(it->second == constraint) { @@ -464,12 +464,16 @@ void Table::addKeyToConstraint(std::shared_ptr constraint, con m_indexConstraints.insert(std::make_pair(new_columns, it->second)); it = m_indexConstraints.erase(it); } + else + { + ++it; + } } } void Table::removeKeyFromConstraint(std::shared_ptr constraint, const std::string& key) { - for(auto it=m_indexConstraints.begin();it!=m_indexConstraints.end();++it) + for(auto it=m_indexConstraints.begin();it!=m_indexConstraints.end();) { if(it->second == constraint) { @@ -485,10 +489,8 @@ void Table::removeKeyFromConstraint(std::shared_ptr constraint m_indexConstraints.insert(std::make_pair(new_columns, it->second)); it = m_indexConstraints.erase(it); } - - // If container is empty now, return here instead of advancing the iterator - if(m_indexConstraints.empty()) - return; + } else { + ++it; } } } @@ -511,17 +513,15 @@ void Table::removeKeyFromAllConstraints(const std::string& key) container.insert(std::make_pair(new_columns, it->second)); it = container.erase(it); } - - // If container is empty now, return here instead of advancing the iterator - if(container.empty()) - return; + } else { + ++it; } }; - for(auto it=m_indexConstraints.begin();it!=m_indexConstraints.end();++it) + for(auto it=m_indexConstraints.begin();it!=m_indexConstraints.end();) match_and_remove(m_indexConstraints, it); - for(auto it=m_foreignKeys.begin();it!=m_foreignKeys.end();++it) + for(auto it=m_foreignKeys.begin();it!=m_foreignKeys.end();) match_and_remove(m_foreignKeys, it); } @@ -544,10 +544,14 @@ void Table::renameKeyInAllConstraints(const std::string& key, const std::string& container.insert(std::make_pair(new_columns, it->second)); it = container.erase(it); } + else + { + ++it; + } }; // Update all constraints - for(auto it=m_indexConstraints.begin();it!=m_indexConstraints.end();++it) + for(auto it=m_indexConstraints.begin();it!=m_indexConstraints.end();) { match_and_rename(m_indexConstraints, it, [key, to](IndexedColumn c) { if(c == key) diff --git a/src/sqlitedb.cpp b/src/sqlitedb.cpp index 8f2cecb419..329a570121 100644 --- a/src/sqlitedb.cpp +++ b/src/sqlitedb.cpp @@ -25,6 +25,32 @@ #include #include +namespace { + +bool equalsIgnoreCase(const std::string& lhs, const std::string& rhs) +{ + if(lhs.size() != rhs.size()) + return false; + + return std::equal(lhs.begin(), lhs.end(), rhs.begin(), rhs.end(), [](unsigned char a, unsigned char b) { + return std::tolower(a) == std::tolower(b); + }); +} + +template +typename MapType::const_iterator findCaseInsensitive(const MapType& map, const std::string& key) +{ + auto it = map.find(key); + if(it != map.end()) + return it; + + return std::find_if(map.begin(), map.end(), [&key](const auto& entry) { + return equalsIgnoreCase(entry.first, key); + }); +} + +} + const QStringList DBBrowserDB::journalModeValues = {"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}; const QStringList DBBrowserDB::lockingModeValues = {"NORMAL", "EXCLUSIVE"}; @@ -871,8 +897,11 @@ bool DBBrowserDB::dump(const QString& filePath, std::vector tables; for(const auto& it : objMap.tables) { - // Never export the sqlite_stat1 and the sqlite_sequence tables if they exist. Also only export any tables which are selected for export. - if(!it.second->isView() && it.first != "sqlite_stat1" && it.first != "sqlite_sequence" && contains(tablesToDump, it.first)) + QString tableName = QString::fromStdString(it.first); + // Never export the sqlite internal tables. Also only export any tables which are selected for export. + if(!it.second->isView() + && !tableName.startsWith("sqlite_", Qt::CaseInsensitive) + && contains(tablesToDump, it.first)) { // Get the number of records in this table and remember to export it tables.push_back(it.second); @@ -910,9 +939,14 @@ bool DBBrowserDB::dump(const QString& filePath, else { QString statement = QString::fromStdString(it->originalSql()); if(keepOldSchema) { - // The statement is guaranteed by SQLite to start with "CREATE TABLE" - const int createTableLength = 12; - statement.replace(0, createTableLength, "CREATE TABLE IF NOT EXISTS"); + if (statement.startsWith("CREATE VIRTUAL TABLE", Qt::CaseInsensitive)) { + const int createTableLength = 20; + statement.replace(0, createTableLength, "CREATE VIRTUAL TABLE IF NOT EXISTS"); + } else { + // The statement is guaranteed by SQLite to start with "CREATE TABLE" + const int createTableLength = 12; + statement.replace(0, createTableLength, "CREATE TABLE IF NOT EXISTS"); + } } stream << statement << ";\n"; } @@ -1202,30 +1236,31 @@ bool DBBrowserDB::executeMultiSQL(QByteArray query, bool dirty, bool log) // Execute next statement if(sqlite3_prepare_v2(_db, tail, static_cast(tail_end - tail + 1), &vm, &tail) == SQLITE_OK) { - switch(sqlite3_step(vm)) + if(vm) { - case SQLITE_OK: - case SQLITE_ROW: - case SQLITE_DONE: - case SQLITE_MISUSE: // This is a workaround around problematic user scripts. If they lead to empty commands, - // SQLite will return a misuse error which we hereby ignore. - sqlite3_finalize(vm); - break; - default: - // In case of *any* error abort the execution and roll back the transaction - - // Make sure to save the error message first before any other function can mess around with it - lastErrorMessage = tr("Error in statement #%1: %2.\nAborting execution%3.").arg( - QString::number(line), - sqlite3_errmsg(_db), - dirty ? tr(" and rolling back") : ""); - qWarning() << lastErrorMessage; - - // Clean up - sqlite3_finalize(vm); - if(dirty) - revertToSavepoint(savepoint_name); - return false; + switch(sqlite3_step(vm)) + { + case SQLITE_OK: + case SQLITE_ROW: + case SQLITE_DONE: + sqlite3_finalize(vm); + break; + default: + // In case of *any* error abort the execution and roll back the transaction + + // Make sure to save the error message first before any other function can mess around with it + lastErrorMessage = tr("Error in statement #%1: %2.\nAborting execution%3.").arg( + QString::number(line), + sqlite3_errmsg(_db), + dirty ? tr(" and rolling back") : ""); + qWarning() << lastErrorMessage; + + // Clean up + sqlite3_finalize(vm); + if(dirty) + revertToSavepoint(savepoint_name); + return false; + } } } else { lastErrorMessage = tr("Error in statement #%1: %2.\nAborting execution%3.").arg( @@ -1336,7 +1371,7 @@ bool DBBrowserDB::getRow(const sqlb::ObjectIdentifier& table, const QString& row unsigned long DBBrowserDB::max(const sqlb::ObjectIdentifier& tableName, const std::string& field) const { // This query returns the maximum value of the given table and column - std::string query = "SELECT MAX(CAST(" + sqlb::escapeIdentifier(field) + " AS INTEGER)) FROM " + tableName.toString() + ")"; + std::string query = "SELECT MAX(CAST(" + sqlb::escapeIdentifier(field) + " AS INTEGER)) FROM " + tableName.toString(); // If, however, there is a sequence table in this database and the given column is the primary key of the table, we try to look up a value in the sequence table if(schemata.at(tableName.schema()).tables.count("sqlite_sequence")) @@ -1347,7 +1382,7 @@ unsigned long DBBrowserDB::max(const sqlb::ObjectIdentifier& tableName, const st // This SQL statement tries to do two things in one statement: get the current sequence number for this table from the sqlite_sequence table or, if there is no record for the table, return the highest integer value in the given column. // This works by querying the sqlite_sequence table and using an aggregate function (SUM in this case) to make sure to always get exactly one result row, no matter if there is a sequence record or not. We then let COALESCE decide // whether to return that sequence value if there is one or fall back to the SELECT MAX statement from avove if there is no sequence value. - query = "SELECT COALESCE(SUM(seq), (" + query + ") FROM sqlite_sequence WHERE name=" + sqlb::escapeString(tableName.name()); + query = "SELECT COALESCE(SUM(seq), (" + query + ")) FROM sqlite_sequence WHERE name=" + sqlb::escapeString(tableName.name()); } } @@ -2217,6 +2252,66 @@ std::vector> DBBrowserDB::queryColumnInforma return result; } +const sqlb::TablePtr DBBrowserDB::getTableByName(const sqlb::ObjectIdentifier& name) const +{ + if(schemata.empty() || name.schema().empty() || name.name().empty()) + return sqlb::TablePtr{}; + + const auto schemaIt = findCaseInsensitive(schemata, name.schema()); + if(schemaIt == schemata.end()) + return sqlb::TablePtr{}; + + const auto& tables = schemaIt->second.tables; + if(tables.empty()) + return sqlb::TablePtr{}; + + const auto tableIt = findCaseInsensitive(tables, name.name()); + if(tableIt == tables.end()) + return sqlb::TablePtr{}; + + return tableIt->second; +} + +const sqlb::IndexPtr DBBrowserDB::getIndexByName(const sqlb::ObjectIdentifier& name) const +{ + if(schemata.empty() || name.schema().empty() || name.name().empty()) + return sqlb::IndexPtr{}; + + const auto schemaIt = findCaseInsensitive(schemata, name.schema()); + if(schemaIt == schemata.end()) + return sqlb::IndexPtr{}; + + const auto& indices = schemaIt->second.indices; + if(indices.empty()) + return sqlb::IndexPtr{}; + + const auto indexIt = findCaseInsensitive(indices, name.name()); + if(indexIt == indices.end()) + return sqlb::IndexPtr{}; + + return indexIt->second; +} + +const sqlb::TriggerPtr DBBrowserDB::getTriggerByName(const sqlb::ObjectIdentifier& name) const +{ + if(schemata.empty() || name.schema().empty() || name.name().empty()) + return sqlb::TriggerPtr{}; + + const auto schemaIt = findCaseInsensitive(schemata, name.schema()); + if(schemaIt == schemata.end()) + return sqlb::TriggerPtr{}; + + const auto& triggers = schemaIt->second.triggers; + if(triggers.empty()) + return sqlb::TriggerPtr{}; + + const auto triggerIt = findCaseInsensitive(triggers, name.name()); + if(triggerIt == triggers.end()) + return sqlb::TriggerPtr{}; + + return triggerIt->second; +} + std::string DBBrowserDB::generateSavepointName(const std::string& identifier) const { // Generate some sort of unique name for a savepoint for internal use. diff --git a/src/sqlitedb.h b/src/sqlitedb.h index d80dd61aba..284ce34114 100644 --- a/src/sqlitedb.h +++ b/src/sqlitedb.h @@ -228,35 +228,14 @@ class DBBrowserDB : public QObject */ bool alterTable(const sqlb::ObjectIdentifier& tablename, const sqlb::Table& new_table, AlterTableTrackColumns track_columns, std::string newSchemaName = std::string()); - const sqlb::TablePtr getTableByName(const sqlb::ObjectIdentifier& name) const - { - if(schemata.empty() || name.schema().empty() || !schemata.count(name.schema())) - return sqlb::TablePtr{}; - const auto& schema = schemata.at(name.schema()); - if(schema.tables.count(name.name())) - return schema.tables.at(name.name()); - return sqlb::TablePtr{}; - } - - const sqlb::IndexPtr getIndexByName(const sqlb::ObjectIdentifier& name) const - { - if(schemata.empty() || name.schema().empty()) - return sqlb::IndexPtr{}; - const auto& schema = schemata.at(name.schema()); - if(schema.indices.count(name.name())) - return schema.indices.at(name.name()); - return sqlb::IndexPtr{}; - } - - const sqlb::TriggerPtr getTriggerByName(const sqlb::ObjectIdentifier& name) const - { - if(schemata.empty() || name.schema().empty()) - return sqlb::TriggerPtr{}; - const auto& schema = schemata.at(name.schema()); - if(schema.triggers.count(name.name())) - return schema.triggers.at(name.name()); - return sqlb::TriggerPtr{}; - } + // Given that sqlite3 does not allow case-differing identifiers, and in some + // situations the stored name in `schemata` may differ only in case from the name used in + // the stored SQL create statement (like issue #4110), the search of these get*Name + // functions is case-insensitive, if not found as is. + + const sqlb::TablePtr getTableByName(const sqlb::ObjectIdentifier& name) const; + const sqlb::IndexPtr getIndexByName(const sqlb::ObjectIdentifier& name) const; + const sqlb::TriggerPtr getTriggerByName(const sqlb::ObjectIdentifier& name) const; bool isOpen() const; bool encrypted() const { return isEncrypted; } diff --git a/src/sqlitetablemodel.cpp b/src/sqlitetablemodel.cpp index df7d4a451b..fe1b690cfb 100644 --- a/src/sqlitetablemodel.cpp +++ b/src/sqlitetablemodel.cpp @@ -1,4 +1,4 @@ -#include "sqlitetablemodel.h" +#include "sqlitetablemodel.h" #include "sqlitedb.h" #include "sqlite.h" #include "Settings.h" @@ -41,6 +41,7 @@ SqliteTableModel::SqliteTableModel(DBBrowserDB& db, QObject* parent, const QStri // any UI updates must be performed in the UI thread, not in the worker thread: connect(worker, &RowLoader::fetched, this, &SqliteTableModel::handleFinishedFetch, Qt::QueuedConnection); connect(worker, &RowLoader::rowCountComplete, this, &SqliteTableModel::handleRowCountComplete, Qt::QueuedConnection); + connect(worker, &RowLoader::error, this, &SqliteTableModel::handleError, Qt::QueuedConnection); reset(); } @@ -106,11 +107,20 @@ void SqliteTableModel::handleRowCountComplete (int life_id, int num_rows) emit finishedRowCount(); } +void SqliteTableModel::handleError(int life_id, const QString& errMsg) +{ + if (life_id < m_lifeCounter) + return; + + m_lastError = errMsg; +} + void SqliteTableModel::reset() { beginResetModel(); clearCache(); + m_lastError.clear(); m_sQuery.clear(); m_query.clear(); m_table_of_query.reset(); diff --git a/src/sqlitetablemodel.h b/src/sqlitetablemodel.h index b24ca058dd..6a0bf61038 100644 --- a/src/sqlitetablemodel.h +++ b/src/sqlitetablemodel.h @@ -143,6 +143,9 @@ class SqliteTableModel : public QAbstractTableModel void reloadSettings(); + bool hasError() const { return !m_lastError.isEmpty(); } + QString lastError() const { return m_lastError; } + public slots: void updateFilter(const std::string& column, const QString& value); void updateGlobalFilter(const std::vector& values); @@ -165,6 +168,7 @@ public slots: void handleFinishedFetch(int life_id, unsigned int fetched_row_begin, unsigned int fetched_row_end); void handleRowCountComplete(int life_id, int num_rows); + void handleError(int life_id, const QString& errMsg); void updateAndRunQuery(); @@ -253,6 +257,8 @@ public slots: * to that row count. */ size_t m_chunkSize; + + QString m_lastError; }; #endif diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index f0e0a8ef4f..d446d68fc8 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -1,5 +1,9 @@ include_directories("${CMAKE_CURRENT_BINARY_DIR}" ..) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + find_package(${QT_MAJOR} REQUIRED COMPONENTS Test Widgets) if(QT_MAJOR STREQUAL "Qt6") find_package(Qt6 REQUIRED COMPONENTS Core5Compat) @@ -31,7 +35,7 @@ set(TESTSQLOBJECTS_HDR add_executable(test-sqlobjects ${TESTSQLOBJECTS_HDR} ${TESTSQLOBJECTS_SRC}) target_link_libraries(test-sqlobjects ${QT_MAJOR}::Test) -add_test(test-sqlobjects test-sqlobjects) +add_test(NAME test-sqlobjects COMMAND $) # test-import @@ -46,7 +50,7 @@ set(TESTIMPORT_MOC_HDR add_executable(test-import ${TESTIMPORT_MOC_HDR} ${TESTIMPORT_SRC}) target_link_libraries(test-import ${QT_MAJOR}::Test) -add_test(test-import test-import) +add_test(NAME test-import COMMAND $) # test regex @@ -62,7 +66,7 @@ set(TESTREGEX_HDR add_executable(test-regex ${TESTREGEX_HDR} ${TESTREGEX_SRC}) target_link_libraries(test-regex ${QT_MAJOR}::Test ${QT_MAJOR}::Widgets ${QT5_COMPAT}) -add_test(test-regex test-regex) +add_test(NAME test-regex COMMAND $) # test cache @@ -76,4 +80,4 @@ set(TESTCACHE_HDR add_executable(test-cache ${TESTCACHE_HDR} ${TESTCACHE_SRC}) target_link_libraries(test-cache ${QT_MAJOR}::Test) -add_test(test-cache test-cache) +add_test(NAME test-cache COMMAND $) diff --git a/src/translations/sqlb_pt_BR.ts b/src/translations/sqlb_pt_BR.ts index 797f9621ab..2288f92047 100644 --- a/src/translations/sqlb_pt_BR.ts +++ b/src/translations/sqlb_pt_BR.ts @@ -2060,7 +2060,7 @@ Deseja inserir mesmo assim? Cut - + Recortar @@ -2785,7 +2785,7 @@ x~y Intervalo: valores entre x e y &Open Database... - &Abrir banco de dados... + Abrir &banco de dados... @@ -2860,7 +2860,7 @@ x~y Intervalo: valores entre x e y E&xit - &Sair + Sai&r @@ -2957,7 +2957,7 @@ x~y Intervalo: valores entre x e y &Recently opened - &Recentemente aberto + Re&centemente aberto @@ -3129,7 +3129,7 @@ Se você disser que não, tentaremos importar os dados do arquivo SQL para o ban Clear List - + Limpar Lista @@ -3144,7 +3144,7 @@ Se você disser que não, tentaremos importar os dados do arquivo SQL para o ban Simplify Window Layout - + Simplificar layout da janela @@ -3341,7 +3341,7 @@ Você tem certeza? &Create Table... - &Criar tabela... + Criar &tabela... @@ -3533,7 +3533,7 @@ Você pode arrastar comandos SQL de uma linha e soltá-los em outras aplicaçõe Foreign Keys - + Chaves estrangeiras @@ -3560,7 +3560,7 @@ Você pode arrastar comandos SQL de uma linha e soltá-los em outras aplicaçõe Normal - + Normal @@ -3576,13 +3576,13 @@ Você pode arrastar comandos SQL de uma linha e soltá-los em outras aplicaçõe File - Arquivo + Arquivo Memory - + Memória @@ -3607,12 +3607,12 @@ Você pode arrastar comandos SQL de uma linha e soltá-los em outras aplicaçõe Exclusive - + Exclusivo Automatic Index - + Índice automático @@ -3637,7 +3637,7 @@ Você pode arrastar comandos SQL de uma linha e soltá-los em outras aplicaçõe Synchronous - + Síncrono @@ -3647,12 +3647,12 @@ Você pode arrastar comandos SQL de uma linha e soltá-los em outras aplicaçõe None - Nenhum + Nenhum Incremental - + Incremental @@ -3662,7 +3662,7 @@ Você pode arrastar comandos SQL de uma linha e soltá-los em outras aplicaçõe Too&ls - Ferr&amentas + &Ferramentas @@ -3711,7 +3711,7 @@ Você pode arrastar comandos SQL da coluna Esquema e largá-los no editor SQL ou &New Database - + &Novo banco de dados @@ -3727,13 +3727,13 @@ Você pode arrastar comandos SQL da coluna Esquema e largá-los no editor SQL ou &Undo - + &Desfazer Undo last change to the database - + Desfaz a última mudança feita no banco de dados @@ -3758,7 +3758,7 @@ Você pode arrastar comandos SQL da coluna Esquema e largá-los no editor SQL ou New &tab - + Nova a&ba @@ -3828,7 +3828,7 @@ Você pode arrastar comandos SQL da coluna Esquema e largá-los no editor SQL ou &Save Project - + &Salvar Projeto @@ -3838,12 +3838,12 @@ Você pode arrastar comandos SQL da coluna Esquema e largá-los no editor SQL ou Open &Project - + Abrir &projeto &Attach Database... - &Anexar banco de dados... + A&nexar banco de dados... @@ -3859,7 +3859,7 @@ Você pode arrastar comandos SQL da coluna Esquema e largá-los no editor SQL ou &Set Encryption... - Definir en&criptação... + Definir e&ncriptação... @@ -3879,7 +3879,7 @@ Você pode arrastar comandos SQL da coluna Esquema e largá-los no editor SQL ou Open Data&base Read Only... - Abrir &banco de dados somente leitura... + Abrir banco de dados somente &leitura... @@ -3982,7 +3982,7 @@ Você pode arrastar comandos SQL da coluna Esquema e largá-los no editor SQL ou Table from CSV data in Clipboard... - + Tabela de dados CSV na área de transferência... @@ -3992,7 +3992,7 @@ Você pode arrastar comandos SQL da coluna Esquema e largá-los no editor SQL ou Show &Row Counts - + Mostrar quantidade de &Linhas @@ -4002,22 +4002,22 @@ Você pode arrastar comandos SQL da coluna Esquema e largá-los no editor SQL ou Save Database &As... - + Sal&var banco de dados como... Save the current database as a different file - + Salvar o banco de dados atual em um arquivo diferente Refresh - Atualizar + Atualizar Reload the database structure - + Recarrega a estrutura do banco de dados @@ -4029,33 +4029,33 @@ Você pode arrastar comandos SQL da coluna Esquema e largá-los no editor SQL ou &Database Structure This has to be equal to the tab title in all the main tabs - + &Estrutura do banco de dados &Browse Data This has to be equal to the tab title in all the main tabs - + &Ver dados Edit P&ragmas This has to be equal to the tab title in all the main tabs - + Editar &pragmas E&xecute SQL This has to be equal to the tab title in all the main tabs - + E&xecutar SQL &Recent Files - + Arquivos &Recentes @@ -4065,7 +4065,7 @@ Você pode arrastar comandos SQL da coluna Esquema e largá-los no editor SQL ou &Open Database - + &Abrir Banco de dados @@ -4258,13 +4258,13 @@ Você pode arrastar comandos SQL da coluna Esquema e largá-los no editor SQL ou Close Pro&ject - + Fechar pro&jeto Close project and database files and return to the initial state - + Fecha os arquivos de projeto e banco de dados e retorna para o estado inicial @@ -4347,12 +4347,12 @@ Você tem certeza que quer salvar o banco de dados? Edit View %1 - + Editar Vista %1 Edit Trigger %1 - + Editar gatilho %1 @@ -4423,12 +4423,12 @@ Você tem certeza que quer salvar o banco de dados? Yes. Don't ask again - + Sim. Não perguntar novamente This action will open a new SQL tab with the following statements for you to edit and run: - + Essa ação abrirá uma nova aba de SQL com os seguintes comandos para você editar e executar: @@ -4468,7 +4468,7 @@ Você tem certeza que quer salvar o banco de dados? Automatically load the last opened DB file at startup - + Automaticamente carregar o último BD aberto ao iniciar @@ -4504,7 +4504,7 @@ Você tem certeza que quer salvar o banco de dados? Attach Database... - Anexar banco de dados... + Anexar banco de dad&os... @@ -5122,7 +5122,7 @@ Aviso: nem todos os dados foram obtidos da tabela ainda devido ao mecanismo de o Save Settings File - + Salvar arquivo de configurações @@ -5139,7 +5139,7 @@ Aviso: nem todos os dados foram obtidos da tabela ainda devido ao mecanismo de o Open Settings File - + Abrir arquivo de configurações @@ -7333,13 +7333,13 @@ Segure %3Shift e clique para ir para lá Export to &JSON - + Exportar para &JSON Export the filtered data to JSON - + Exporta os dados filtrados para JSON @@ -7349,7 +7349,7 @@ Segure %3Shift e clique para ir para lá Copy column name - + Copiar nome da coluna @@ -7365,12 +7365,12 @@ Segure %3Shift e clique para ir para lá Add a new docked Data Browser - + Adicionar nova aba de ver dados This button adds a new docked Data Browser, which you can detach and arrange in different layouts. - + Este botão adiciona uma nova aba para ver dados, que você pode desacoplar e organizar em layouts diferentes. @@ -7854,7 +7854,7 @@ Segure %3Shift e clique para ir para lá Filter in any column - + Filtrar em qualquer coluna diff --git a/src/translations/sqlb_ru.ts b/src/translations/sqlb_ru.ts index 3f82f192a6..bc4fb6ba84 100644 --- a/src/translations/sqlb_ru.ts +++ b/src/translations/sqlb_ru.ts @@ -16,7 +16,7 @@ <html><head/><body><p>DB Browser for SQLite is an open source, freeware visual tool used to create, design and edit SQLite database files.</p><p>It is bi-licensed under the Mozilla Public License Version 2, as well as the GNU General Public License Version 3 or later. You can modify or redistribute it under the conditions of these licenses.</p><p>See <a href="https://www.gnu.org/licenses/gpl.html"><span style=" text-decoration: underline; color:#0000ff;">https://www.gnu.org/licenses/gpl.html</span></a> and <a href="https://www.mozilla.org/MPL/2.0/index.txt"><span style=" text-decoration: underline; color:#0000ff;">https://www.mozilla.org/MPL/2.0/index.txt</span></a> for details.</p><p>For more information on this program please visit our website at: <a href="https://sqlitebrowser.org"><span style=" text-decoration: underline; color:#0000ff;">https://sqlitebrowser.org</span></a></p><p><span style=" font-size:small;">This software uses the GPL/LGPL Qt Toolkit from </span><a href="https://qt-project.org/"><span style=" font-size:small; text-decoration: underline; color:#0000ff;">https://qt-project.org/</span></a><span style=" font-size:small;"><br/>See </span><a href="https://doc.qt.io/qt-5/licensing.html"><span style=" font-size:small; text-decoration: underline; color:#0000ff;">https://doc.qt.io/qt-5/licensing.html</span></a><span style=" font-size:small;"> for licensing terms and information.</span></p><p><span style=" font-size:8pt;">We use the nalgeon/sqlean library for SQLite extensions support.<br/>This library is licensed under the MIT license, see the following for more information:<br/></span><a href="https://github.com/nalgeon/sqlean"><span style=" font-size:8pt; text-decoration: underline; color:#0000ff;">https://github.com/nalgeon/sqlean</span></a></p><p><span style=" font-size:small;">It also uses the Pastel SVG icon set by Michael Buckley under a Creative Commons Attribution Share Alike 4.0 license.<br/>See </span><a href="https://codefisher.org/pastel-svg/"><span style=" font-size:small; text-decoration: underline; color:#0000ff;">https://codefisher.org/pastel-svg/</span></a><span style=" font-size:small;"> for details.</span></p></body></html> - + <html><head/><body><p>DB Browser for SQLite это свободно распространяемая утилита для визуализации разработки, создания и редактирования баз данных SQLite с открытым исходным кодом.</p><p>Он распространяется под двойной лицензией Mozilla Public License версии 2, а также под лицензией GNU General Public License версии 3 или более поздней. Вы можете изменять или распространять его в соответствии с условиями этих лицензий.</p><p>Посетите <a href="https://www.gnu.org/licenses/gpl.html"><span style=" text-decoration: underline; color:#0000ff;">https://www.gnu.org/licenses/gpl.html</span></a> и <a href="https://www.mozilla.org/MPL/2.0/index.txt"><span style=" text-decoration: underline; color:#0000ff;">https://www.mozilla.org/MPL/2.0/index.txt</span></a> для получения подробностей.</p><p>Больше информации об этой программе можно получить, посетив нашу страницу: <a href="https://sqlitebrowser.org"><span style=" text-decoration: underline; color:#0000ff;">https://sqlitebrowser.org</span></a></p><p><span style=" font-size:small;">Эта программа использует GPL/LGPL Qt Toolkit от </span><a href="https://qt-project.org/"><span style=" font-size:small; text-decoration: underline; color:#0000ff;">https://qt-project.org/</span></a><span style=" font-size:small;"><br/>Смотри </span><a href="https://doc.qt.io/qt-5/licensing.html"><span style=" font-size:small; text-decoration: underline; color:#0000ff;">https://doc.qt.io/qt-5/licensing.html</span></a><span style=" font-size:small;"> с требованиями лицензии и информацией.</span></p><p><span style=" font-size:8pt;">Мы используем библиотеку nalgeon/sqlean для поддержки расширений SQLite.<br/>Эта библиотека лицензирована по лицензии MIT, для получения дополнительной информации смотри:<br/></span><a href="https://github.com/nalgeon/sqlean"><span style=" font-size:8pt; text-decoration: underline; color:#0000ff;">https://github.com/nalgeon/sqlean</span></a></p><p><span style=" font-size:small;">Программа также использует набор иконок Pastel SVG, созданный Michael Buckley по лицензии Creative Commons Attribution Share Alike 4.0.<br/>Смотри: </span><a href="https://codefisher.org/pastel-svg/"><span style=" font-size:small; text-decoration: underline; color:#0000ff;">https://codefisher.org/pastel-svg/</span></a><span style=" font-size:small;"> для подробностей.</span></p></body></html> @@ -24,7 +24,7 @@ Add New Record - Добавить Новую Запись + Добавить новую запись @@ -59,7 +59,7 @@ When you edit the values in the upper frame, the SQL query for inserting this new record is shown here. You can edit manually the query before saving. - Когда вы редактируете значения в верхнем фрейме, здесь отображается запрос SQL для вставки этой новой записи. Вы можете вручную отредактировать запрос перед сохранением. + Когда вы редактируете значения в верхнем окне, здесь отображается запрос SQL для вставки этой новой записи. Вы можете вручную отредактировать запрос перед сохранением. @@ -70,7 +70,7 @@ Auto-increment - Авто-увеличение + Автоинкремент @@ -121,202 +121,202 @@ Possible command line arguments: - + Возможные аргументы коммандной строки: The user settings file location is replaced with the argument value instead of the environment variable value. - + Расположение файла настроек пользователя заменяется значением аргумента вместо значения переменной среды. Ignored environment variable (DB4S_SETTINGS_FILE) value: - + Игнорируемое значение переменной среды (DB4S_SETTINGS_FILE): The file %1 does not exist - + Файл %1 не существует Usage - + Использование options - + опции database - + база данных project - + проект csv-file - + csv-файл Show command line options - + Показать опции коммандной строки Exit application after running scripts - + Выход из приложения после выполнения скрипта file - + файл Execute this SQL file after opening the DB - + Выполнить этот SQL файл после открытия БД Import this CSV file into the passed DB or into a new DB - + Импорт этого CSV-файла в существующую БД или в новую БД table - + таблица Browse this table, or use it as target of a data import - + Просмотр таблицы или ипользование ее как получателя импортируемых данных Open database in read-only mode - + Открыть базу данных только для чтения settings_file - + файл_настроек Run application based on this settings file - + Запуск на основе этого файла настоек group - + группа settings - + установки value - + значение Run application with this setting temporarily set to value - + Запуск приложения, с временным значением этого параметра Run application saving this value for this setting - + Запустить приложение, сохранив это значение для этого параметра Display the current version - + Показать текущую версию Open this SQLite database - + Открыть эту SQLite базу данных Open this project file (*.sqbpro) - + Открыть этот файл проекта (*.sqbpro) Import this CSV file into an in-memory database - + Импорт этот CSV файл в базу данных в памяти The %1 option requires an argument - + Для опции %1 требуется аргумент The -S/--settings option requires an argument. The option is ignored. - + Параметр -S/--settings требует аргумента. Параметр игнорируется. The -o/--option and -O/--save-option options require an argument in the form group/setting=value - + Параметры -o/--option и -O/--save-option требуют аргумент в форме группа/настройка=значение Invalid option/non-existent file: %1 - + Неверная настройка/несуществующий файл: %1 SQLite Version - Версия SQLite + Версия SQLite SQLCipher Version %1 (based on SQLite %2) - + SQLCipher версия %1 (на основе SQLite %2) DB Browser for SQLite Version %1. - + DB Browser for SQLite Версия %1. Last commit hash when built: %1 - + Последний хэш коммита при сборке: %1 Built for %1, running on %2 - + Создано для %1, работает на %2 Qt Version %1 - + Qt Версия %1 @@ -339,22 +339,22 @@ Encr&yption settings - + Настройки &шифрования SQLCipher &3 defaults - + SQLCipher &3 SQLCipher &4 defaults - + SQLCipher &4 Custo&m - + &Другой @@ -364,22 +364,22 @@ &KDF iterations - + &KDF итераций HMAC algorithm - + HMAC алгоритм KDF algorithm - + KDF алгоритм Plaintext Header Size - + Размер открытого заголовка @@ -400,14 +400,14 @@ The encryption process might take some time and you should have a backup copy of Пожалуйста укажите ключ шифрования. Если вы измените какую-либо опциональную настройку, то ее нужно будет вводить при каждом открытии этого файла базы данных. Оставьте пароль пустым если шифрование не требуется. -Процесс может занять некоторое время и настоятельно рекомендуем создать резервную копию перед продолжением! Не сохраненные изменения автоматически будут сохранены. +Процесс может занять некоторое время и настоятельно рекомендуем создать резервную копию перед продолжением! Не сохраненные изменения будут применены перед изменением шифрования. Please enter the key used to encrypt the database. If any of the other settings were altered for this database file you need to provide this information as well. Пожалуйста введите ключ для шифрования базы данных. -Если любые другие настройки были изменены для данной базы данный то нужно так же предоставить данную информацию. +Если любые другие настройки были изменены для данной базы данных то нужно так же предоставить данную информацию. @@ -465,7 +465,7 @@ If any of the other settings were altered for this database file you need to pro .NET DateTime.Ticks to date - + .NET DateTime.Ticks дата @@ -475,17 +475,17 @@ If any of the other settings were altered for this database file you need to pro Unix epoch to local time - Unix-время + Unix-локальное время WebKit / Chromium epoch to date - + WebKit / Chromium epoch дата WebKit / Chromium epoch to local time - + WebKit / Chromium epoch локальное время @@ -500,29 +500,31 @@ If any of the other settings were altered for this database file you need to pro Binary GUID to text - + Двоичный GUID в текст SpatiaLite Geometry to SVG - + SpatiaLite Geometry в SVG Custom display format must contain a function call applied to %1 - + Пользовательский формат должен содержать вызов функции, примененный к %1 Error in custom display format. Message from database engine: %1 - + Ошибка в пользовательском формате. Сообщение от ядра базы данных: + +%1 Custom display format must return only one column but it returned %1. - + Пользовательский формат должен возвращать только один столбец, но он вернул %1. @@ -560,118 +562,118 @@ If any of the other settings were altered for this database file you need to pro Conditional Format Manager - + Менеджер форматирования This dialog allows creating and editing conditional formats. Each cell style will be selected by the first accomplished condition for that cell data. Conditional formats can be moved up and down, where those at higher rows take precedence over those at lower. Syntax for conditions is the same as for filters and an empty condition applies to all values. - + Этот диалог позволяет задавать формат содержимого. Стиль каждой ячейки будет выбран в соответствии с содержимым. Условия форматирования можно перемещать вверх и вниз, те, что находятся выше, имеют приоритет над теми, что находятся ниже. Синтаксис для содержимого такой же, как и для фильтров, а пустое условие применяется ко всем значениям. Add new conditional format - + Добавить новый формат &Add - &Добавить + &Добавить Remove selected conditional format - + Удалить формат &Remove - &Удалить + &Удалить Move selected conditional format up - + Переместить вверх Move &up - + Вверх Move selected conditional format down - + Переместить вниз Move &down - + Вниз Foreground - Передний план + Передний план Text color - Цвет текста + Цвет текста Background - Фон + Фон Background color - Цвет фона + Цвет фона Font - Шрифт + Шрифт Size - Размер + Размер Bold - Жирный + Жирный Italic - Курсив + Курсив Underline - Подчёркивание + Подчёркнутый Alignment - + Выравнивание Condition - + Содержимое Click to select color - + Выбор цвета Are you sure you want to clear all the conditional formats of this field? - + Вы уверены, что хотите очистить форматирование этого поля? @@ -679,7 +681,7 @@ If any of the other settings were altered for this database file you need to pro Please specify the database name under which you want to access the attached database - + Укажите имя базы данных, под которым вы хотите получить доступ к прикрепленной базе данных @@ -705,7 +707,7 @@ If any of the other settings were altered for this database file you need to pro Executing SQL... - Выполнить код SQL... + Выполнить SQL... @@ -720,18 +722,18 @@ If any of the other settings were altered for this database file you need to pro Database didn't close correctly, probably still busy - + База данных не закрылась корректно, возможно, она все еще занята Cannot open destination file: '%1' - + Невозможно открыть целевой файл: '%1' Cannot backup to file: '%1'. Message: %2 - + Невозможно сохранить резервную копию в файл: '%1'. Сообщение: %2 @@ -755,23 +757,23 @@ If any of the other settings were altered for this database file you need to pro Error in statement #%1: %2. Aborting execution%3. Ошибка в выражении #%1: %2. -Прерываем выполнение%3. +Выполнение прервано %3. and rolling back - и отменяем + и выполняется откат didn't receive any output from %1 - + не получил никаких выходных данных от %1 could not execute command: %1 - + не удалось выполнить команду: %1 @@ -781,7 +783,7 @@ Aborting execution%3. Cannot set data on this object - Невозможно назначить данные для этого объекта + Невозможно установить данные по этому объекту @@ -792,58 +794,61 @@ Aborting execution%3. No table with name '%1' exists in schema '%2'. - + В схеме '%2' нет таблицы с именем '%1'. Cannot find column %1. - + Не найден столбец %1. Creating savepoint failed. DB says: %1 - + Не удалось создать точку сохранения. Сообщение БД: %1 Renaming the column failed. DB says: %1 - + Переименование колонки не удалось. Сообщение БД: +%1 Releasing savepoint failed. DB says: %1 - + Реализовать точку сохранения не удалось. Сообщение БД: %1 Creating new table failed. DB says: %1 - + Создание новой таблицы не удалось. Сообщение БД: %1 Copying data to new table failed. DB says: %1 - + Копирование данных в новую таблицу не удалось. Сообщение БД: %1 Deleting old table failed. DB says: %1 - + Удаление старой таблицы не удалось. Сообщение БД: %1 Error renaming table '%1' to '%2'. Message from database engine: %3 - + Ошибка переименования таблицы '%1' в '%2'. +Сообщение БД: +%3 could not get list of db objects: %1 - + не удалось получить список объектов базы данных: %1 @@ -867,7 +872,7 @@ Message from database engine: Error loading built-in extension: %1 - + Ошибка загрузки встроенного расширения: %1 @@ -999,27 +1004,27 @@ Message from database engine: RTL Text - + RTL текст Binary - Двоичные данные + Бинарный JSON - + JSON XML - + XML Evaluation - + Оценочный @@ -1030,7 +1035,7 @@ Message from database engine: This checkable button enables or disables the automatic switching of the editor mode. When a new cell is selected or new data is imported and the automatic switching is enabled, the mode adjusts to the detected data type. You can then change the editor mode manually. If you want to keep this manually switched mode while moving through the cells, switch the button off. - Эта кнопка позволяет включать или отключать автоматическое переключение режима редактора. Когда выбрана новая ячейка или импортированы новые данные, а автоматическое переключение включено, режим настраивается на обнаруженный тип данных. Вы можете вручную изменить режим редактора. Если вы хотите сохранить этот режим ручного переключения при перемещении по ячейкам, выключите кнопку. + Позволяет включать или отключать автоматическое переключение режима редактора. Когда выбрана новая ячейка или импортированы новые данные, а автоматическое переключение включено, режим настраивается на обнаруженный тип данных. Вы можете вручную изменить режим редактора. Если вы хотите сохранить этот режим ручного переключения при перемещении по ячейкам, выключите кнопку. @@ -1040,74 +1045,74 @@ Message from database engine: This Qt editor is used for right-to-left scripts, which are not supported by the default Text editor. The presence of right-to-left characters is detected and this editor mode is automatically selected. - + Этот редактор Qt используется для скриптов справа налево, которые не поддерживаются текстовым редактором по умолчанию. Присутствие символов справа налево обнаруживается, и этот режим редактора выбирается автоматически. Identification of the cell currently in the editor - + Идентификация ячейки, находящейся в данный момент в редакторе Type and size of data currently in table - + Тип и размер данных, находящихся в таблице в данный момент Open preview dialog for printing the data currently stored in the cell - + Открыть диалоговое окно предварительного просмотра для печати данных, которые в данный момент хранятся в ячейке Auto-format: pretty print on loading, compact on saving. - Автоматическое форматирование: стилистическое форматирование при загрузке, компактность - при сохранении. + Автоматическое форматирование: стилистическое форматирование при загрузке, компактность при сохранении. When enabled, the auto-format feature formats the data on loading, breaking the text in lines and indenting it for maximum readability. On data saving, the auto-format feature compacts the data removing end of lines, and unnecessary whitespace. - Когда включено, функция автоматического форматирования форматирует данные по загрузке, разбивая текст в строках и отступы для максимальной читаемости. При сохранении данных функция автоматического форматирования объединяет данные, удаляющие конец строк, и ненужные пробелы. + Когда включено, автоматически форматирует данные по мере загрузки, разбивая текст в строках и отступы для максимальной читаемости. При сохранении данных функция автоматического форматирования объединяет данные, удаляющие конец строк, и ненужные пробелы. Word Wrap - + Перенос слов Wrap lines on word boundaries - + Перенос строк по границам слов Open in default application or browser - + Открыть в приложении по умолчанию или браузере Open in application - + Открыть в приложении The value is interpreted as a file or URL and opened in the default application or web browser. - + Значение интерпретируется как файл или URL-адрес и открывается в приложении по умолчанию или веб-браузере. Save file reference... - + Сохранить ссылку на файл... Save reference to file - + Сохранить ссылку в файл Open in external application - + Открыть во внешнем приложении @@ -1117,13 +1122,13 @@ Message from database engine: &Export... - + &Экспорт... &Import... - + &Импорт... @@ -1185,7 +1190,7 @@ Message from database engine: Ctrl+Shift+C - + Ctrl+Shift+C @@ -1202,7 +1207,7 @@ Message from database engine: Try switching to Image or Binary mode. - Попробуйте переключиться в Бинарный режим или режим Изображения. + Попробуйте переключиться в Двоичный режим или режим Изображения. @@ -1220,50 +1225,50 @@ Message from database engine: Type: NULL; Size: 0 bytes - + Тип: NULL; Размер: 0 байт Type: Text / Numeric; Size: %n character(s) - - - - + + Тип: Text / Numeric; Размер: %n символ + Тип: Text / Numeric; Размер: %n символа + Тип: Text / Numeric; Размер: %n символов Type: %1 Image; Size: %2x%3 pixel(s) - + Тип: %1 изображение; Размер: %2x%3 pixel(s) Type: Valid JSON; Size: %n character(s) - - - - + + Тип: Действительный JSON; Размер: %n символ + Тип: Действительный JSON; Размер: %n символа + Тип: Действительный JSON; Размер: %n символов Type: Binary; Size: %n byte(s) - - - - + + Тип: Binary; Размер: %n байт + Тип: Binary; Размер: %n байта + Тип: Binary; Размер: %n байт Couldn't save file: %1. - Не удалось сохранить файл:%1. + Не удалось сохранить файл:%1. The data has been saved to a temporary file and has been opened with the default application. You can now edit the file and, when you are ready, apply the saved new data to the cell or cancel any changes. - + Данные были сохранены во временном файле и открыты с помощью приложения по умолчанию. Теперь вы можете редактировать файл и, когда будете готовы, применить сохраненные новые данные к ячейке или отменить любые изменения. @@ -1288,28 +1293,33 @@ Message from database engine: Errors are indicated with a red squiggle underline. In the Evaluation mode, entered SQLite expressions are evaluated and the result applied to the cell. - + Режимы текстового редактора позволяют редактировать обычный текст, а также данные JSON или XML с подсветкой синтаксиса, автоматическим форматированием и проверкой перед сохранением. + +Ошибки обозначаются красным волнистым подчеркиванием. + +В режиме оценки введенные выражения SQLite оцениваются, а результат применяется к ячейке. Unsaved data in the cell editor - + Несохраненные данные в редакторе ячеек The cell editor contains data not yet applied to the database. Do you want to apply the edited data to row=%1, column=%2? - + Редактор ячеек содержит данные, которые еще не применены к базе данных. +Хотите применить отредактированные данные к строке=%1, столбцу=%2? Editing row=%1, column=%2 - + Редактирование строки=%1, столбца=%2 No cell active. - + Ни одна ячейка не активна. @@ -1347,7 +1357,7 @@ Do you want to apply the edited data to row=%1, column=%2? Edit Index Schema - Редактирование Индекса + Редактирование индекса @@ -1357,7 +1367,7 @@ Do you want to apply the edited data to row=%1, column=%2? For restricting the index to only a part of the table you can specify a WHERE clause here that selects the part of the table that should be indexed - Для ограничения индекса только частью таблицы вы можете указать здесь выражение WHERE, которое выбирает часть таблицы, которая должна быть проиндексирована + Для ограничения индексом только части таблицы вы можете указать здесь выражение WHERE, которое выбирает часть таблицы, которая должна быть проиндексирована @@ -1372,7 +1382,8 @@ Do you want to apply the edited data to row=%1, column=%2? Table column - Колонка таблицы + Колонка +таблицы @@ -1387,7 +1398,8 @@ Do you want to apply the edited data to row=%1, column=%2? Index column - Колонка индекса + Колонка +индекса @@ -1424,32 +1436,32 @@ Do you want to apply the edited data to row=%1, column=%2? Without Rowid - Без rowid + Без Rowid Database sche&ma - + Схе&ма БД Make this a 'WITHOUT ROWID' table. Setting this flag requires specifying a PRIMARY KEY (which can be of any type, and can be composite), and forbids the AUTOINCREMENT flag. - + Делает эту таблицу «БЕЗ ROWID». Установка этого флага требует указания ПЕРВИЧНОГО КЛЮЧА (который может быть любого типа и может быть составным) и запрещает флаг АВТОИНКРЕМЕНТ. On Conflict - + При конфликте Strict - + Строго When the strict option is enabled SQLite enforces the data types of each column when updating or inserting data. - + Если включена строгая опция, SQLite применяет типы данных каждого столбца при обновлении или вставке данных. @@ -1461,34 +1473,34 @@ Do you want to apply the edited data to row=%1, column=%2? Add - + Добавить Remove - + Удалить Move to top - + Вверх Move up - + Выше Move down - + Ниже Move to bottom - + Вниз @@ -1507,12 +1519,12 @@ Do you want to apply the edited data to row=%1, column=%2? NN - НП + Не null Not null - Не пустое (null) + Не пустое (not null) @@ -1522,7 +1534,7 @@ Do you want to apply the edited data to row=%1, column=%2? <html><head/><body><p><img src=":/icons/field_key"/> Primary key</p></body></html> - + <html><head/><body><p><img src=":/icons/field_key"/> Первичный ключ</p></body></html> @@ -1537,7 +1549,7 @@ Do you want to apply the edited data to row=%1, column=%2? U - У + Уникальное @@ -1560,17 +1572,17 @@ Do you want to apply the edited data to row=%1, column=%2? Check - Проверить + Проверка Check constraint - Проверить ограничение + Ограничение проверкой Collation - + Collation @@ -1580,50 +1592,50 @@ Do you want to apply the edited data to row=%1, column=%2? <html><head/><body><p><img src=":/icons/field_fk"/> Foreign Key</p></body></html> - + <html><head/><body><p><img src=":/icons/field_fk"/> Внешний ключ</p></body></html> Index Constraints - + Индекс Add constraint - + Добавить ограничение Remove constraint - + Удалить ограничение Columns - Столбцы + Столбцы SQL - + SQL Foreign Keys - + Внешний ключ References - + Ссылки Check Constraints - + Ограничения проверкой @@ -1634,23 +1646,24 @@ Do you want to apply the edited data to row=%1, column=%2? Primary Key - + Первичный ключ Add a primary key constraint - + Добавить ограничение первичного ключа Add a unique constraint - + Добавить ограничение уникальности Error creating table. Message from database engine: %1 - Ошибка создания таблицы. Сообщение от движка базы данных: %1 + Ошибка создания таблицы. Сообщение от движка базы данных: +%1 @@ -1661,12 +1674,12 @@ Do you want to apply the edited data to row=%1, column=%2? There can only be one primary key for each table. Please modify the existing primary key instead. - + Для каждой таблицы может быть только один первичный ключ. Вместо этого измените существующий первичный ключ. This column is referenced in a foreign key in table %1 and thus its name cannot be changed. - На данную колонку ссылается внешний ключ в таблице %1, поэтому ее имя не может быть изменено. + На эту колонку ссылается внешний ключ в таблице %1, поэтому ее имя не может быть изменено. @@ -1682,12 +1695,13 @@ Do you want to apply the edited data to row=%1, column=%2? Column '%1' has duplicate data. - + Столбец '%1' содержит повторяющиеся данные. + This makes it impossible to enable the 'Unique' flag. Please remove the duplicate data, which will allow the 'Unique' flag to then be enabled. - + Это делает невозможным включение флага «Уникальный». Пожалуйста, удалите дублирующиеся данные, что позволит включить флаг «Уникальный». @@ -1709,7 +1723,8 @@ All data currently stored in this field will be lost. Please add a field which meets the following criteria before setting the on conflict action: - Primary key flag set - + Пожалуйста, добавьте поле, которое соответствует следующим критериям, прежде чем устанавливать действие при конфликте: +- Установлен флаг первичного ключа @@ -1784,7 +1799,7 @@ All data currently stored in this field will be lost. Windows: CR+LF (\r\n) - + Windows: CR+LF (\r\n) @@ -1822,7 +1837,7 @@ All data currently stored in this field will be lost. Error while writing the file '%1': %2 - + Ошибка при записи файла '%1': %2 @@ -1847,7 +1862,7 @@ All data currently stored in this field will be lost. Export finished with errors. - + Экспорт завершен с ошибками. @@ -1880,7 +1895,7 @@ All data currently stored in this field will be lost. Keep column names in INSERT INTO - Имя столбцов в выражении INSERT INTO + Сохранить названия столбцов в выражении INSERT INTO @@ -1900,12 +1915,12 @@ All data currently stored in this field will be lost. Keep original CREATE statements - + Сохранить оригинальные инструкции CREATE Keep old schema (CREATE TABLE IF NOT EXISTS) - Проверять существование таблицы (CREATE TABLE IF NOT EXISTS) + Сохранять схему (CREATE TABLE IF NOT EXISTS) @@ -1944,23 +1959,23 @@ All data currently stored in this field will be lost. Ctrl+H - + Ctrl+H Ctrl+F - + Ctrl+F Ctrl+P - + Ctrl+P Find... - + Поиск... @@ -1978,7 +1993,7 @@ All data currently stored in this field will be lost. Use as Exact Filter - Использовать как Точный Фильтр + Использовать как точный фильтр @@ -1988,7 +2003,7 @@ All data currently stored in this field will be lost. Not containing - + Не содержит @@ -2023,22 +2038,22 @@ All data currently stored in this field will be lost. Regular expression - + Регулярное выражение Edit Conditional Formats... - + Редактировать формат... Set to NULL - Сбросить в NULL + Установить в NULL Cut - + Вырезать @@ -2068,22 +2083,22 @@ All data currently stored in this field will be lost. Use in Filter Expression - Использовать в Выражении Фильтра + Использовать в выражении фильтра Alt+Del - + Alt+Del Ctrl+Shift+C - + Ctrl+Shift+C Ctrl+Alt+C - + Ctrl+Alt+C @@ -2095,12 +2110,12 @@ Do you want to insert it anyway? <p>Not all data has been loaded. <b>Do you want to load all data before selecting all the rows?</b><p><p>Answering <b>No</b> means that no more data will be loaded and the selection will not be performed.<br/>Answering <b>Yes</b> might take some time while the data is loaded but the selection will be complete.</p>Warning: Loading all the data might require a great amount of memory for big tables. - + <p>Не все данные загружены. <b>Хотите ли вы загрузить все данные перед выбором всех строк?</b><p><p>Ответ<b>Нет</b> означает, что больше данные загружены не будут и выбор не будет выполнен.<br/>Ответ <b>Да</b> Загрузка данных может занять некоторое время, но выборка будет завершена.</p>Предупреждение: загрузка всех данных может потребовать большого объема памяти для больших таблиц. Cannot set selection to NULL. Column %1 has a NOT NULL constraint. - + Невозможно установить выборку в NULL. Столбец %1 имеет ограничение NOT NULL. @@ -2144,7 +2159,7 @@ Do you want to insert it anyway? *.extension - + *.extension @@ -2168,57 +2183,68 @@ The following operators are also supported: <> Unequal: exact inverse match x~y Range: values between x and y /regexp/ Values matching the regular expression - + Эти поля ввода позволяют выполнять быстрые фильтры в текущей выбранной таблице. +По умолчанию строки, содержащие введенный текст, отфильтровываются. +Поддерживаются следующие операторы: +% Подстановочный знак +> Больше +< Меньше +>= Равно или больше +<= Равно или меньше += Равно: точное совпадение +<> Неравно: точное обратное совпадение +x~y Диапазон: значения между x и y +/regexp/ Значения, соответствующие регулярному выражению Clear All Conditional Formats - + Очистить все форматирование Use for Conditional Format - + Использовать формат Edit Conditional Formats... - + Редактировать форматы... Set Filter Expression - Установить Выражение Фильтра + Установить выражение фильтра What's This? - Что Это? + Что это? Is NULL - NULL + Is NULL Is not NULL - не NULL + Не NULL Is empty - пусто + Пусто Is not empty - не пусто + Не пусто Not containing... - + Не содержит... @@ -2258,7 +2284,7 @@ x~y Range: values between x and y Regular expression... - + Регулярное выражение... @@ -2266,7 +2292,7 @@ x~y Range: values between x and y Find and Replace - Поиск и Замена + Поиск и замена @@ -2311,12 +2337,12 @@ x~y Range: values between x and y <html><head/><body><p>When checked, the pattern to find is searched only in the current selection.</p></body></html> - + <html><head/><body><p>Если этот флажок установлен, поиск по искомому шаблону выполняется только в текущем выделенном фрагменте.</p></body></html> &Selection only - + Только в в&ыделенном @@ -2341,7 +2367,7 @@ x~y Range: values between x and y F3 - + F3 @@ -2409,7 +2435,7 @@ x~y Range: values between x and y Foreign key clauses (ON UPDATE, ON DELETE etc.) - Условия (ON UPDATE, ON DELETE и т.д.) + Действия внешнего ключа (ON UPDATE, ON DELETE и т.д.) @@ -2417,32 +2443,32 @@ x~y Range: values between x and y Image Viewer - + Просмотр графики Reset the scaling to match the original size of the image. - + Сброс масштаба до исходного размера изображения. Set the scaling to match the size of the viewport. - + Подогнать масштаб к размеру окна. Print... - Печать... + Печать... Open preview dialog for printing displayed image - Открыть диалоговое окно предварительного просмотра для печати отображаемого изображения + Открыть диалоговое окно предварительного просмотра для печати отображаемого изображения Ctrl+P - + Ctrl+P @@ -2450,7 +2476,7 @@ x~y Range: values between x and y Import CSV file - Импортировать файл в формате CSV + Импортировать CSV файл @@ -2502,13 +2528,13 @@ x~y Range: values between x and y Other (printable) - + Другой (для печати) Other (code) - + Другой (код) @@ -2588,48 +2614,48 @@ x~y Range: values between x and y Use local number conventions - + Использовать местные правила нумерации Use decimal and thousands separators according to the system locale. - + Используйте десятичные разделители и разделители тысяч в соответствии с языковыми настройками системы. When importing into an existing table with a primary key, unique constraints or a unique index there is a chance for a conflict. This option allows you to select a strategy for that case: By default the import is aborted and rolled back but you can also choose to ignore and not import conflicting rows or to replace the existing row in the table. - + При импорте в существующую таблицу с первичным ключом, уникальными ограничениями или уникальным индексом существует вероятность конфликта. Эта опция позволяет выбрать стратегию для этого случая: по умолчанию импорт прерывается и откатывается, но вы также можете выбрать игнорирование и не импортировать конфликтующие строки или замену существующей строки в таблице. Abort import - + Прервать импорт Ignore row - + Игнорировать строку Replace existing row - + Заменить существующую строку Conflict strategy - + Действия при конфликте Deselect All - Отменить Выбор + Снять выделение Match Similar - Найти Совпадения + Найти совпадения @@ -2664,12 +2690,12 @@ x~y Range: values between x and y Could not prepare INSERT statement: %1 - + Не удалось подготовить оператор INSERT: %1 Unexpected end of file. Please make sure that you have configured the correct quote characters and the file is not malformed. - + Неожиданный конец файла. Убедитесь, что вы настроили правильные символы кавычек и файл не имеет неправильного формата. @@ -2687,12 +2713,12 @@ x~y Range: values between x and y DB Browser for SQLite - Обозреватель для SQLite + DB Browser for SQLite toolBar1 - панельИнструментов1 + ПанельИнструментов1 @@ -2702,161 +2728,161 @@ x~y Range: values between x and y Temp Store - + Temp Store (Временное хранилище) Secure Delete - + Secure Delete (Безопасное удаление) Case Sensitive Like - + Case Sensitive Like (Регистрозависимый Like) Journal Mode - + Journal Mode (Режим журнала) Journal Size Limit - + Journal Size Limit (Лимит размера журнала) Recursive Triggers - + Recursive Triggers (Рекурсивные триггеры) Page Size - + Page Size (Размер страницы) Foreign Keys - + Foreign Keys (Внешние ключи) Auto Vacuum - + Auto Vacuum (Автовакуум) Max Page Count - + Max Page Count (Макс число страниц) Checkpoint Full FSYNC - + Checkpoint Full FSYNC Off - + Откл Normal - + Обычный Full - + Полный Default - По умолчанию + По умолчанию File - Файл + Файл Memory - + Память Delete - + Удаление Truncate - + Усекать Persist - + Persist WAL - + WAL Exclusive - + Эксклюзив Automatic Index - + Automatic Index (Автоиндексирование) Ignore Check Constraints - + Ignore Check Constraints (Игнорировать Check Constraints) Full FSYNC - + Full FSYNC WAL Auto Checkpoint - + WAL Auto Checkpoint User Version - + User Version (Версия пользователя) Synchronous - + Synchronous (Синхронизация) None - Нет + Нет Incremental - + Инкремент Locking Mode - + Locking Mode (Режим блокировки) @@ -2866,7 +2892,7 @@ x~y Range: values between x and y Error Log - + Журнал ошибок @@ -2885,7 +2911,7 @@ You can drag multiple object names from the Name column and drop them into the S You can drag SQL statements from the Schema column and drop them into the SQL editor or into other applications. Это структура открытой БД. -Вы можете перетащить несколько имен объектов из столбца "Имя" и отбросить их в редактор SQL, и вы можете настроить свойства сброшенных имен с помощью контекстного меню. Это поможет вам при составлении SQL-инструкций. +Вы можете перетащить несколько имен объектов из столбца "Имя" в редактор SQL, и вы можете настроить свойства перемещенных имен с помощью контекстного меню. Это поможет вам при составлении SQL-инструкций. Вы можете перетаскивать операторы SQL из столбца "Схема" и переносить их в редактор SQL или в другие приложения. @@ -2893,12 +2919,12 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Project Toolbar - Панель Инструментов Проекта + Панель инструментов проекта Extra DB toolbar - Дополнительная Панель Инструментов БД + Дополнительная панель инструментов БД @@ -2910,34 +2936,34 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed &New Database - + &Новая БД Ctrl+F4 - + Ctrl+F4 &Undo - + &Отменить Undo last change to the database - + Отменить последнее изменение в БД This action undoes the last change performed to the database in the Database Browser or in Execute SQL. Redoing is not possible. - + Это действие отменяет последнее изменение, выполненное в базе данных в Database Browser или в Execute SQL. Повтор невозможен. Open the Modify Table wizard, where it is possible to rename an existing table. It is also possible to add or delete fields from a table, as well as modify field names and types. - + Открывает мастер изменения таблицы, где можно переименовать существующую таблицу. Также можно добавлять или удалять поля из таблицы, а также изменять имена и типы полей. @@ -2947,7 +2973,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed New &tab - + Новая вкладка редактора SQL @@ -2967,7 +2993,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Ctrl+Shift+T - + Ctrl+Shift+T @@ -2977,7 +3003,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Execute line - + Выполнить строку @@ -2992,7 +3018,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed F1 - + F1 @@ -3017,7 +3043,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed &Save Project - + Сохранить проект @@ -3027,7 +3053,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Open &Project - + Открыть проект @@ -3058,7 +3084,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed SQLCipher &FAQ - + SQLCipher &FAQ @@ -3073,7 +3099,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Ctrl+Shift+O - + Ctrl+Shift+O @@ -3099,7 +3125,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Find - + Найти @@ -3109,7 +3135,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Ctrl+F - + Ctrl+F @@ -3120,7 +3146,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Find or replace - + Найти или заменить @@ -3130,7 +3156,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Ctrl+H - + Ctrl+H @@ -3140,7 +3166,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Export to &JSON - + Экспорт в JSON @@ -3155,127 +3181,127 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Shows or hides the Project toolbar. - Показывает или скрывает панель инструментов Проекта. + Показывает или скрывает панель инструментов проекта. Extra DB Toolbar - Дополнительная Панель Инструментов БД + Дополнительная панель инструментов БД Ctrl+Shift+W - + Ctrl+Shift+W Table from CSV data in Clipboard... - + Таблица из CSV данных в буфере обмена... This treats the current clipboard contents as a CSV file and opens the same import wizard that is used for importing CSV data from a file. - + При этом текущее содержимое буфера обмена обрабатывается как CSV-файл и открывается тот же мастер импорта, который используется для импорта CSV-данных из файла. Show &Row Counts - + Показать счетчик строк This shows the number of rows for each table and view in the database. - + Здесь отображается количество строк для каждой таблицы и представления в базе данных. Save Database &As... - + Сохранить БД как... Save the current database as a different file - + Сохранить текущую БД как другой файл Refresh - Обновить + Обновить Reload the database structure - + Перезагрузить структуру базы данных Open SQL file(s) - + Открыть SQL файл(ы) &Database Structure This has to be equal to the tab title in all the main tabs - + Структура БД &Browse Data This has to be equal to the tab title in all the main tabs - + Просмотр данных Edit P&ragmas This has to be equal to the tab title in all the main tabs - + Прагмы E&xecute SQL This has to be equal to the tab title in all the main tabs - + Редактор SQL &Recent Files - + Недавние &файлы This button opens files containing SQL statements and loads them in new editor tabs - + Эта кнопка открывает файлы, содержащие SQL инструкции и загружает их в новую вкладку редактора This button lets you open a DB Browser for SQLite project file - + Эта кнопка позволяет открыть браузер БД для файла проекта SQLite &Open Database - + &Открыть БД New In-&Memory Database - Новая БД в &Памяти + Новая БД в па&мяти Drag && Drop SELECT Query - + Перетаскивание запроса SELECT When dragging fields from the same table or a single table, drop a SELECT query into the editor - + При перетаскивании полей из той же таблицы или из одной таблицы, перетащите запрос SELECT в редактор @@ -3378,104 +3404,104 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Un/comment block of SQL code - + Рас/комментировать блок SQL кода Un/comment block - + Рас/комментировать блок Comment or uncomment current line or selected block of code - + Рас/комментировать текущую строку выбранного блока кода Comment or uncomment the selected lines or the current line, when there is no selection. All the block is toggled according to the first line. - + Комментировать или раскомментировать выделенные строки или текущую строку, если нет выделения. Весь блок переключается в соответствии с первой строкой. Ctrl+/ - + Ctrl+/ Stop SQL execution - + Остановить выполнение SQL Stop execution - + Остановить выполнение Stop the currently running SQL script - + Остановить выполняемый SQL скрипт Browse Table - + Просмотр таблицы Close Pro&ject - + Закрыть проект Close project and database files and return to the initial state - + Закрыть проект и БД файлы и вернуться в исходное состояние Ctrl+Shift+F4 - + Ctrl+Shift+F4 Detach Database - + Отсоединить БД Detach database file attached to the current database connection - + Отсоединить файл базы данных, прикрепленный к текущему подключению &Save Project As... - + Сохранить проект как... Save the project in a file selected in a dialog - + Сохраните проект в файле, выбранном в диалоговом окне Save A&ll - + Сохранить все Save DB file, project file and opened SQL files - + Сохранить файл базы данных, файл проекта и открытые файлы SQL Ctrl+Shift+S - + Ctrl+Shift+S @@ -3536,7 +3562,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Shift+F5 - + Shift+F5 @@ -3603,7 +3629,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Ctrl+N - + Ctrl+N @@ -3630,7 +3656,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Ctrl+O - + Ctrl+O @@ -3646,7 +3672,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Ctrl+W - + Ctrl+W @@ -3673,7 +3699,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Ctrl+S - + Ctrl+S @@ -3699,7 +3725,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Ctrl+Q - + Ctrl+Q @@ -3724,7 +3750,7 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Export a database to a .sql dump text file. - Экспортировать базу данных в текстовый файл .sql. + Экспортировать базу данных в текстовый дамп файл .sql. @@ -3780,17 +3806,17 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed Shift+F1 - + Shift+F1 &Recently opened - &Недавно открываемые + &Недавние файлы Ctrl+T - + Ctrl+T @@ -3859,7 +3885,7 @@ You can drag SQL statements from an object row and drop them into other applicat &Modify Table... - &Изменить таблицу... + &Модифицировать таблицу... @@ -3874,7 +3900,7 @@ You can drag SQL statements from an object row and drop them into other applicat &Execute SQL - В&ыполнить код SQL + В&ыполнить SQL @@ -3886,7 +3912,7 @@ You can drag SQL statements from an object row and drop them into other applicat Ctrl+E - + Ctrl+E @@ -3902,12 +3928,12 @@ You can drag SQL statements from an object row and drop them into other applicat Save the current session to a file - Сохранить текущее состояние в файл + Сохранить текущее состояние в файл This button lets you save all the settings associated to the open DB to a DB Browser for SQLite project file - + Эта кнопка позволяет сохранить все настройки, связанные с открытой базой данных, в файле проекта браузера баз данных для SQLite @@ -3928,43 +3954,43 @@ You can drag SQL statements from an object row and drop them into other applicat Ctrl+Return - + Ctrl+Return Ctrl+L - + Ctrl+L Ctrl+P - + Ctrl+P Ctrl+D - + Ctrl+D Ctrl+I - + Ctrl+I Reset Window Layout - + Сбросить структуру окна The database is currently busy. - + База данных сейчас занята. Click here to interrupt the currently running query. - + Нажмите здесь, чтобы прервать текущий выполняемый запрос. @@ -4014,7 +4040,7 @@ Reason: %1 A new DB Browser for SQLite version is available (%1.%2.%3).<br/><br/>Please download at <a href='%4'>%4</a>. - Вышла новая версия Обозревателя для SQLite (%1.%2.%3).<br/><br/>Она доступна для скачивания по адресу <a href='%4'>%4</a>. + Вышла новая версия DB Browser for SQLite (%1.%2.%3).<br/><br/>Она доступна для скачивания по адресу <a href='%4'>%4</a>. @@ -4055,87 +4081,87 @@ If you answer no we will attempt to import the data in the SQL file to the curre Ctrl+Tab - + Ctrl+Tab Ctrl+Shift+Tab - + Ctrl+Shift+Tab Clear List - + Очистить список Window Layout - + Расположение окон Ctrl+0 - + Ctrl+0 Simplify Window Layout - + Упорядочить окна Alt+Shift+0 - + Alt+Shift+0 Dock Windows at Bottom - + Закрепить окна снизу Dock Windows at Left Side - + Закрепить окна слева Dock Windows at Top - + Закрепить окна сверху Alt+Shift+W - + Alt+Shift+W Choose a database file to save under - + Выберите файл БД для сохранения Error while saving the database to the new file. - + Ошибка при сохранении БД в новый файл. You are still executing SQL statements. Closing the database now will stop their execution, possibly leaving the database in an inconsistent state. Are you sure you want to close the database? - + Вы все еще выполняете SQL-выражения. Закрытие базы данных сейчас остановит их выполнение, возможно, оставив базу данных в несогласованном состоянии. Вы уверены, что хотите закрыть базу данных? Do you want to save the changes made to the project file '%1'? - + Хотите сохранить изменения, внесенные в файл проекта «%1»? Edit View %1 - + Редактировать представление %1 Edit Trigger %1 - + Редактировать триггер %1 @@ -4180,52 +4206,52 @@ If you answer no we will attempt to import the data in the SQL file to the curre Opened '%1' in read-only mode from recent file list - + Открыт '%1' в режиме только для чтения из списка последних файлов Opened '%1' from recent file list - + Открыт '%1' из списка последних файлов &%1 %2%3 - &%1 %2%3 + &%1 %2%3 (read only) - + (только для чтения) Open Database or Project - + Attach Database... - + Присоединить БД... Import CSV file(s)... - + Импорт CSV файла(ов)... Do you want to save the changes made to SQL tabs in a new project file? - + Хотите ли вы сохранить изменения, внесенные во вкладки SQL, в новом файле проекта? Do you want to save the changes made to the SQL file %1? - + Хотите сохранить изменения, внесенные в файл SQL %1? Could not find resource file: %1 - + Could not find resource file: %1 @@ -4235,28 +4261,29 @@ If you answer no we will attempt to import the data in the SQL file to the curre DB file '%1' could not be opened - + Файл базы данных '%1' не может быть открыт Table '%1' not found; settings ignored - + Таблица '%1' не найдена; настройки игнорируются Could not open project file for writing. Reason: %1 - + Не удалось открыть файл проекта для записи. +Причина: %1 -- Reference to file "%1" (not supported by this version) -- - + -- Ссылка на файл "%1" (не поддерживается этой версией) -- Busy (%1) - + Занят (%1) @@ -4271,12 +4298,12 @@ Reason: %1 Execution finished with errors. - + Выполнено с ошибками. Execution finished without errors. - + Выполнено без ошибок. @@ -4298,7 +4325,7 @@ Are you sure? Automatically load the last opened DB file at startup - + Автоматически загружать последнюю БД при запуске @@ -4346,7 +4373,7 @@ All data associated with the table will be lost. Message from database engine: %1 - Сообщение от СУБД: + Сообщение от DB-engine: %1 @@ -4359,7 +4386,7 @@ Are you sure you want to save the database? You are already executing SQL statements. Do you want to stop them in order to execute the current statements instead? Note that this might leave the database in an inconsistent state. - + Вы уже выполняете SQL-выражения. Хотите остановить их, чтобы вместо этого выполнить текущие выражения? Обратите внимание, что это может привести базу данных в несогласованное состояние. @@ -4379,24 +4406,24 @@ Are you sure you want to save the database? -- EXECUTING ALL IN '%1' -- - -- ВЫПОЛНЕНИЕ ВСЕ В '%1' + -- ВЫПОЛНЕНИЕ ВСЕГО В '%1' -- At line %1: - + На строке %1: Result: %1 - + Результат: %1 Result: %2 - + Результат: %2 @@ -4409,7 +4436,7 @@ Are you sure? Select the action to apply to the dropped file(s). <br/>Note: only 'Import' will process more than one file. Note for translation: Although there is no %n in the original, you can use the numerus-form to adjust 'files(s)' and remove the note when n = 1. Including %n in the translation will also work. - + @@ -4418,52 +4445,52 @@ Are you sure? The statements in the tab '%1' are still executing. Closing the tab will stop the execution. This might leave the database in an inconsistent state. Are you sure you want to close the tab? - + Операторы на вкладке '%1' все еще выполняются. Закрытие вкладки остановит выполнение. Это может привести к тому, что база данных окажется в несогласованном состоянии. Вы уверены, что хотите закрыть вкладку? This project file is using an old file format because it was created using DB Browser for SQLite version 3.10 or lower. Loading this file format is no longer fully supported. If you want to load it completely, please use DB Browser for SQLite version 3.12 to convert it to the new file format. - + Этот файл проекта использует старый формат файла, поскольку он был создан с помощью DB Browser for SQLite версии 3.10 или ниже. Загрузка этого формата файла больше не поддерживается полностью. Если вы хотите загрузить его полностью, используйте DB Browser for SQLite версии 3.12, чтобы преобразовать его в новый формат файла. Yes. Don't ask again - + Да. Не спрашивать снова This action will open a new SQL tab with the following statements for you to edit and run: - + Это действие откроет новую вкладку SQL со следующими операторами, которые вы можете редактировать и выполнять: Rename Tab - + Переименовать вкладку Duplicate Tab - + Дублировать вкладку Close Tab - + Закрыть вкладку Opening '%1'... - + Открывается '%1'... There was an error opening '%1'... - + Произошла ошибка при открытии '%1'... Value is not a valid URL or filename: %1 - + Значение не является допустимым URL-адресом или именем файла: %1 @@ -4483,17 +4510,17 @@ Are you sure? Modify Index - Модифицировать Индекс + Модифицировать индекс Modify Table - Модифицировать Таблицу + Модифицировать таблицу Do you want to save the changes made to SQL tabs in the project file '%1'? - + Хотите сохранить изменения, внесенные во вкладки SQL в файле проекта «%1»? @@ -4534,26 +4561,26 @@ Are you sure? Project saved to file '%1' - + Проект сохранен в файл '%1' Collation needed! Proceed? - Нужно выполнить сопоставление! Продолжить? + Нужно Collation! Продолжить? A table in this database requires a special collation function '%1' that this application can't provide without further knowledge. If you choose to proceed, be aware bad things can happen to your database. Create a backup! - Таблица в базе данных требует выполнения специальной функции сопоставления '%1'. + Таблица в базе данных требует наличия функции сопоставления (Collation)'%1'.чего это приложение не может предоставить без дополнительных знаний Если вы продолжите, то возможна порча вашей базы данных. Создайте резервную копию! creating collation - + создание Collation @@ -4601,7 +4628,7 @@ Create a backup! Alt+Del - + Alt+Del @@ -4629,17 +4656,17 @@ Create a backup! Y1 - + Y1 Y2 - + Y2 Axis Type - Ось + Тип оси @@ -4805,7 +4832,7 @@ Select the axes or axes labels to drag and zoom only in that orientation. Help - + Помощь @@ -4815,12 +4842,12 @@ Select the axes or axes labels to drag and zoom only in that orientation. Stacked bars - + Сложенные панели Fixed number format - + Фиксированный формат чисел @@ -4878,12 +4905,12 @@ Warning: not all data has been fetched from the table yet due to the partial fet There are curves in this plot and the selected line style can only be applied to graphs sorted by X. Either sort the table or query by X to remove curves or select one of the styles supported by curves: None or Line. - На этом графике есть кривые, и выбранный стиль линии может применяться только к графикам, отсортированным по X. Либо сортируйте таблицу или запрос по X, чтобы удалить кривые, либо выберите один из стилей, поддерживаемых кривыми: None или Line. + На этом графике есть кривые, и выбранный стиль линии может применяться только к графикам, отсортированным по X. Либо сортируйте таблицу или запрос по X, чтобы удалить кривые, либо выберите один из стилей, поддерживаемых кривыми: Нет или Обычная. Loading all remaining data for this table took %1ms. - + Загрузка всех оставшихся данных для этой таблицы заняла %1мс. @@ -4901,12 +4928,12 @@ Warning: not all data has been fetched from the table yet due to the partial fet Database &encoding - &Кодировка базы данных + &Кодировка текста БД Open databases with foreign keys enabled. - Открывать базы данных с включенными внешними ключами. + Открывать базы данных с поддержкой внешних ключей. @@ -4974,7 +5001,7 @@ Warning: not all data has been fetched from the table yet due to the partial fet SQ&L to execute after opening database - + SQL для выполнения после открытия базы данных @@ -5049,17 +5076,17 @@ Warning: not all data has been fetched from the table yet due to the partial fet Threshold for completion and calculation on selection - + Порог для завершения и расчета по выделенному Show images in cell - + Показывать изображения в ячейке Enable this option to show a preview of BLOBs containing image data in the cells. This can affect the performance of the data browser, however. - + Включите эту опцию, чтобы отобразить предварительный просмотр BLOB-объектов, содержащих данные изображений в ячейках. Однако это может повлиять на производительность браузера данных. @@ -5206,7 +5233,7 @@ Warning: not all data has been fetched from the table yet due to the partial fet The text appears beside the icon - Текст над иконкой + Текст рядом с иконкой @@ -5239,73 +5266,74 @@ Warning: not all data has been fetched from the table yet due to the partial fet Main Window - + Главное окно Database Structure - Структура БД + Структура БД Browse Data - Данные + Просмотр данных Execute SQL - SQL + Редактор SQL Edit Database Cell - Редактирование ячейки БД + Правка ячейки БД When this value is changed, all the other color preferences are also set to matching colors. - + При изменении этого значения все остальные цветовые настройки также устанавливаются на соответствующие цвета. Follow the desktop style - + Стиль рабочего стола Dark style - + Темный Application style - + Стиль приложения This sets the font size for all UI elements which do not have their own font size option. - + Размер шрифта для всех элементов пользовательского интерфейса, которые не имеют собственной опции размера шрифта. Font size - + Размет шрифта Max Recent Files - + Кол. недавних файлов Prompt to save SQL tabs in new project file - + Предлагать сохранение вкладок SQL +в новом файле проекта If this is turned on, then changes to the SQL editor generate a save a project confirmation dialog when closing the SQL editor tab. - + Если эта опция включена, то изменения в редакторе SQL при закрытии вкладки редактора SQL будут приводить к появлению диалогового окна подтверждения сохранения проекта. @@ -5315,7 +5343,7 @@ in new project file Database structure font size - + Рзмер шрифта во вкладке структуры БД @@ -5328,7 +5356,10 @@ in new project file Maximum number of rows in a table for enabling the value completion based on current values in the column. Maximum number of indexes in a selection for calculating sum and average. Can be set to 0 for disabling the functionalities. - + Это максимальное количество элементов, разрешенное для включения некоторых вычислительно затратных функций: +Максимальное количество строк в таблице для включения автодополнения значений на основе текущих значений в столбце. +Максимальное количество индексов в выборке для расчета суммы и среднего. +Можно установить на 0 для отключения функций. @@ -5345,7 +5376,7 @@ Can be set to 0 for disabling completion. Light style - + Светлый @@ -5355,7 +5386,7 @@ Can be set to 0 for disabling completion. Formatted - + Отформатированный @@ -5367,7 +5398,7 @@ Can be set to 0 for disabling completion. Click to set this color - + Клик для выбора цвета @@ -5383,7 +5414,7 @@ Can be set to 0 for disabling completion. Preview only (N/A) - Предв. просмотр + Только просмотр @@ -5393,32 +5424,32 @@ Can be set to 0 for disabling completion. Selection background - + Фон выделения Selection foreground - + Выделение на переднем плане Highlight - + Выделение SQL &results font size - &Размер шрифта + &Размер шрифта в окне результата SQL Use tabs for indentation - + Табуляции для отступов When set, the Tab key will insert tab and space characters for indentation. Otherwise, just spaces will be used. - + При установке, клавишей Tab будут вставляться символы табуляции и пробела для отступа. В противном случае будут использоваться только пробелы. @@ -5488,12 +5519,12 @@ Can be set to 0 for disabling completion. Close button on tabs - + Кнопка закрытия на вкладках If enabled, SQL editor tabs will have a close button. In any case, you can use the contextual menu or the keyboard shortcut to close them. - + Если включено, вкладки редактора SQL будут иметь кнопку закрытия. В любом случае, вы можете использовать контекстное меню или сочетание клавиш, чтобы закрыть их. @@ -5518,7 +5549,7 @@ Can be set to 0 for disabling completion. Select built-in extensions to load for every database: - + Выбрать встроенные расширения для загрузки для каждой базы данных: @@ -5533,12 +5564,12 @@ Can be set to 0 for disabling completion. <html><head/><body><p>SQLite provides an SQL function for loading extensions from a shared library file. Activate this if you want to use the <span style=" font-style:italic;">load_extension()</span> function from SQL code.</p><p>For security reasons, extension loading is turned off by default and must be enabled through this setting. You can always load extensions through the GUI, even though this option is disabled.</p></body></html> - + <html><head/><body><p>SQLite предоставляет функцию SQL для загрузки расширений из файла общей библиотеки. Активируйте ее, если хотите использовать функцию <span style=" font-style:italic;">load_extension()</span> из кода SQL.</p><p>В целях безопасности загрузка расширений по умолчанию отключена и должна быть включена с помощью этого параметра. Вы всегда можете загружать расширения через графический интерфейс, даже если эта опция отключена.</p></body></html> Allow loading extensions from SQL code - + Разрешить загрузку расширений из кода SQL @@ -5553,61 +5584,61 @@ Can be set to 0 for disabling completion. Proxy - + Прокси Configure - + Настроить Export Settings - + Экспорт настроек Import Settings - + Импорт настроек Subject CN - + Субъект Common Name - + Общепринятое имя Subject O - + Организация Organization - + Организация Valid from - + Действует с Valid to - + Действует по Serial number - + Серийный № @@ -5622,17 +5653,17 @@ Can be set to 0 for disabling completion. Subject Common Name - + Общепринятое имя Субъекта Issuer CN - + Издатель Issuer Common Name - + общепринятое имя издателя @@ -5659,7 +5690,7 @@ Can be set to 0 for disabling completion. Extensions(*.so *.dylib *.dll);;All files(*) - + Расширения(*.so *.dylib *.dll);;All files(*) @@ -5686,35 +5717,37 @@ All your preferences will be lost and default values will be used. Save Settings File - + Сохранить файл настроек Initialization File (*.ini) - + Файл инициализации(*.ini) The settings file has been saved in location : - + Файл настроек сохранен в: + Open Settings File - + Открыть файл настроек The settings file was loaded properly. - + Файл настроек загружен корректно. The selected settings file is not a normal settings file. Please check again. - + Выбранный файл настроек не является обычным файлом настроек. +Проверьте еще раз. @@ -5722,57 +5755,57 @@ Please check again. Proxy Configuration - + Конфигурация прокси-сервера Pro&xy Type - + Тип прокси Host Na&me - + Имя хоста Port - + Порт Authentication Re&quired - + Требуется аутентификация &User Name - + Пользователь Password - + Пароль None - Нет + Нет System settings - + Системные настройки HTTP - + SOCKS5 - + @@ -5816,102 +5849,103 @@ Please check again. Left - + Влево Right - + Вправо Center - + По центру Justify - + По ширине SQLite Database Files (*.db *.sqlite *.sqlite3 *.db3) - + SQLite Database файлы (*.db *.sqlite *.sqlite3 *.db3) DB Browser for SQLite Project Files (*.sqbpro) - + DB Browser for SQLite Project файлы (*.sqbpro) SQL Files (*.sql) - + SQL файлы (*.sql) All Files (*) - + Все файлы (*) Text Files (*.txt) - + Текстовые файлы (*.txt) Comma-Separated Values Files (*.csv) - + Файлы с разделителями запятой (*.csv) Tab-Separated Values Files (*.tsv) - + Файлы с разделением табуляцией (*.tsv) Delimiter-Separated Values Files (*.dsv) - + Файлы с разделителями значений + Delimiter-Separated Values Files (*.dsv) Concordance DAT files (*.dat) - + DAT файлы (*.dat) JSON Files (*.json *.js) - + JSON файлы (*.json *.js) XML Files (*.xml) - + XML файлы (*.xml) Binary Files (*.bin *.dat) - + Двоичные файлы (*.bin *.dat) SVG Files (*.svg) - + SVG файлы (*.svg) Hex Dump Files (*.dat *.bin) - + Файлы шестнадцатиричных дампов Extensions (*.so *.dylib *.dll) - + Расширения (*.so *.dylib *.dll) Initialization File (*.ini) - + Файл инициализации (*.ini) @@ -5994,37 +6028,37 @@ Please check again. Commit ID - + Коммит ID Message - + Комментарий Date - Дата + Дата Author - + Автор Size - Размер + Размер Authored and committed by %1 - + Автор и коммит %1 Authored by %1, committed by %2 - + Автор %1, коммит %2 @@ -6054,7 +6088,7 @@ Please check again. Identity - ID + Удостоверение @@ -6064,172 +6098,172 @@ Please check again. Upload - + Загрузить DBHub.io - + <html><head/><body><p>In this pane, remote databases from dbhub.io website can be added to DB Browser for SQLite. First you need an identity:</p><ol style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Login to the dbhub.io website (use your GitHub credentials or whatever you want)</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Click the button to &quot;Generate client certificate&quot; (that's your identity). That'll give you a certificate file (save it to your local disk).</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Go to the Remote tab in DB Browser for SQLite Preferences. Click the button to add a new certificate to DB Browser for SQLite and choose the just downloaded certificate file.</li></ol><p>Now the Remote panel shows your identity and you can add remote databases.</p></body></html> - + <html><head/><body><p>На этой панели БД с dbhub.io может быть добавлена в DB Browser for SQLite. Сначала вам нужно удостоверение:</p><ol style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Авторизоваться на dbhub.io (используйте ваши учетные данные GitHub или что-то еще, если угодно)</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Нажмите кнопку &quot;Generate client certificate&quot; (это ваше удостоверение). Вы получите файл сертификата (сохраните его на локальный диск).</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Перейдите на вкладку Удаленный сервер настроек DB Browser for SQLite. Добавьтесертификат в DB Browser for SQLite и выберите скачанный файл.</li></ol><p>Теперь на панели «Удалённый сервер» отображается ваше удостоверение, и вы можете добавлять удалённые базы данных.</p></body></html> Local - + Локальный Current Database - + Текущая БД Clone - + Клонировать Branch - Ветка + Ветка Commits - + Коммиты Commits for - + Коммиты для Delete Database - + Удалить БД Delete the local clone of this database - + Удалить локальный клон этой БД Open in Web Browser - + Открыть в Web браузере Open the web page for the current database in your browser - + Открыть веб-страницу текущей базы данных в вашем браузере Clone from Link - + Клонировать из ссылки Use this to download a remote database for local editing using a URL as provided on the web page of the database. - + Используйте это для загрузки удаленной базы данных для локального редактирования с использованием URL-адреса, указанного на веб-странице базы данных. Refresh - Обновить + Обновить Reload all data and update the views - + Перезагрузите все данные и обновите представления Clone Database - + Клонировать БД Open Database - + Открыть БД Open the local copy of this database - + Открыть локально копию этой БД Check out Commit - + Проверить коммит Download and open this specific commit - + Загрузить и открыть этот коммит Check out Latest Commit - + Проверить последний коммит Check out the latest commit of the current branch - + Проверить последний коммит текущей ветки Save Revision to File - + Сохранить версию в файл Saves the selected revision of the database to another file - + Сохранить выбранную версию БД в другой файл Upload Database - + Загрузить БД Upload this database as a new commit - + Загрузить эту БД как новый коммит <html><head/><body><p>You are currently using a built-in, read-only identity. For uploading your database, you need to configure and use your DBHub.io account.</p><p>No DBHub.io account yet? <a href="https://dbhub.io/"><span style=" text-decoration: underline; color:#007af4;">Create one now</span></a> and import your certificate <a href="#preferences"><span style=" text-decoration: underline; color:#007af4;">here</span></a> to share your databases.</p><p>For online help visit <a href="https://dbhub.io/about"><span style=" text-decoration: underline; color:#007af4;">here</span></a>.</p></body></html> - + <html><head/><body><p>Вы используете встроенное удостоверение.Для загрузки БД, нужно настроить аккаунт на DBHub.io.</p><p>Нет аккаунта на DBHub.io? <a href="https://dbhub.io/"><span style=" text-decoration: underline; color:#007af4;">Создайте сейчас</span></a> и импортируйте ваш сертификат <a href="#preferences"><span style=" text-decoration: underline; color:#007af4;">сюда</span></a> для доступа к вашим базам данных.</p><p>Инструкции можно посмотреть здесь <a href="https://dbhub.io/about"><span style=" text-decoration: underline; color:#007af4;">here</span></a>.</p></body></html> &User - + Пользователь &Database - &База данных + &База данных Back - + Назад Select an identity to connect - + Выбрать удостоверение для подключения Public - Публичный + Публичный @@ -6237,43 +6271,47 @@ Please check again. Please enter the URL to clone from. You can generate this URL by clicking the 'Clone Database in DB4S' button on the web page of the database. - + Загрузка базы данных с удаленного сервера для локального редактирования. +Введите URL для клонирования. Вы можете сгенерировать этот URL, +нажав кнопку «Клонировать базу данных в DB4S» на веб-странице +базы данных. Invalid URL: The host name does not match the host name of the current identity. - + Неверный URL: имя хоста не совпадает с именем хоста текущего удостоверения. Invalid URL: No branch name specified. - + Неверный URL: не указано имя ветки. Invalid URL: No commit ID specified. - + Неверный URL: не указан идентификатор фиксации. You have modified the local clone of the database. Fetching this commit overrides these local changes. Are you sure you want to proceed? - + Вы изменили локальный клон базы данных. Извлечение этого коммита переопределит эти локальные изменения. +Вы уверены, что хотите продолжить? The database has unsaved changes. Are you sure you want to push it before saving? - + В базе данных есть несохраненные изменения. Вы уверены, что хотите отправить их перед сохранением? The database you are trying to delete is currently opened. Please close it before deleting. - + База данных, которую вы пытаетесь удалить, в данный момент открыта. Пожалуйста, закройте ее перед удалением. This deletes the local version of this database with all the changes you have not committed yet. Are you sure you want to delete this database? - + Действие удалит локальную версию этой базы данных со всеми изменениями, которые вы еще не зафиксировали. Вы уверены, что хотите удалить эту базу данных? @@ -6281,32 +6319,32 @@ Are you sure you want to proceed? Name - Имя + Имя Branch - Ветка + Ветка Last modified - Изменен + Изменен Size - Размер + Размер Commit - Коммит + Коммит File - Файл + Файл @@ -6334,22 +6372,22 @@ Are you sure you want to proceed? Size: - + Размер: Last Modified: - + Изменен: Licence: - + Лицензия: Default Branch: - + Ветка по умолчанию: @@ -6357,48 +6395,48 @@ Are you sure you want to proceed? Choose a location to save the file - + Выбрать месторасположение для сохранения файла Error opening remote file at %1. %2 - Ошибка открытия файла %1. + Ошибка открытия файла %1. %2 Error: Invalid client certificate specified. - Ошибка: Указан неверный сертификат клиента. + Ошибка: Указан неверный сертификат клиента. Please enter the passphrase for this client certificate in order to authenticate. - Пожалуйста введите ключевую фразу для этого сертификата клиента. + Пожалуйста введите ключевую фразу для этого сертификата клиента. Cancel - Отменить + Отменить Uploading remote database to %1 - Загружается удаленная БД в + Загружается удаленная БД в %1 Downloading remote database from %1 - Скачивается удаленная БД из + Скачивается удаленная БД из %1 Error: Cannot open the file for sending. - Ошибка: не удается открыть файл для отправки. + Ошибка: не удается открыть файл для отправки. @@ -6441,7 +6479,7 @@ Are you sure you want to proceed? Username - + Пользователь @@ -6464,22 +6502,22 @@ Are you sure you want to proceed? Execution aborted by user - Выполнение прервано пользователем + Выполнение прервано пользователем , %1 rows affected - , %1 строк изменено + , %1 строк затронуто query executed successfully. Took %1ms%2 - запрос успешно выполнен. Заняло %1мс%2 + запрос успешно выполнен. Заняло %1мс%2 executing query - + выполнение запроса @@ -6487,12 +6525,12 @@ Are you sure you want to proceed? A&vailable - + Доступные Sele&cted - + Выбранные @@ -6515,7 +6553,7 @@ Are you sure you want to proceed? Shift+F3 - + Shift+F3 @@ -6545,7 +6583,7 @@ Are you sure you want to proceed? Case Sensitive - Учитывать Регистр + Учитывать регистр @@ -6560,7 +6598,7 @@ Are you sure you want to proceed? F3 - + F3 @@ -6586,7 +6624,7 @@ Are you sure you want to proceed? <html><head/><body><p>Results of the last executed statements.</p><p>You may want to collapse this panel and use the <span style=" font-style:italic;">SQL Log</span> dock with <span style=" font-style:italic;">User</span> selection instead.</p></body></html> - + <html><head/><body><p>Результаты выполнения последних инструкций.</p><p>Вы можете свернуть эту панель и использовать <span style=" font-style:italic;">SQL Log</span> на <span style=" font-style:italic;">Ваш</span> выбор.</p></body></html> @@ -6601,48 +6639,48 @@ Are you sure you want to proceed? Ctrl+PgUp - + Ctrl+PgUp Ctrl+PgDown - + Ctrl+PgDown Couldn't read file "%1": %2. - + Не удалось прочитать файл "%1": %2. Couldn't save file: %1. - Не удалось сохранить файл:%1. + Не удалось сохранить файл:%1. Your changes will be lost when reloading it! - + При повторной загрузке ваши изменения будут потеряны! The file "%1" was modified by another program. Do you want to reload it?%2 - + Файл "%1" был изменен другой программой. Хотите перезагрузить его?%2 Answer "Yes to All" to reload the file on any external update without further prompting. - + Ответ «Да для всех», чтобы перезагрузить файл при любом внешнем обновлении без дополнительных запросов. Answer "No to All" to ignore any external update without further prompting. - + Ответ «Нет для всех», чтобы игнорировать любые внешние обновления без дальнейших запросов. Modifying and saving the file will restore prompting. - + При изменении и сохранении файла появится запрос на восстановление. @@ -6650,7 +6688,7 @@ Are you sure you want to proceed? Ctrl+/ - + Ctrl+/ @@ -6698,7 +6736,7 @@ Are you sure you want to proceed? (X,Y,Z) The iif(X,Y,Z) function returns the value Y if X is true, and Z otherwise. - + (X,Y,Z) iif(условие, значение_1, значение_2) возвращает значение_1 если условие верно, и значение_2 в противном случае. @@ -6724,13 +6762,15 @@ Are you sure you want to proceed? (X) The load_extension(X) function loads SQLite extensions out of the shared library file named X. Use of this function must be authorized from Preferences. - + (X) Функция load_extension(X) загружает расширения SQLite из файла общей библиотеки с именем X. +Использование этой функции должно быть разрешено в настройках. (X,Y) The load_extension(X) function loads SQLite extensions out of the shared library file named X using the entry point Y. Use of this function must be authorized from Preferences. - + (X,Y) Функция load_extension(X) загружает расширения SQLite из файла общей библиотеки с именем X, используя точку входа Y. +Использование этой функции должно быть разрешено в настройках. @@ -6981,140 +7021,140 @@ Use of this function must be authorized from Preferences. (X) Return the arccosine of X. The result is in radians. - + (X) Возвращает арккосинус X. Результат выражается в радианах. (X) Return the hyperbolic arccosine of X. - + (X) Возвращает гиперболический арккосинус X. (X) Return the arcsine of X. The result is in radians. - + (X) Возвращает арксинус числа X. Результат выражается в радианах. (X) Return the hyperbolic arcsine of X. - + (X) Возвращает гиперболический арксинус X. (X) Return the arctangent of X. The result is in radians. - + (X) Возвращает арктангенс числа X. Результат выражается в радианах. (X,Y) Return the arctangent of Y/X. The result is in radians. The result is placed into correct quadrant depending on the signs of X and Y. - + (X,Y) Возвращает арктангенс Y/X. Результат в радианах. Результат помещается в правильный квадрант в зависимости от знаков X и Y. (X) Return the hyperbolic arctangent of X. - + (X) Возвращает гиперболический арктангенс X. (X) Return the first representable integer value greater than or equal to X. For positive values of X, this routine rounds away from zero. For negative values of X, this routine rounds toward zero. - + (X) Возвращает первое представимое целое значение, большее или равное X. Для положительных значений X эта процедура округляет от нуля. Для отрицательных значений X эта процедура округляет в сторону нуля. (X) Return the cosine of X. X is in radians. - + (X) Возвращает косинус числа X. X измеряется в радианах. (X) Return the hyperbolic cosine of X. - + (X) Возвращает гиперболический косинус X. (X) Convert value X from radians into degrees. - + (X) Преобразует значение X из радиан в градусы. (X) Compute e (Euler's number, approximately 2.71828182845905) raised to the power X. - + (X) Вычисляет e (число Эйлера, приблизительно 2,71828182845905), возведенное в степень X. (X) Return the first representable integer value less than or equal to X. For positive numbers, this function rounds toward zero. For negative numbers, this function rounds away from zero. - + (X) Возвращает первое представимое целое число, меньшее или равное X. Для положительных чисел эта функция округляет в сторону нуля. Для отрицательных чисел эта функция округляет от нуля. (X) Return the natural logarithm of X. - + (X) Возвращает натуральный логарифм X. (B,X) Return the base-B logarithm of X. - + (B,X) Возвращает логарифм X по основанию B. (X) Return the base-10 logarithm for X. - + (X) Возвращает логарифм по основанию 10 для X. (X) Return the logarithm base-2 for the number X. - + (X) Возвращает логарифм по основанию 2 для числа X. (X,Y) Return the remainder after dividing X by Y. - + (X,Y) Возвращает остаток после деления X на Y. () Return an approximation for π. - + () Возвращает приближенное значение для π. (X,Y) Compute X raised to the power Y. - + (X,Y) Вычислить X, возведенное в степень Y. (X) Convert X from degrees into radians. - + (X) Преобразовать X из градусов в радианы. (X) Return the sine of X. X is in radians. - + (X) Возвращает синус числа X. X измеряется в радианах. (X) Return the hyperbolic sine of X. - + (X) Возвращает гиперболический синус X. (X) Return the square root of X. NULL is returned if X is negative. - + (X) Возвращает квадратный корень из X. Возвращается NULL, если X отрицательно. (X) Return the tangent of X. X is in radians. - + (X) Возвращает тангенс числа X. X измеряется в радианах. (X) Return the hyperbolic tangent of X. - + (X) Возвращает гиперболический тангенс X. (X) Return the representable integer in between X and 0 (inclusive) that is furthest away from zero. Or, in other words, return the integer part of X, rounding toward zero. - + (X) Возвращает представимое целое число между X и 0 (включительно), которое находится дальше всего от нуля. Или, другими словами, возвращает целую часть X, округляя ее в сторону нуля. @@ -7165,22 +7205,22 @@ Hold %3Shift and click to jump there Browse Data - Данные + Просмотр данных &Table: - &Таблица: + &Таблица: Select a table to browse data - Выберите таблицу для просмотра данных + Выберите таблицу для просмотра данных Use this list to select a table to be displayed in the database view - Используйте этот список, чтобы выбрать таблицу, которая должна быть отображена в представлении базы данных + Используйте этот список, чтобы выбрать таблицу, которая должна быть отображена в представлении базы данных @@ -7191,7 +7231,7 @@ Hold %3Shift and click to jump there - Ctrl+" for duplicating the current record. - Ctrl+' for copying the value from the cell above. - Standard selection and copy/paste operations. - Это представление таблицы БД. Вы можете выполнить следующие действия: + Это представление таблицы БД. Вы можете выполнить следующие действия: - Начните писать для редактирования, введя значение. - Дважды щелкните любую запись, чтобы отредактировать ее содержимое в окне редактора ячеек. - Alt + Del для обнуления содержимого ячейки в NULL. @@ -7202,615 +7242,615 @@ Hold %3Shift and click to jump there Text pattern to find considering the checks in this frame - Шаблон для поиска, учитывая все проверки + Шаблон для поиска, учитывая все проверки Find in table - + Поиск в таблице Find previous match [Shift+F3] - Найти предыдущее совпадение [Shift+F3] + Найти предыдущее совпадение [Shift+F3] Find previous match with wrapping - Найти предыдущее совпадение, закольцевав поиск + Найти предыдущее совпадение, закольцевав поиск Shift+F3 - + Shift+F3 Find next match [Enter, F3] - Найти следующее совпадение [Enter, F3] + Найти следующее совпадение [Enter, F3] Find next match with wrapping - Найти следующее совпадение, закольцевав поиск + Найти следующее совпадение, закольцевав поиск F3 - + F3 The found pattern must match in letter case - У найденного шаблона должен совпадать регистр + У шаблона должен совпадать регистр Case Sensitive - Учитывать Регистр + Учитывать регистр The found pattern must be a whole word - Найденный шаблон должен быть целым словом + Шаблон должен быть целым словом Whole Cell - + Ячейка целиком Interpret search pattern as a regular expression - Интерпретировать шаблон поиска как регулярное выражение + Интерпретировать шаблон поиска как регулярное выражение <html><head/><body><p>When checked, the pattern to find is interpreted as a UNIX regular expression. See <a href="https://en.wikibooks.org/wiki/Regular_Expressions">Regular Expression in Wikibooks</a>.</p></body></html> - <html><head/><body><p>При проверке шаблон для поиска интерпретируется как регулярное выражение UNIX. <a href="https://en.wikibooks.org/wiki/Regular_Expressions">Узнать больше о Регулярных выражениях на Wikibooks.org</a>.</p></body></html> + <html><head/><body><p>При проверке шаблон для поиска интерпретируется как регулярное выражение UNIX. <a href="https://en.wikibooks.org/wiki/Regular_Expressions">Узнать больше о Регулярных выражениях на Wikibooks.org</a>.</p></body></html> Regular Expression - Регулярное выражение + Регулярное выражение Close Find Bar - Закрыть Поисковую Панель + Закрыть панель поиска Text to replace with - + Замена текста Replace with - + Заменить на Replace next match - + Заменить следуещее совпадение Replace - + Заменить Replace all matches - + Заменить все совпадения Replace all - + Заменить все Export to &JSON - + Экспорт в JSON Export the filtered data to JSON - + Экспортировать отфильтрованные данные в JSON This button exports the data of the browsed table as currently displayed (after filters, display formats and order column) as a JSON file. - + Эта кнопка экспортирует данные просматриваемой таблицы в том виде, в котором они отображаются в данный момент (после фильтров, форматов отображения и столбца порядка), в виде файла JSON. Copy column name - + Копировать имя столбца Copy the database table column name to your clipboard - + Копировать имя столбца таблицы БД в буфер обмена New Data Browser - + Новая вкладка Add a new docked Data Browser - + Открыть новую вкладку обозревателя данных This button adds a new docked Data Browser, which you can detach and arrange in different layouts. - + Эта кнопка добавляет новую вкладку просмотрщика данных. <html><head/><body><p>Scroll to the beginning</p></body></html> - <html><head/><body><p>Прокрутить к началу</p></body></html> + <html><head/><body><p>Прокрутить к началу</p></body></html> <html><head/><body><p>Clicking this button navigates to the beginning in the table view above.</p></body></html> - <html><head/><body><p>Нажатие этой кнопки переводит к началу в таблице выше.</p></body></html> + <html><head/><body><p>Нажатие этой кнопки переводит к началу в таблице выше.</p></body></html> |< - + Scroll one page upwards - Страница вверх + Страница вверх <html><head/><body><p>Clicking this button navigates one page of records upwards in the table view above.</p></body></html> - <html><head/><body><p>Нажатие этой кнопки перемещает одну страницу записей вверх в виде таблицы выше.</p></body></html> + <html><head/><body><p>Нажатие этой кнопки перемещает по записям на одну страницу вверх.</p></body></html> < - < + < 0 - 0 of 0 - 0 - 0 из 0 + 0 - 0 из 0 Scroll one page downwards - Страница вниз + На страницу вниз <html><head/><body><p>Clicking this button navigates one page of records downwards in the table view above.</p></body></html> - <html><head/><body><p>Нажатие этой кнопки перемещает одну страницу записей вниз в виде таблицы выше.</p></body></html> + <html><head/><body><p>Нажатие этой кнопки перемещает на одну страницу записей вниз.</p></body></html> > - > + > Scroll to the end - Прокрутить к концу + Прокрутить к концу <html><head/><body><p>Clicking this button navigates up to the end in the table view above.</p></body></html> - + <html><head/><body><p>Нажатие этой кнопки позволяет перейти к концу таблицы.</p></body></html> >| - + <html><head/><body><p>Click here to jump to the specified record</p></body></html> - <html><head/><body><p>Нажмите здесь, чтобы перейти к указанной записи</p></body></html> + <html><head/><body><p>Нажмите здесь, чтобы перейти к указанной записи</p></body></html> <html><head/><body><p>This button is used to navigate to the record number specified in the Go to area.</p></body></html> - <html><head/><body><p>Эта кнопка используется, чтобы переместиться к записи, номер которой указан в области Перейти к</p></body></html> + <html><head/><body><p>Эта кнопка используется, чтобы переместиться к записи, номер которой указан в области Перейти к</p></body></html> Go to: - Перейти к: + Перейти к: Enter record number to browse - Введите номер записи для просмотра + Введите номер записи для просмотра Type a record number in this area and click the Go to: button to display the record in the database view - Напечатайте номер записи в этой области и нажмите кнопку Перейти к:, чтобы отобразить запись в представлении базы данных + Напечатайте номер записи в этой области и нажмите кнопку Перейти к:, чтобы отобразить запись в представлении базы данных 1 - 1 + 1 Show rowid column - Отображать колонку rowid + Отображать колонку rowid Toggle the visibility of the rowid column - + Переключить видимость столбца rowid Unlock view editing - Разблокировать возможность редактирования + Разблокировать возможность редактирования This unlocks the current view for editing. However, you will need appropriate triggers for editing. - Разблокировать текущий вид для редактирования. Однако для редактирования вам понадобятся соответствующие триггеры. + Разблокировать текущий вид для редактирования. Однако для редактирования вам понадобятся соответствующие триггеры. Edit display format - Формат отображения + Изменить формат отображения Edit the display format of the data in this column - Редактирование формата отображения для данных из этой колонки + Редактирование формата отображения для данных из этой колонки New Record - Добавить запись + Добавить запись Insert a new record in the current table - Добавить новую запись в текущую таблицу + Вставить новую запись в текущую таблицу <html><head/><body><p>This button creates a new record in the database. Hold the mouse button to open a pop-up menu of different options:</p><ul><li><span style=" font-weight:600;">New Record</span>: insert a new record with default values in the database.</li><li><span style=" font-weight:600;">Insert Values...</span>: open a dialog for entering values before they are inserted in the database. This allows to enter values accomplishing the different constraints. This dialog is also open if the <span style=" font-weight:600;">New Record</span> option fails due to these constraints.</li></ul></body></html> - <html><head/><body><p>Эта кнопка создает новую запись в базе данных. Удерживайте кнопку мыши, чтобы открыть всплывающее меню различных параметров:</p><ul><li><span style=" font-weight:600;">Новая Запись</span>: вставляет новую запись со значениями по умолчанию.</li><li><span style=" font-weight:600;">Вставить Значения...</span>: открывает диалог для ввода значений перед тем, как они будут вставлены в БД. Это позволяет вводить значения, назначая различные ограничения. Этот диалог также открывается, если <span style=" font-weight:600;">Новая Запись</span> опция не срабатывает из-за этих ограничений.</li></ul></body></html> + <html><head/><body><p>Эта кнопка создает новую запись в базе данных. Удерживайте кнопку мыши, чтобы открыть всплывающее меню различных параметров:</p><ul><li><span style=" font-weight:600;">Новая Запись</span>: вставляет новую запись со значениями по умолчанию.</li><li><span style=" font-weight:600;">Вставить Значения...</span>: открывает диалог для ввода значений перед тем, как они будут вставлены в БД. Это позволяет вводить значения, назначая различные ограничения. Этот диалог также открывается, если <span style=" font-weight:600;">Новая Запись</span> опция не срабатывает из-за этих ограничений.</li></ul></body></html> Delete Record - Удалить запись + Удалить запись Delete the current record - Удалить текущую запись + Удалить текущую запись This button deletes the record or records currently selected in the table - Эта кнопка удаляет запись или записи, выбранные в настоящее время в таблице + Эта кнопка удаляет запись или записи, выбранные в настоящее время в таблице Insert new record using default values in browsed table - Вставляет новую запись, используя значения по умолчанию в просматриваемой таблице + Вставляет новую запись, используя значения по умолчанию в просматриваемой таблице Insert Values... - Вставить Значения... + Вставить значения... Open a dialog for inserting values in a new record - Открывает диалоговое окно для вставки значений в новую запись + Открывает диалоговое окно для вставки значений в новую запись Export to &CSV - Экспортировать в &CSV + Экспортировать в &CSV Export the filtered data to CSV - Экспортировать отфильтрованные данные в CSV + Экспортировать отфильтрованные данные в CSV This button exports the data of the browsed table as currently displayed (after filters, display formats and order column) as a CSV file. - Эта кнопка экспортирует данные просматриваемой таблицы так как отображается (после обработки фильтрами, форматами отображения и т.д.) в виде файла CSV. + Эта кнопка экспортирует данные просматриваемой таблицы так как отображается (после обработки фильтрами, форматами отображения и т.д.) в виде файла CSV. Save as &view - Сохранить как &представление + Сохранить как &представление Save the current filter, sort column and display formats as a view - Сохранить текущие фильтры, столбцы сортировки и форматы отображения в виде представления + Сохранить текущие фильтры, столбцы сортировки и форматы отображения в виде представления This button saves the current setting of the browsed table (filters, display formats and order column) as an SQL view that you can later browse or use in SQL statements. - Эта кнопка сохраняет текущие настройки просматриваемой таблицы (фильтры, форматы отображения и столбец сортировки) в виде представления SQL, которое вы можете впоследствии просмотреть или использовать в операторах SQL. + Эта кнопка сохраняет текущие настройки просматриваемой таблицы (фильтры, форматы отображения и столбец сортировки) в виде представления SQL, которое вы можете впоследствии просмотреть или использовать в операторах SQL. Save Table As... - + Сохранить таблицу как... Save the table as currently displayed - Сохранить таблицу так как сейчас отображается + Сохранить таблицу так как сейчас отображается <html><head/><body><p>This pop-up menu provides the following options applying to the currently browsed and filtered table:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Export to CSV: this option exports the data of the browsed table as currently displayed (after filters, display formats and order column) to a CSV file.</li><li style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Save as view: this option saves the current setting of the browsed table (filters, display formats and order column) as an SQL view that you can later browse or use in SQL statements.</li></ul></body></html> - <html><head/><body><p>Это всплывающее меню предоставляет следующие параметры, применяемые к текущей просматриваемой и отфильтрованной таблице:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Экспортировать в виде CSV: данные просматриваемой таблицы сохраняется так как отображается (после применения фильтров, форматов отображения и порядка колонок) в CSV файл.</li><li style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Сохранить как вид: эта опция сохраняет настройки текущей отображаемой таблицы (фильтры, форматы отображения и порядок колонок) как SQL вид, который вы позже можете просматривать или использовать в SQL выражениях.</li></ul></body></html> + <html><head/><body><p>Это всплывающее меню предоставляет следующие параметры, применяемые к текущей просматриваемой и отфильтрованной таблице:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Экспортировать в виде CSV: данные просматриваемой таблицы сохраняется так как отображается (после применения фильтров, форматов отображения и порядка колонок) в CSV файл.</li><li style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Сохранить как вид: эта опция сохраняет настройки текущей отображаемой таблицы (фильтры, форматы отображения и порядок колонок) как SQL вид, который вы позже можете просматривать или использовать в SQL выражениях.</li></ul></body></html> Hide column(s) - Скрыть колонки + Скрыть колонку(и) Hide selected column(s) - Скрыть выбранные колонки + Скрыть выбранные колонки Show all columns - Показать все колонки + Показать все колонки Show all columns that were hidden - Показать все колонки, которые были скрыты + Показать все колонки, которые были скрыты Set encoding - Кодировка + Кодировка Change the encoding of the text in the table cells - Изменение кодировки текста в данной таблице + Изменение кодировки текста в данной таблице Set encoding for all tables - Установить кодировку для всех таблиц + Установить кодировку для всех таблиц Change the default encoding assumed for all tables in the database - Изменить кодировку по умолчанию для всех таблиц в базе данных + Изменить кодировку по умолчанию для всех таблиц в базе данных Clear Filters - + Очистить фильтры Clear all filters - Очистить все фильтры + Очистить все фильтры This button clears all the filters set in the header input fields for the currently browsed table. - Эта кнопка очищает все фильтры, установленные в полях ввода заголовка для текущей просматриваемой таблицы. + Эта кнопка очищает все фильтры, установленные в полях ввода заголовка для текущей просматриваемой таблицы. Clear Sorting - + Убрать сортировку Reset the order of rows to the default - + Сбросить порядок строк This button clears the sorting columns specified for the currently browsed table and returns to the default order. - + Эта кнопка сбрасывает сортировку столбцов в отображаемой таблице. Print - Печать + Печать Print currently browsed table data - Печатать отображаемую таблицу + Печатать отображаемую таблицу Print currently browsed table data. Print selection if more than one cell is selected. - Распечатывайте текущие данные таблицы. Выбор печати, если выбрано несколько ячеек. + Печать данных текущей таблицы. Печать выделения, если выбрано несколько ячеек. Ctrl+P - + Ctrl+P Refresh - Обновить + Обновить Refresh the data in the selected table - Обновить данные в выбранной таблице + Обновить данные в выбранной таблице This button refreshes the data in the currently selected table. - Эта кнопка обновляет данные выбранной в данный момент таблицы. + Эта кнопка обновляет данные выбранной в данный момент таблицы. F5 - + F5 Find in cells - + Найти в ячейках Open the find tool bar which allows you to search for values in the table view below. - + Открыть панель инструментов поиска, которая позволяет искать значения в представленной ниже таблице. Bold - Жирный + Жирный Ctrl+B - + Ctrl+B Italic - Курсив + Курсив Underline - Подчёркивание + Подчёркнутый Ctrl+U - + Ctrl+U Align Right - + Выровнять вправо Align Left - + Выровнять влево Center Horizontally - + Центрировать по горизонтали Justify - + По ширине Edit Conditional Formats... - + Изменить форматирование... Edit conditional formats for the current column - + Изменить форматирование для текущего столбца Clear Format - + Очистить форматирование Clear All Formats - + Очистить все форматирование Clear all cell formatting from selected cells and all conditional formats from selected columns - + Очистить фоматирование в выделенном Font Color - + Цвет шрифта Background Color - + Цвет фона Toggle Format Toolbar - + Панель форматирования Show/hide format toolbar - + Показать/скрыть панель форматирования This button shows or hides the formatting toolbar of the Data Browser - + Эта кнопка показывает/скрывает панель форматирования Select column - + Выделить столбец @@ -7820,143 +7860,143 @@ Hold %3Shift and click to jump there Replace text in cells - + Замена текста в ячейках Freeze columns - + Заморозить столбец Make all columns from the first column up to this column not move when scrolling horizontally - + Сделать так, чтобы все столбцы, от первого столбца до этого столбца, не перемещались при горизонтальной прокрутке Filter in any column - + Фильтр в столбцах Ctrl+R - + Ctrl+R %n row(s) - - - - + + %n строка + %n строки + %n строк , %n column(s) - - - - + + , %n столбец + , %n столбца + , %n столбцов . Sum: %1; Average: %2; Min: %3; Max: %4 - + . Сумма: %1; Среднее: %2; Мин: %3; Макс: %4 Conditional formats for "%1" - + Форматирование для "%1" determining row count... - определяем количество строк... + определяем количество строк... %L1 - %L2 of >= %L3 - + %L1 - %L2 of %L3 - + (clipped at %L1 rows) - + (выбрано до %L1 строк) Please enter a pseudo-primary key in order to enable editing on this view. This should be the name of a unique column in the view. - Пожалуйста, введите псевдо-первичный ключ, чтобы разрешить редактирование в этом представлении. Это должно быть имя уникального столбца в представлении. + Пожалуйста, введите псевдо-первичный ключ, чтобы разрешить редактирование в этом представлении. Это должно быть имя уникального столбца в представлении. Delete Records - Удалить Записи + Удалить записи Duplicate records - Дублированные записи + Дублированные записи Duplicate record - Дубликат записи + Дубликат записи Ctrl+" - + Ctrl+" Adjust rows to contents - + Подогнать по содержимому Error deleting record: %1 - Ошибка удаления записи: %1 + Ошибка удаления записи: %1 Please select a record first - Сначала выберите запись + Сначала выберите запись Please choose a new encoding for all tables. - Пожалуйста выберите новую кодировку для всех таблиц. + Пожалуйста выберите новую кодировку для всех таблиц. Please choose a new encoding for this table. - Пожалуйста выберите новую кодировку для данной таблицы. + Пожалуйста выберите новую кодировку для данной таблицы. %1 Leave the field empty for using the database encoding. - %1 + %1 Оставьте это поле пустым если хотите чтобы использовалась кодировка по умолчанию. This encoding is either not valid or not supported. - Неверная кодировка либо она не поддерживается. + Неверная кодировка либо она не поддерживается. %1 replacement(s) made. - + %1 замен(а) произведено(а). @@ -7964,22 +8004,22 @@ Leave the field empty for using the database encoding. New Data Browser - + Новая вкладка обозревателя Rename Data Browser - + Переименовать обозреватель данных Close Data Browser - + Закрыть обозреватель данных Set a new name for the data browser. Use the '&&' character to allow using the following character as a keyboard shortcut. - + Задайте новое имя для вкладки SQL. Используйте символ '&&', чтобы разрешить использование следующего символа в качестве сочетания клавиш. diff --git a/src/translations/sqlb_tr.ts b/src/translations/sqlb_tr.ts index fd55fee824..ed548917f8 100644 --- a/src/translations/sqlb_tr.ts +++ b/src/translations/sqlb_tr.ts @@ -11,12 +11,12 @@ Version - Versiyon + Sürüm <html><head/><body><p>DB Browser for SQLite is an open source, freeware visual tool used to create, design and edit SQLite database files.</p><p>It is bi-licensed under the Mozilla Public License Version 2, as well as the GNU General Public License Version 3 or later. You can modify or redistribute it under the conditions of these licenses.</p><p>See <a href="https://www.gnu.org/licenses/gpl.html"><span style=" text-decoration: underline; color:#0000ff;">https://www.gnu.org/licenses/gpl.html</span></a> and <a href="https://www.mozilla.org/MPL/2.0/index.txt"><span style=" text-decoration: underline; color:#0000ff;">https://www.mozilla.org/MPL/2.0/index.txt</span></a> for details.</p><p>For more information on this program please visit our website at: <a href="https://sqlitebrowser.org"><span style=" text-decoration: underline; color:#0000ff;">https://sqlitebrowser.org</span></a></p><p><span style=" font-size:small;">This software uses the GPL/LGPL Qt Toolkit from </span><a href="https://qt-project.org/"><span style=" font-size:small; text-decoration: underline; color:#0000ff;">https://qt-project.org/</span></a><span style=" font-size:small;"><br/>See </span><a href="https://doc.qt.io/qt-5/licensing.html"><span style=" font-size:small; text-decoration: underline; color:#0000ff;">https://doc.qt.io/qt-5/licensing.html</span></a><span style=" font-size:small;"> for licensing terms and information.</span></p><p><span style=" font-size:8pt;">We use the nalgeon/sqlean library for SQLite extensions support.<br/>This library is licensed under the MIT license, see the following for more information:<br/></span><a href="https://github.com/nalgeon/sqlean"><span style=" font-size:8pt; text-decoration: underline; color:#0000ff;">https://github.com/nalgeon/sqlean</span></a></p><p><span style=" font-size:small;">It also uses the Pastel SVG icon set by Michael Buckley under a Creative Commons Attribution Share Alike 4.0 license.<br/>See </span><a href="https://codefisher.org/pastel-svg/"><span style=" font-size:small; text-decoration: underline; color:#0000ff;">https://codefisher.org/pastel-svg/</span></a><span style=" font-size:small;"> for details.</span></p></body></html> - + <html><head/><body><p>DB Browser for SQLite, SQLite veritabanı dosyaları oluşturmak, tasarlamak ve düzenlemek için kullanılan açık kaynaklı, ücretsiz bir görsel araçtır.</p><p>Mozilla Public License Version 2 ve GNU General Public License Version 3 veya sonraki sürümleri altında çift lisanslıdır. Bu lisansların koşulları altında bunu değiştirebilir veya yeniden dağıtabilirsiniz.</p><p>Ayrıntılar için <a href="https://www.gnu.org/licenses/gpl.html"><span style=" text-decoration: underline; color:#0000ff;">https://www.gnu.org/licenses/gpl.html</span></a> ve <a href="https://www.mozilla.org/MPL/2.0/index.txt"><span style=" text-decoration: underline; color:#0000ff;">https://www.mozilla.org/MPL/2.0/index.txt</span></a> adreslerine bakın.</p><p>Bu program hakkında daha fazla bilgi için lütfen web sitemizi ziyaret edin: <a href="https://sqlitebrowser.org"><span style=" text-decoration: underline; color:#0000ff;">https://sqlitebrowser.org</span></a></p><p><span style=" font-size:small;">Bu yazılım, lisanslama koşulları ve diğer bilgiler için </span><a href="https://qt-project.org/"><span style=" font-size:small; text-decoration: underline; color:#0000ff;">https://qt-project.org/</span></a><span style=" font-size:small;"><br/>https://doc.qt.io/qt-5/licensing.html"><span style=" font-size:small; text-decoration: underline; color:#0000ff;">https://doc.qt.io/qt-5/licensing.html</span></a><span style=" font-size:small;"> adresine bakın</span><a href="https://doc.qt.io/qt-5/licensing.html</span></a><span style=" font-size:small;"> bilgi.</span></p><p><span style=" font-size:8pt;">SQLite uzantılarını desteklemek için nalgeon/sqlean kütüphanesini kullanıyoruz.<br/>Bu kütüphane MIT lisansı altında lisanslanmıştır, daha fazla bilgi için aşağıdakilere bakın:<br/></span><a href="https://github.com/nalgeon/sqlean"><span style=" font-size:8pt; text-decoration: underline; color:#0000ff;">https://github.com/nalgeon/sqlean</span></a></p><p><span style=" font-size:small;">Ayrıca, Creative Commons Atıf-Benzer Paylaşım 4.0 lisansı altında Michael Buckley tarafından oluşturulan Pastel SVG simge setini kullanır.<br/>Bkz.</span><a href="https://codefisher.org/pastel-svg/"><span style=" font-size:small; text-decoration: underline; color:#0000ff;">https://codefisher.org/pastel-svg/</span></a><span style=" font-size:small;"> ayrıntılar için.</span></p></body></html> @@ -91,7 +91,7 @@ Foreign key: %1 - Yabancı anahatar: %1 + Yabancı anahtar: %1 @@ -126,12 +126,12 @@ The user settings file location is replaced with the argument value instead of the environment variable value. - + Kullanıcı ayarları dosyasının konumu, ortam değişkeni değeri yerine argüman değeriyle değiştirilir. Ignored environment variable (DB4S_SETTINGS_FILE) value: - + Yoksayılan ortam değişkeni (DB4S_SETTINGS_FILE) değeri: @@ -142,141 +142,141 @@ Usage - + Kullanım options - + seçenekler database - + veritabanı project - + proje csv-file - + csv dosyası Show command line options - + Komut satırı seçeneklerini göster Exit application after running scripts - + Betikleri çalıştırdıktan sonra uygulamadan çıkın file - + dosya Execute this SQL file after opening the DB - + Veritabanını açtıktan sonra bu SQL dosyasını çalıştırın Import this CSV file into the passed DB or into a new DB - + Bu CSV dosyasını geçirilen veritabanına veya yeni bir veritabanına aktarın table - + tablo Browse this table, or use it as target of a data import - + Bu tabloya göz atın veya bir veri içe aktarımının hedefi olarak kullanın Open database in read-only mode - + Veritabanını salt okunur modunda aç settings_file - + settings_file Run application based on this settings file - + Uygulamayı bu ayar dosyasına göre çalıştır group - + grup settings - + ayarlar value - + değer Run application with this setting temporarily set to value - + Bu ayarı geçici değere ayarlayarak uygulamayı çalıştırın Run application saving this value for this setting - + Bu ayar için bu değeri kaydederek uygulamayı çalıştırın Display the current version - + Mevcut sürümü görüntüle Open this SQLite database - + Bu SQLite veritabanını açın Open this project file (*.sqbpro) - + Bu proje dosyasını açın (*.sqbpro) Import this CSV file into an in-memory database - + Bu CSV dosyasını bellek içi bir veritabanına aktarın The %1 option requires an argument - + %1 seçeneği bir argüman gerektirir The -S/--settings option requires an argument. The option is ignored. - + -S/--settings seçeneği bir argüman gerektirir. Seçenek yok sayılır. @@ -291,32 +291,32 @@ SQLite Version - SQLite Versiyonu + SQLite Versiyonu SQLCipher Version %1 (based on SQLite %2) - + SQLCipher Sürümü %1 (%2 SQLite'a dayalı) DB Browser for SQLite Version %1. - + DB Browser for SQLite Versiyon %1. Last commit hash when built: %1 - + Derleme sırasında son commit karma değeri: %1 Built for %1, running on %2 - %1 için derlendi, %2 üzerinde çalışıyor + %1 için derlendi, %2 üzerinde çalışıyor Qt Version %1 - + Qt Sürümü %1 @@ -379,7 +379,7 @@ Plaintext Header Size - Düz Metin Üstbilgi Boyutu + Düz Metin Üst Bilgi Boyutu @@ -415,17 +415,17 @@ Bu veritabanı için herhangi bir başka ayar daha yapılmışsa, bu bilgileri d Choose display format - Görüntüleme formatını seçiniz + Görüntüleme formatını seçin Display format - Görüntüleme formatı + Formatı görüntüle Choose a display format for the column '%1' which is applied to each value prior to showing it. - '%1' sütunu için görüntülemeden önce uygulanacak bir görüntüleme formatı seçin. + Her değer gösterilmeden önce, '%1' sütunu için uygulanacak bir görüntüleme biçimi seçin. @@ -445,7 +445,7 @@ Bu veritabanı için herhangi bir başka ayar daha yapılmışsa, bu bilgileri d Hex blob - Onaltılık ikili veri + Onaltılık blob @@ -465,7 +465,7 @@ Bu veritabanı için herhangi bir başka ayar daha yapılmışsa, bu bilgileri d .NET DateTime.Ticks to date - + .NET DateTime.Ticks tarihine @@ -480,12 +480,12 @@ Bu veritabanı için herhangi bir başka ayar daha yapılmışsa, bu bilgileri d WebKit / Chromium epoch to date - + WebKit / Chromium'un bugüne kadarki tarihi WebKit / Chromium epoch to local time - + WebKit / Chromium dönemi yerel zamana göre @@ -500,12 +500,12 @@ Bu veritabanı için herhangi bir başka ayar daha yapılmışsa, bu bilgileri d Binary GUID to text - + İkili GUID'den metne SpatiaLite Geometry to SVG - + SpatiaLite Geometriden SVG'ye @@ -534,7 +534,7 @@ Bu veritabanı için herhangi bir başka ayar daha yapılmışsa, bu bilgileri d Round number - Küsüratsız sayı + Küsuratsız sayı @@ -587,7 +587,7 @@ Bu veritabanı için herhangi bir başka ayar daha yapılmışsa, bu bilgileri d &Remove - &Sil + Kaldı&r @@ -607,7 +607,7 @@ Bu veritabanı için herhangi bir başka ayar daha yapılmışsa, bu bilgileri d Move &down - Aşağı &Taşı + Aşağı &taşı @@ -617,7 +617,7 @@ Bu veritabanı için herhangi bir başka ayar daha yapılmışsa, bu bilgileri d Text color - Yazı rengi + Metin rengi @@ -696,7 +696,7 @@ Bu veritabanı için herhangi bir başka ayar daha yapılmışsa, bu bilgileri d Exporting database to SQL file... - veritabanı, SQL dosyası olarak dışa aktarılıyor... + Veritabanı, SQL dosyası olarak dışa aktarılıyor... @@ -727,13 +727,13 @@ Bu veritabanı için herhangi bir başka ayar daha yapılmışsa, bu bilgileri d Cannot open destination file: '%1' - + Hedef dosya açılamıyor: '%1' Cannot backup to file: '%1'. Message: %2 - + Dosyaya yedekleme yapılamıyor: '%1'. Mesaj: %2 @@ -805,37 +805,37 @@ Yürütme durduruluyor %3. Creating savepoint failed. DB says: %1 - Kayıt noktası oluşturulamadı. Veritabanı mesajı: %1 + Kayıt noktası oluşturulamadı. DB mesajı: %1 Renaming the column failed. DB says: %1 - Sütun yeniden adlandırılamadı. Veritabanı motoru mesajı: + Sütun yeniden adlandırılamadı. DB mesajı: %1 Releasing savepoint failed. DB says: %1 - Kayıt noktası serbest bırakılamadı. Veritabanı motoru mesajı: %1 + Kayıt noktasının serbest bırakılması başarısız. DB mesajı: %1 Creating new table failed. DB says: %1 - Veri tabanı oluşturulamadı. Veritabanı mesajı: %1 + Veritabanı oluşturulamadı. DB mesajı: %1 Copying data to new table failed. DB says: %1 - Yeni tabloya veri kopyalanamadı. Veritabanı mesajı: + Yeni tabloya veri kopyalanamadı. DB mesajı: %1 Deleting old table failed. DB says: %1 - Eski tablolar silinemedi: Veritabanı mesajı: %1 + Eski tablolar silinemedi: DB mesajı: %1 @@ -863,7 +863,7 @@ Veritabanı motoru mesajı: could not get list of databases: %1 - veri tabanı listesi alınamadı: %1 + veritabanı listesi alınamadı: %1 @@ -873,7 +873,7 @@ Veritabanı motoru mesajı: Error loading built-in extension: %1 - + Yerleşik uzantı yüklenirken hata oluştu: %1 @@ -906,7 +906,7 @@ Veritabanı motoru mesajı: Type - Tip + Tür @@ -941,7 +941,7 @@ Veritabanı motoru mesajı: Indices (%1) - İndisler (%1) + İndeksler (%1) @@ -995,7 +995,7 @@ Veritabanı motoru mesajı: Evaluation - + Değerlendirme @@ -1016,22 +1016,22 @@ Veritabanı motoru mesajı: This Qt editor is used for right-to-left scripts, which are not supported by the default Text editor. The presence of right-to-left characters is detected and this editor mode is automatically selected. - Qt editör, varsayılan metin editörü tarafından desteklenmeyen sağdan sola okunan dillde yazılmış betikleri için kullanılır. + Qt editör, varsayılan metin editörü tarafından desteklenmeyen sağdan sola okunan dillde yazılmış betikler için kullanılır. Identification of the cell currently in the editor - + Şu anda düzenleyicide bulunan hücrenin tanımlanması Type and size of data currently in table - + Tabloda şu anda bulunan verilerin türü ve boyutu Open preview dialog for printing the data currently stored in the cell - Şu anda hücrede saklanan veriyi yazdırmak için önizleme penceresini aç + Şu anda hücrede saklanan veriyi yazdırmak için ön izleme penceresini aç @@ -1136,7 +1136,7 @@ Veritabanı motoru mesajı: Open preview dialog for printing displayed text - Görüntülenen yazıyı yazdırmak için önizleme penceresini aç + Görüntülenen metni yazdırmak için ön izleme penceresini aç @@ -1211,28 +1211,33 @@ Veritabanı motoru mesajı: Errors are indicated with a red squiggle underline. In the Evaluation mode, entered SQLite expressions are evaluated and the result applied to the cell. - + Metin düzenleyici modları, sözdizimi vurgulama, otomatik biçimlendirme ve kaydetmeden önce doğrulama ile düz metinlerin yanı sıra JSON veya XML verilerini düzenlemenize olanak tanır. + +Hatalar kırmızı kıvrımlı alt çizgiyle gösterilir. + +Değerlendirme modunda, girilen SQLite ifadeleri değerlendirilir ve sonuç hücreye uygulanır. Unsaved data in the cell editor - + Hücre düzenleyicide kaydedilmemiş veriler The cell editor contains data not yet applied to the database. Do you want to apply the edited data to row=%1, column=%2? - + Hücre düzenleyicisi henüz veritabanına uygulanmamış veriler içeriyor. +Düzenlenen verileri satır=%1, sütun=%2'ye uygulamak ister misiniz? Editing row=%1, column=%2 - + Satır=%1, sütun=%2 düzenleniyor No cell active. - + Hiçbir hücre aktif değil. @@ -1267,44 +1272,44 @@ Do you want to apply the edited data to row=%1, column=%2? Type: NULL; Size: 0 bytes - + Tür: NULL; Boyut: 0 bayt Type: Text / Numeric; Size: %n character(s) - + Type: %1 Image; Size: %2x%3 pixel(s) - + Tür: %1 Görüntü; Boyut: %2x%3 piksel Type: Valid JSON; Size: %n character(s) - - + + Tür: Geçerli JSON; Boyut: %n karakter Type: Binary; Size: %n byte(s) - - + + Tür: İkili; Boyut: %n bayt Choose a file to import - İçe aktarmak için dosya seçiniz + İçe aktarmak için dosya seçin %1 Image - %1 imajı + %1 Görüntü @@ -1319,12 +1324,12 @@ Do you want to apply the edited data to row=%1, column=%2? Couldn't save file: %1. - Dosya kaydedilemedi: %1. + Dosya kaydedilemedi: %1. The data has been saved to a temporary file and has been opened with the default application. You can now edit the file and, when you are ready, apply the saved new data to the cell or cancel any changes. - + Veriler geçici bir dosyaya kaydedildi ve varsayılan uygulamayla açıldı. Artık dosyayı düzenleyebilir ve hazır olduğunuzda kaydedilen yeni verileri hücreye uygulayabilir veya değişiklikleri iptal edebilirsiniz. @@ -1347,7 +1352,7 @@ Do you want to apply the edited data to row=%1, column=%2? Edit Index Schema - Index Şemasını Düzenle + İndeks Şemasını Düzenle @@ -1357,12 +1362,12 @@ Do you want to apply the edited data to row=%1, column=%2? For restricting the index to only a part of the table you can specify a WHERE clause here that selects the part of the table that should be indexed - Index'i tablonun yalnızca bir bölümüyle sınırlamak için, burada tablonun dizine alınması gereken kısmını seçen bir WHERE deyimi belirtebilirsiniz + İndeksi tablonun yalnızca bir bölümüyle sınırlamak için, burada tablonun dizine alınması gereken kısmını seçen bir WHERE deyimi belirtebilirsiniz Partial inde&x clause - Kısmi inde&x hükmü + Kısmi inde&ks hükmü @@ -1377,23 +1382,23 @@ Do you want to apply the edited data to row=%1, column=%2? Type - Tip + Tür Add a new expression column to the index. Expression columns contain SQL expression rather than column names. - Index için yeni bir ifade sütunu ekleyin. İfade sütunları, sütun adları değil SQL ifadesi içerir. + İndeks için yeni bir ifade sütunu ekleyin. İfade sütunları, sütun adları değil SQL ifadesi içerir. Index column - Index sütunu + İndeks sütunu Deleting the old index failed: %1 - Eski index silinemedi: + Eski indeks silinemedi: %1 @@ -1423,32 +1428,32 @@ Do you want to apply the edited data to row=%1, column=%2? Without Rowid - Satır ID(Rowid) Kullanma + Rowid olmadan Database sche&ma - Veritabanı &Şeması + Veritabanı şe&ması Make this a 'WITHOUT ROWID' table. Setting this flag requires specifying a PRIMARY KEY (which can be of any type, and can be composite), and forbids the AUTOINCREMENT flag. - + Bunu 'WITHOUT ROWID' tablosu yapın. Bu bayrağı ayarlamak, bir PRIMARY KEY (herhangi bir türde ve bileşik olabilir) belirtmeyi gerektirir ve AUTOINCREMENT bayrağını yasaklar. On Conflict - + On Conflict Strict - + Strict When the strict option is enabled SQLite enforces the data types of each column when updating or inserting data. - + Strict seçeneği etkinleştirildiğinde SQLite, veri güncellerken veya eklerken her sütunun veri türlerini uygular. @@ -1467,12 +1472,12 @@ Do you want to apply the edited data to row=%1, column=%2? Remove - Sil + Kaldır Move to top - En yukarı taşı + En üste taşı @@ -1487,7 +1492,7 @@ Do you want to apply the edited data to row=%1, column=%2? Move to bottom - En aşağı taşı + En alta taşı @@ -1501,7 +1506,7 @@ Do you want to apply the edited data to row=%1, column=%2? Type - Tip + Tür @@ -1511,17 +1516,17 @@ Do you want to apply the edited data to row=%1, column=%2? Not null - NULL Olamaz + NULL değil PK - Birincil Anahtar + PK <html><head/><body><p><img src=":/icons/field_key"/> Primary key</p></body></html> - + <html><head/><body><p><img src=":/icons/field_key"/> Birincil anahtar</p></body></html> @@ -1536,7 +1541,7 @@ Do you want to apply the edited data to row=%1, column=%2? U - Benzersiz + U @@ -1564,7 +1569,7 @@ Do you want to apply the edited data to row=%1, column=%2? Check constraint - Kısıtlama Kontrol + Kısıtlama kontrolü @@ -1579,12 +1584,12 @@ Do you want to apply the edited data to row=%1, column=%2? <html><head/><body><p><img src=":/icons/field_fk"/> Foreign Key</p></body></html> - + <html><head/><body><p><img src=":/icons/field_fk"/> Yabancı Anahtar</p></body></html> Index Constraints - + İndeks Kısıtlamaları @@ -1612,17 +1617,17 @@ Do you want to apply the edited data to row=%1, column=%2? Foreign Keys - + Yabancı Anahtarlar References - + Referanslar Check Constraints - + Kontrol Kısıtlamaları @@ -1660,7 +1665,7 @@ Do you want to apply the edited data to row=%1, column=%2? There already is a field with that name. Please rename it first or choose a different name for this field. - Bu isme sahip alan zaten var. Lütfen bu alan için farklı bir isim kullanın veya aynı isme sahip alanı yeniden adlandırın. + Bu isimde bir alan zaten var. Lütfen önce bu alanı yeniden adlandırın veya farklı bir isim seçin. @@ -1688,7 +1693,8 @@ Do you want to apply the edited data to row=%1, column=%2? Please add a field which meets the following criteria before setting the on conflict action: - Primary key flag set - + Lütfen çakışma eylemini ayarlamadan önce aşağıdaki ölçütleri karşılayan bir alan ekleyin: + - Birincil anahtar bayrağı ayarı @@ -1721,7 +1727,7 @@ All data currently stored in this field will be lost. Tab&le(s) - Tab&lolar + Tab&lo(lar) @@ -1731,7 +1737,7 @@ All data currently stored in this field will be lost. Fie&ld separator - &Alan ayracı + A&lan ayracı @@ -1793,7 +1799,7 @@ All data currently stored in this field will be lost. Pretty print - Düzenli baskı + Güzel baskı @@ -1821,7 +1827,7 @@ All data currently stored in this field will be lost. Error while writing the file '%1': %2 - + '%1' dosyası yazılırken hata oluştu: %2 @@ -1846,7 +1852,7 @@ All data currently stored in this field will be lost. Export finished with errors. - + Dışa aktarma hatalarla tamamlandı. @@ -1859,7 +1865,7 @@ All data currently stored in this field will be lost. Tab&le(s) - Tablo&lar + Tab&lo(lar) @@ -1899,7 +1905,7 @@ All data currently stored in this field will be lost. Keep original CREATE statements - + Orijinal CREATE ifadelerini koruyun @@ -2037,7 +2043,7 @@ All data currently stored in this field will be lost. Cut - + Kes @@ -2052,7 +2058,7 @@ All data currently stored in this field will be lost. Copy as SQL - SQL olarak Kopyala + SQL Olarak Kopyala @@ -2094,7 +2100,7 @@ Do you want to insert it anyway? <p>Not all data has been loaded. <b>Do you want to load all data before selecting all the rows?</b><p><p>Answering <b>No</b> means that no more data will be loaded and the selection will not be performed.<br/>Answering <b>Yes</b> might take some time while the data is loaded but the selection will be complete.</p>Warning: Loading all the data might require a great amount of memory for big tables. - <p> Tüm veriler yüklenmedi. <b>Tüm satırları seçmeden önce tüm verileri yüklemek istiyor musunuz?</b> </p> <p></p> <p> <b>Hayır</b> olarak cevaplamak, tüm verileri yüklemeyecek ve seçim işlemini uygulanmayacak. <br /> <b>Evet</b> seçeneği biraz zaman alabilir ama seçim işlemini gerçekleştirecektir. </p> Uyarı: Tüm verilerin yüklenmesi büyük tablolar için büyük miktarda bellek gerektirebilir. + <p> Tüm veriler yüklenmedi. <b>Tüm satırları seçmeden önce tüm verileri yüklemek istiyor musunuz?</b> </p> <p></p> <p> <b>Hayır</b> olarak cevaplamak, tüm verileri yüklemeyecek ve seçim işlemini uygulamayacak. <br /> <b>Evet</b> seçeneği biraz zaman alabilir ama seçim işlemini gerçekleştirecektir. </p> Uyarı: Tüm verilerin yüklenmesi büyük tablolar için büyük miktarda bellek gerektirebilir. @@ -2127,7 +2133,7 @@ Do you want to insert it anyway? &Remove - &Sil + &Kaldır @@ -2188,7 +2194,7 @@ x~y Aralık: değerler x ve y arasında Use for Conditional Format - Koşullu Biçim için Kullan + Koşullu Biçim İçin Kullan @@ -2281,7 +2287,7 @@ x~y Aralık: değerler x ve y arasında Fi&nd text: - &Aranan Metin: + Met&ni bul: @@ -2291,12 +2297,12 @@ x~y Aralık: değerler x ve y arasında Match &exact case - Büyük kü&çük harfe duyarlı + Büyük/küçük harf tam &eşleşme Match &only whole words - Kelimenin ta&mamını eşleştir + &Sadece tam kelimeleri eşleştir @@ -2326,7 +2332,7 @@ x~y Aralık: değerler x ve y arasında &Selection only - Sadece se&çimde ara + Sadece se&çim @@ -2351,7 +2357,7 @@ x~y Aralık: değerler x ve y arasında F3 - + F3 @@ -2361,7 +2367,7 @@ x~y Aralık: değerler x ve y arasında Highlight all the occurrences of the text in the page - Eşleşen tüm kelimeleri vurgula + Sayfadaki metnin tüm oluşumlarını vurgula @@ -2371,7 +2377,7 @@ x~y Aralık: değerler x ve y arasında Replace all the occurrences of the text in the page - Sayfadaki bulunan metinlerin tümünü değiştir + Sayfadaki metnin tüm oluşumlarını değiştir @@ -2386,7 +2392,7 @@ x~y Aralık: değerler x ve y arasında The searched text was not found. - The searched text was not found. + Aranan metin bulunamadı. @@ -2427,32 +2433,32 @@ x~y Aralık: değerler x ve y arasında Image Viewer - + İmaj Görüntüleyici Reset the scaling to match the original size of the image. - + Ölçeklemeyi görüntünün orijinal boyutuna uyacak şekilde sıfırla. Set the scaling to match the size of the viewport. - + Ölçeklemeyi, görünüm alanının boyutuna uyacak şekilde ayarla. Print... - Yazdır... + Yazdır... Open preview dialog for printing displayed image - Görüntülenen resmi yazdırmak için önizleme penceresini aç + Görüntülenen resmi yazdırmak için ön izleme penceresini aç Ctrl+P - + Ctrl+P @@ -2465,7 +2471,7 @@ x~y Aralık: değerler x ve y arasında Table na&me - Tablo İs&mi + Tablo is&mi @@ -2578,7 +2584,7 @@ x~y Aralık: değerler x ve y arasında Activate this option to stop the import when trying to import an empty value into a NOT NULL column without a default value. - Varsayılan değeri olmayan NOT NULL kısıtına sahip bir sütuna, boş bir değer içe aktarmaya çalışırken içe aktarmayı durdurmak için bu seçeneği etkinleştirin. + Varsayılan değeri olmayan bir NOT NULL sütununa boş bir değer aktarmaya çalışırken aktarmayı durdurmak için bu seçeneği etkinleştirin. @@ -2598,12 +2604,12 @@ x~y Aralık: değerler x ve y arasında Use local number conventions - + Yerel numara kurallarını kullanın Use decimal and thousands separators according to the system locale. - + Sistem yerel ayarlarına göre ondalık ve binlik ayraçlarını kullanın. @@ -2674,12 +2680,12 @@ x~y Aralık: değerler x ve y arasında Could not prepare INSERT statement: %1 - + INSERT ifadesi hazırlanamadı: %1 Unexpected end of file. Please make sure that you have configured the correct quote characters and the file is not malformed. - + Beklenmeyen dosya sonu. Lütfen tırnak işaretlerini doğru yapılandırdığınızdan ve dosyanın hatalı olmadığından emin olun. @@ -2707,7 +2713,7 @@ x~y Aralık: değerler x ve y arasında Opens the SQLCipher FAQ in a browser window - SQLCipher Hakkında SSS bölümünü tarayıcı penceresinde açar + SQLCipher SSS bölümünü bir tarayıcı penceresinde açar @@ -2717,7 +2723,7 @@ x~y Aralık: değerler x ve y arasında Open an existing database file in read only mode - Mevcut bir veritabanı dosyasını salt okunur modda açar + Mevcut bir veritabanı dosyasını salt okunur modda açın @@ -2894,7 +2900,7 @@ x~y Aralık: değerler x ve y arasında Compact the database file, removing space wasted by deleted records - Veritabanı dosyasını genişletmek, silinen kayıtlardan dolayı meydana gelen boşlukları temizler + Veritabanı dosyasını sıkıştırarak silinen kayıtların kapladığı alanı boşaltın @@ -3125,7 +3131,7 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul Close the current database file - Geçerli veritabano dosyasını kapat + Geçerli veritabanı dosyasını kapat @@ -3151,32 +3157,32 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul Compact &Database... - Veriabanını &Sıkıştır... + Veritabanını &Sıkıştır... New &tab - + Yeni &sekme Execute all/selected SQL - Tüm/seçin SQL sorgusunu yürüt + Tüm/seçili SQL'i çalıştır This button executes the currently selected SQL statements. If no text is selected, all SQL statements are executed. - Bu buton seçili olan SQL ifadesini yürütür. Hiçbir metin seçilmezse, tüm SQL ifadeleri yürütülür. + Bu düğme seçili olan SQL ifadesini yürütür. Hiçbir metin seçilmezse tüm SQL ifadeleri yürütülür. Open SQL file(s) - + SQL dosyasını/dosyalarını açın This button opens files containing SQL statements and loads them in new editor tabs - + Bu düğme, SQL ifadeleri içeren dosyaları açar ve bunları yeni düzenleyici sekmelerine yükler @@ -3186,7 +3192,7 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul Execute line - Tek satır yürüt + Satırı yürüt @@ -3211,7 +3217,7 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul Web&site - Web &Sitesi + Web &sitesi @@ -3221,30 +3227,30 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul Close Pro&ject - + Pro&jeyi Kapat Close project and database files and return to the initial state - + Proje ve veritabanı dosyalarını kapatın ve başlangıç ​​durumuna dönün Ctrl+Shift+F4 - + Ctrl+Shift+F4 Detach Database - + Veritabanını Ayır Detach database file attached to the current database connection - + Geçerli veritabanı bağlantısına bağlı veritabanı dosyasını ayırın @@ -3260,7 +3266,7 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul Add another database file to the current database connection - Şu anki veritabanı bağğlantısına başka bir veritabanı dosyası ekle + Şu anki veritabanı bağlantısına başka bir veritabanı dosyası ekle @@ -3285,7 +3291,7 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul Open Data&base Read Only... - Salt &Okunur Veritabanı Aç... + Salt Okunur Verita&banı Aç... @@ -3311,7 +3317,7 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul Find text in SQL editor - SQL editörünte metin ara + SQL editöründe metin ara @@ -3352,7 +3358,7 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul Export to &CSV - &CSV dosyası olarak dışa aktar + &CSV'ye aktar @@ -3379,248 +3385,248 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul &Database Structure This has to be equal to the tab title in all the main tabs - + &Veritabanı Yapısı &Browse Data This has to be equal to the tab title in all the main tabs - + Verileri &Görüntüle Edit P&ragmas This has to be equal to the tab title in all the main tabs - + P&ragmaları Düzenle Temp Store - + Geçici Depo Secure Delete - + Güvenli Silme Case Sensitive Like - + Büyük/Küçük Harfe Duyarlı Like İşleci İle Journal Mode - + Günlük Modu Journal Size Limit - + Günlük Boyut Sınırı Recursive Triggers - + Özyinelemeli Tetikleyiciler Page Size - + Sayfa Boyutu Foreign Keys - + Yabancı Anahtarlar Auto Vacuum - + Otomatik Vakum Max Page Count - + Maksimum Sayfa Sayısı Checkpoint Full FSYNC - + Kontrol Noktası Tam FSYNC Off - + Kapalı Normal - + Normal Full - + Tam Default - Varsayılan + Varsayılan File - Dosya + Dosya Memory - + Bellek Delete - + Sil Truncate - + Kısalt Persist - + Sürdür WAL - + WA Exclusive - + Özel Automatic Index - + Otomatik İndeks Ignore Check Constraints - + Kontrol Kısıtlamalarını Yoksay Full FSYNC - + Tam FSYNC WAL Auto Checkpoint - + WAL Oto Kontrol Noktası User Version - + Kullanıcı Sürümü Synchronous - + Eş zamanlı None - Hiçbiri + Hiçbiri Incremental - + Artımlı Locking Mode - + Kilitleme Modu E&xecute SQL This has to be equal to the tab title in all the main tabs - + SQL'i &çalıştır &Recent Files - + &Son Dosyalar &New Database - + Ye&ni Veritabanı &Undo - + &Geri Al Undo last change to the database - + Veritabanındaki son değişikliği geri al This action undoes the last change performed to the database in the Database Browser or in Execute SQL. Redoing is not possible. - + Bu işlem, Veritabanı Tarayıcısı'nda veya SQL Çalıştır'da veritabanında gerçekleştirilen son değişikliği geri alır. Yeniden yapılması mümkün değildir. Open the Modify Table wizard, where it is possible to rename an existing table. It is also possible to add or delete fields from a table, as well as modify field names and types. - + Mevcut bir tablonun adını değiştirebileceğiniz Tablo Değiştirme sihirbazını açın. Ayrıca, bir tabloya alan ekleyebilir veya silebilir, alan adlarını ve türlerini değiştirebilirsiniz. Ctrl+Shift+T - + Ctrl+Shift+T &Save Project - + Projeyi &Kaydet This button lets you save all the settings associated to the open DB to a DB Browser for SQLite project file - + Bu düğme, açık DB ile ilişkili tüm ayarları SQLite proje dosyası için DB Tarayıcısına kaydetmenize olanak tanır Open &Project - + &Proje Aç This button lets you open a DB Browser for SQLite project file - + Bu düğme, SQLite proje dosyası için bir DB Tarayıcısı açmanıza olanak tanır Export to &JSON - + &JSON'a aktar @@ -3630,7 +3636,7 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul &Open Database - + Veritabanını &Aç @@ -3640,13 +3646,13 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul Drag && Drop SELECT Query - + Sürükle && Bırak SEÇ Sorgusu When dragging fields from the same table or a single table, drop a SELECT query into the editor - + Aynı tablodan veya tek bir tablodan alanları sürüklerken, düzenleyiciye bir SEÇ sorgusu bırakın @@ -3668,7 +3674,7 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul Use escaped identifiers (e.g. "Table1") when dragging the objects and dropping them into the editor - Nesneleri sürükleyip editöre bırakırken çıkış karakter belirleyicilerini kullanın(ör. "Tablo1") kullanın + Nesneleri sürükleyip düzenleyiciye bırakırken kaçış tanımlayıcıları (örneğin "Tablo1") kullanın @@ -3698,7 +3704,7 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul Run a quick integrity check over the open DB - Açık veritabanı üzerinde hızlı bir bütünlük denetimi yapın + Açık DB üzerinde hızlı bir bütünlük denetimi yapın @@ -3739,7 +3745,7 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul Print the structure of the opened database - Şu anda açık olan veritabanı yapısını yazdırın + Açılan veritabanının yapısını yazdırın @@ -3749,7 +3755,7 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul &Save Project As... - Projeyi &Farklı Kaydet... + Projeyi Farklı &Kaydet... @@ -3768,7 +3774,7 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul Save DB file, project file and opened SQL files - Veritabanı dosyasını, proje dosyasını ve açılan SQL dosyalarını kaydedin + DB dosyasını, proje dosyasını ve açılan SQL dosyalarını kaydedin @@ -3779,47 +3785,47 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul Ctrl+Shift+W - + Ctrl+Shift+W Table from CSV data in Clipboard... - + Panodaki CSV verilerinden tablo... This treats the current clipboard contents as a CSV file and opens the same import wizard that is used for importing CSV data from a file. - + Bu, geçerli panonun içeriğini bir CSV dosyası olarak ele alır ve bir dosyadan CSV verilerini içe aktarmak için kullanılan aynı içe aktarma sihirbazını açar. Show &Row Counts - + &Satır Sayılarını Göster This shows the number of rows for each table and view in the database. - + Bu, veritabanındaki her tablo ve görünüm için satır sayısını gösterir. Save Database &As... - + Veritabanını &Farklı Kaydet... Save the current database as a different file - + Geçerli veritabanını farklı bir dosya olarak kaydedin Refresh - Yenile + Yenile Reload the database structure - + Veritabanı yapısını yeniden yükle @@ -3859,7 +3865,7 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul Create &Index... - &Index Oluştur... + &İndeks Oluştur... @@ -3869,12 +3875,12 @@ SQL deyimlerini Şema sütunundan sürükleyip SQL editörüne veya diğer uygul &About - &İptal + &Hakkında This button opens a new tab for the SQL editor - Bu buton SQL editörü için yeni bir sekme açar + Bu düğme SQL editörü için yeni bir sekme açar @@ -4119,7 +4125,7 @@ Eğer cevabınız hayır ise biz SQL dosyasındaki verileri geçerli veritabanı Modify Index - Index'i Düzenle + İndeksi Düzenle @@ -4168,22 +4174,22 @@ Bunu yapmak istediğinize emin misiniz? Ctrl+Tab - + Ctrl+Tab Ctrl+Shift+Tab - + Ctrl+Shift+Tab Clear List - + Listeyi Temizle Window Layout - + Pencere Düzeni @@ -4193,32 +4199,32 @@ Bunu yapmak istediğinize emin misiniz? Simplify Window Layout - + Pencere Düzenini Basitleştirin Alt+Shift+0 - + Alt+Shift+0 Dock Windows at Bottom - + Pencereleri Altta Sabitle Dock Windows at Left Side - + Pencereleri Sol Tarafa Yerleştir Dock Windows at Top - + Pencereleri Üstte Sabitle The database is currently busy. - Verştabanı şu anda meşgul. + Veritabanı şu anda meşgul. @@ -4228,7 +4234,7 @@ Bunu yapmak istediğinize emin misiniz? Alt+Shift+W - + Alt+Shift+W @@ -4245,12 +4251,12 @@ Nedeni: %1 Choose a database file to save under - + Kaydedilecek bir veritabanı dosyası seçin Error while saving the database to the new file. - + Veritabanını yeni dosyaya kaydederken hata oluştu. @@ -4272,7 +4278,7 @@ Tabloyla ilişkili tüm veriler kaybedilecektir. Are you sure you want to delete the index '%1'? - '%1' indexsini silmek istediğinizden emin misiniz? + '%1' indeksini silmek istediğinizden emin misiniz? @@ -4292,7 +4298,7 @@ Tabloyla ilişkili tüm veriler kaybedilecektir. Error: could not delete the index. - Hata: index silinemedi. + Hata: indeks silinemedi. @@ -4311,12 +4317,12 @@ Veritabanını kaydetmek istediğinizden emin misiniz? Edit View %1 - + Görünümü Düzenle %1 Edit Trigger %1 - + Tetikleyiciyi Düzenle %1 @@ -4370,55 +4376,55 @@ Emin misiniz? Opened '%1' in read-only mode from recent file list - + Son dosya listesinden '%1' salt okunur modunda açıldı Opened '%1' from recent file list - + Son dosya listesinden '%1' açıldı Select the action to apply to the dropped file(s). <br/>Note: only 'Import' will process more than one file. Note for translation: Although there is no %n in the original, you can use the numerus-form to adjust 'files(s)' and remove the note when n = 1. Including %n in the translation will also work. - - Bırakılan dosyalara uygulanacak eylemi seçin. <br/>Not: yalnızca 'İçe Aktar' birden fazla dosyayı işleyecektir. + + Bırakılan dosyalara uygulanacak eylemi seçin. %n Not: yalnızca 'İçe Aktar' birden fazla dosyayı işler. The statements in the tab '%1' are still executing. Closing the tab will stop the execution. This might leave the database in an inconsistent state. Are you sure you want to close the tab? - + '%1' sekmesindeki ifadeler hâlâ yürütülüyor. Sekmeyi kapatmak yürütmeyi durduracaktır. Bu, veritabanını tutarsız bir durumda bırakabilir. Sekmeyi kapatmak istediğinizden emin misiniz? DB file '%1' could not be opened - + Veritabanı dosyası '%1' açılamadı This project file is using an old file format because it was created using DB Browser for SQLite version 3.10 or lower. Loading this file format is no longer fully supported. If you want to load it completely, please use DB Browser for SQLite version 3.12 to convert it to the new file format. - + Bu proje dosyası, DB Browser for SQLite sürüm 3.10 veya daha eski bir sürüm kullanılarak oluşturulduğu için eski bir dosya biçimi kullanıyor. Bu dosya biçiminin yüklenmesi artık tam olarak desteklenmiyor. Dosyayı tamamen yüklemek istiyorsanız lütfen DB Browser for SQLite sürüm 3.12'yi kullanarak yeni dosya biçimine dönüştürün. Table '%1' not found; settings ignored - + '%1' tablosu bulunamadı; ayarlar göz ardı edildi -- Reference to file "%1" (not supported by this version) -- - + -- "%1" dosyasına başvuru (bu sürüm tarafından desteklenmiyor) -- Yes. Don't ask again - + Evet. Bir daha sorma This action will open a new SQL tab with the following statements for you to edit and run: - + Bu eylem, düzenlemeniz ve çalıştırmanız için aşağıdaki ifadeleri içeren yeni bir SQL sekmesi açacaktır: @@ -4458,12 +4464,12 @@ Emin misiniz? Automatically load the last opened DB file at startup - + Başlangıçta son açılan DB dosyasını otomatik olarak yükle Ctrl+0 - + Ctrl+0 @@ -4504,7 +4510,7 @@ Emin misiniz? Don't show again - Bir daha gös'terme + Bir daha gösterme @@ -4568,7 +4574,7 @@ Bir yedek oluşturun! This action will open a new SQL tab for running: - Bu işlem çalıştırmak için yeni bir SQL sekmesi açar: + Bu işlem, çalıştırmak için yeni bir SQL sekmesi açar: @@ -4606,12 +4612,12 @@ Bir yedek oluşturun! Set to NULL - NULL olarak ayarlar + NULL olarak ayarla Alt+Del - + Alt+Del @@ -4639,17 +4645,17 @@ Bir yedek oluşturun! Y1 - + Y1 Y2 - + Y2 Axis Type - Eksen Tipi + Eksen Türü @@ -4815,7 +4821,7 @@ Yalnızca geçerli yönde sürüklemek ve yakınlaştırmak için eksen veya eks Help - + Yardım @@ -4830,7 +4836,7 @@ Yalnızca geçerli yönde sürüklemek ve yakınlaştırmak için eksen veya eks Fixed number format - + Sabit sayı biçimi @@ -4878,7 +4884,7 @@ Uyarı: Kısmi yükleme mekanizması nedeniyle tüm veriler tablodan henüz alı Choose a filename to save under - Altına kaydetmek için dosya ismi seçiniz + Altına kaydetmek için dosya ismi seçin @@ -5008,17 +5014,17 @@ Uyarı: Kısmi yükleme mekanizması nedeniyle tüm veriler tablodan henüz alı Default field type - Varsayılan dosya tipi + Varsayılan alan türü Font - Yazı + Yazı Tipi &Font - &Yazı + &Yazı Tipi @@ -5043,7 +5049,7 @@ Uyarı: Kısmi yükleme mekanizması nedeniyle tüm veriler tablodan henüz alı Enable this option to show a preview of BLOBs containing image data in the cells. This can affect the performance of the data browser, however. - Hücrelerde görüntü verileri içeren BLOB tipindeki verilerin önizlemesini göstermek için bu seçeneği etkinleştirin. Ancak bu, veri görüntüleyicisinin performansını etkileyebilir. + Hücrelerde görüntü verileri içeren BLOB tipindeki verilerin ön izlemesini göstermek için bu seçeneği etkinleştirin. Ancak bu, veri görüntüleyicisinin performansını etkileyebilir. @@ -5063,7 +5069,7 @@ Uyarı: Kısmi yükleme mekanizması nedeniyle tüm veriler tablodan henüz alı Background - Arka plan + Arka Plan @@ -5082,7 +5088,7 @@ Uyarı: Kısmi yükleme mekanizması nedeniyle tüm veriler tablodan henüz alı Only display the icon - Sadece ikonu göster + Sadece simgeyi göster @@ -5143,17 +5149,17 @@ Uyarı: Kısmi yükleme mekanizması nedeniyle tüm veriler tablodan henüz alı Browse Data - Verileri Görüntüle + Verilere Göz At Execute SQL - SQL kodunu yürütme + SQL kodunu yürüt Edit Database Cell - Veritabanı Hücresini Düzenleme + Veritabanı Hücresini Düzenle @@ -5168,12 +5174,12 @@ Uyarı: Kısmi yükleme mekanizması nedeniyle tüm veriler tablodan henüz alı Dark style - Koyu Tema + Koyu tema Light style - + Aydınlık stil @@ -5183,28 +5189,29 @@ Uyarı: Kısmi yükleme mekanizması nedeniyle tüm veriler tablodan henüz alı This sets the font size for all UI elements which do not have their own font size option. - + Bu, kendi yazı tipi boyutu seçeneği olmayan tüm kullanıcı arayüzü ögelerinin yazı tipi boyutunu ayarlar. Font size - + Yazı tipi boyutu Max Recent Files - + En Son Dosyalar Prompt to save SQL tabs in new project file - + SQL sekmelerini kaydetme istemi +yeni proje dosyasına If this is turned on, then changes to the SQL editor generate a save a project confirmation dialog when closing the SQL editor tab. - + Bu açıksa SQL düzenleyici sekmesi kapatıldığında SQL düzenleyicide yapılan değişiklikler bir proje kaydetme onay iletişim kutusu oluşturur. @@ -5214,17 +5221,17 @@ in new project file Database structure font size - + Veritabanı yapısı yazı tipi boyutu Font si&ze - Yazı B&oyutu + Yazı tipi b&oyutu Formatted - + Biçimlendirilmiş @@ -5232,9 +5239,9 @@ in new project file Maximum number of rows in a table for enabling the value completion based on current values in the column. Maximum number of indexes in a selection for calculating sum and average. Can be set to 0 for disabling the functionalities. - Bu, bazı hesaplama açısından pahalı işlevlerin etkinleştirilmesine izin verilen maksimum öğe sayısıdır: + Bu, bazı hesaplama açısından pahalı işlevlerin etkinleştirilmesine izin verilen maksimum öge sayısıdır: Sütundaki geçerli değerlere dayalı olarak değer tamamlamayı etkinleştirmek için bir tablodaki maksimum satır sayısı. -Toplam ve ortalamayı hesaplamak için bir seçimdeki maksimum index sayısı. +Toplam ve ortalamayı hesaplamak için bir seçimdeki maksimum indeks sayısı. İşlevleri devre dışı bırakmak için 0 olarak ayarlanabilir. @@ -5247,42 +5254,42 @@ Tamamlamayı devre dışı bırakmak için 0 olarak ayarlanabilir. Selection background - + Seçim arka planı Selection foreground - + Seçim ön planı Highlight - + Vurgu Use tabs for indentation - + Girinti için sekmeleri kullanın When set, the Tab key will insert tab and space characters for indentation. Otherwise, just spaces will be used. - + Ayarlandığında, Tab tuşu girinti için sekme ve boşluk karakterleri ekler. Aksi takdirde, yalnızca boşluklar kullanılır. Close button on tabs - + Sekmelerdeki düğmeyi kapat If enabled, SQL editor tabs will have a close button. In any case, you can use the contextual menu or the keyboard shortcut to close them. - + Etkinleştirildiğinde, SQL düzenleyici sekmelerinde bir kapatma düğmesi bulunur. Her durumda, bağlam menüsünü veya klavye kısayolunu kullanarak bunları kapatabilirsiniz. Select built-in extensions to load for every database: - + Her veritabanı için yüklenecek yerleşik uzantıları seçin: @@ -5297,12 +5304,12 @@ Tamamlamayı devre dışı bırakmak için 0 olarak ayarlanabilir. Export Settings - + Ayarları Dışa Aktar Import Settings - + Ayarları İçe Aktar @@ -5340,7 +5347,7 @@ Tamamlamayı devre dışı bırakmak için 0 olarak ayarlanabilir. Preview only (N/A) - Sadece önizleme (N/A) + Sadece ön izleme (N/A) @@ -5355,7 +5362,7 @@ Tamamlamayı devre dışı bırakmak için 0 olarak ayarlanabilir. Set the waiting time before a new filter value is applied. Can be set to 0 for disabling waiting. - Yeni bir filtre değeri uygulanmadan önce bekleme süresini ayarlayın. Beklemeyi devre dışı bırakmak için 0 olarak ayarlanabilir. + Yeni bir filtre değeri uygulanmadan önce bekleme süresini ayarla. Beklemeyi devre dışı bırakmak için 0 değeri ayarlanabilir. @@ -5415,7 +5422,7 @@ Tamamlamayı devre dışı bırakmak için 0 olarak ayarlanabilir. String - String + Dize @@ -5425,7 +5432,7 @@ Tamamlamayı devre dışı bırakmak için 0 olarak ayarlanabilir. SQL &editor font size - SQL &Editör yazı boyutu + SQL &editör yazı tipi boyutu @@ -5460,7 +5467,7 @@ Tamamlamayı devre dışı bırakmak için 0 olarak ayarlanabilir. &Quotes for identifiers - Tanımlıyıcılar için &tırnaklar + Tanımlayıcılar için &tırnaklar @@ -5531,7 +5538,7 @@ Tamamlamayı devre dışı bırakmak için 0 olarak ayarlanabilir. Subject O - Konu O + O Konusu @@ -5542,7 +5549,7 @@ Tamamlamayı devre dışı bırakmak için 0 olarak ayarlanabilir. Valid from - Şundan tarihten itibaren geçerli + Şu tarihten itibaren geçerli @@ -5589,7 +5596,7 @@ Tamamlamayı devre dışı bırakmak için 0 olarak ayarlanabilir. SQL editor &font - SQL Editör &yazı boyutu + SQL editör &yazı tipi @@ -5599,12 +5606,12 @@ Tamamlamayı devre dışı bırakmak için 0 olarak ayarlanabilir. Hori&zontal tiling - Ya&tay Döşeme + Ya&tay döşeme If enabled the SQL code editor and the result table view are shown side by side instead of one over the other. - Etkinleştirilirse, SQL kod düzenleyicisi ve sonuç tablosu görünümü üst üste yerine yan yana gösterilir. + Etkinleştirilirse SQL kod düzenleyicisi ve sonuç tablosu görünümü üst üste yerine yan yana gösterilir. @@ -5698,35 +5705,37 @@ Tüm tercihleriniz kaybolacak ve varsayılan değerler kullanılacak. Save Settings File - + Ayarlar Dosyasını Kaydet Initialization File (*.ini) - + Başlatma Dosyası (*.ini) The settings file has been saved in location : - + Ayarlar dosyası şu konuma kaydedildi: + Open Settings File - + Ayarlar Dosyasını Aç The settings file was loaded properly. - + Ayarlar dosyası düzgün bir şekilde yüklendi. The selected settings file is not a normal settings file. Please check again. - + Seçilen ayar dosyası normal bir ayar dosyası değil. + Lütfen tekrar kontrol edin. @@ -5739,7 +5748,7 @@ Please check again. Pro&xy Type - Pro&xy Tipi + Pro&xy Türü @@ -5893,7 +5902,7 @@ Please check again. JSON Files (*.json *.js) - JSON dosyaları (*.json *.js) + JSON Dosyaları (*.json *.js) @@ -5923,7 +5932,7 @@ Please check again. Initialization File (*.ini) - + Başlatma Dosyası (*.ini) @@ -6020,37 +6029,37 @@ Please check again. Commit ID - + Commit ID Message - + Mesaj Date - Tarih + Tarih Author - + Yazar Size - Boyut + Boyut Authored and committed by %1 - + %1 tarafından yazıldı ve işlendi Authored by %1, committed by %2 - + %1 tarafından yazıldı, %2 tarafından işlendi @@ -6090,142 +6099,142 @@ Please check again. Upload - + Yükle DBHub.io - + DBHub.io <html><head/><body><p>In this pane, remote databases from dbhub.io website can be added to DB Browser for SQLite. First you need an identity:</p><ol style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Login to the dbhub.io website (use your GitHub credentials or whatever you want)</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Click the button to &quot;Generate client certificate&quot; (that's your identity). That'll give you a certificate file (save it to your local disk).</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Go to the Remote tab in DB Browser for SQLite Preferences. Click the button to add a new certificate to DB Browser for SQLite and choose the just downloaded certificate file.</li></ol><p>Now the Remote panel shows your identity and you can add remote databases.</p></body></html> - + <html><head/><body><p>Bu bölmede, dbhub.io web sitesinden uzak veritabanları SQLite için DB Tarayıcısına eklenebilir. Öncelikle bir kimliğe ihtiyacınız var:</p><ol style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">dbhub.io web sitesine giriş yapın (GitHub kimlik bilgilerinizi veya istediğiniz herhangi bir şeyi kullanın)</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">"İstemci sertifikası oluştur" düğmesine tıklayın (bu sizin kimliğinizdir). Bu size bir sertifika dosyası verecektir (bunu yerel diskinize kaydedin).</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">SQLite için Veritabanı Tarayıcısı Tercihleri'ndeki Uzak sekmesine gidin. SQLite için Veritabanı Tarayıcısı'na yeni bir sertifika eklemek için düğmeye tıklayın ve az önce indirdiğiniz sertifika dosyasını seçin.</li></ol><p>Artık Uzak paneli kimliğinizi gösterecek ve uzak veritabanları ekleyebilirsiniz.</p></body></html> Local - + Yerel Current Database - + Güncel Veritabanı Clone - + Klonla Branch - Branch + Dal Commits - + Commitler Commits for - + Commitler için Delete Database - + Veritabanını Sil Delete the local clone of this database - + Bu veritabanının yerel klonunu silin Open in Web Browser - + Web Tarayıcısında Aç Open the web page for the current database in your browser - + Tarayıcınızda geçerli veritabanının web sayfasını açın Clone from Link - + Bağlantıdan Klonla Use this to download a remote database for local editing using a URL as provided on the web page of the database. - + Bunu, veritabanının web sayfasında sağlanan bir URL'yi kullanarak yerel düzenleme için uzak bir veritabanını indirmek için kullanın. Refresh - Yenile + Yenile Reload all data and update the views - + Tüm verileri yeniden yükle ve görünümleri güncelle Clone Database - + Veritabanını Klonla Open Database - + Veritabanını Aç Open the local copy of this database - + Bu veritabanının yerel kopyasını açın Check out Commit - + Commit'i Kontrol Edin Download and open this specific commit - + Bu özel commiti indirin ve açın Check out Latest Commit - + En Son Commiti Kontrol Edin Check out the latest commit of the current branch - + Mevcut dalın en son commitini kontrol edin Save Revision to File - + Revizyonu Dosyaya Kaydet Saves the selected revision of the database to another file - + Veritabanının seçili revizyonunu başka bir dosyaya kaydeder Upload Database - + Veritabanını Yükle Upload this database as a new commit - + Bu veritabanını yeni bir commit olarak yükleyin @@ -6235,12 +6244,12 @@ Please check again. &User - + &Kullanıcı &Database - &Veritabanı + &Veritabanı @@ -6250,12 +6259,12 @@ Please check again. Select an identity to connect - + Bağlanmak için bir kimlik seçin Public - + Genel @@ -6263,43 +6272,47 @@ Please check again. Please enter the URL to clone from. You can generate this URL by clicking the 'Clone Database in DB4S' button on the web page of the database. - + Bu, yerel düzenleme için uzak bir sunucudan bir veritabanı indirir. +Lütfen klonlanacak URL'yi girin. Bu URL'yi, veritabanının web +sayfasındaki 'DB4S'de Veritabanını Klonla' düğmesine +tıklayarak oluşturabilirsiniz. Invalid URL: The host name does not match the host name of the current identity. - + Geçersiz URL: Ana bilgisayar adı, geçerli kimliğin ana bilgisayar adıyla eşleşmiyor. Invalid URL: No branch name specified. - + Geçersiz URL: Herhangi bir dal adı belirtilmedi. Invalid URL: No commit ID specified. - + Geçersiz URL: Belirtilen bir commit kimliği yok. You have modified the local clone of the database. Fetching this commit overrides these local changes. Are you sure you want to proceed? - + Veritabanının yerel klonunu değiştirdiniz. Bu commit'i almak, bu yerel değişiklikleri geçersiz kılar. +Devam etmek istediğinizden emin misiniz? The database has unsaved changes. Are you sure you want to push it before saving? - + Veritabanında kaydedilmemiş değişiklikler var. Kaydetmeden önce göndermek istediğinizden emin misiniz? The database you are trying to delete is currently opened. Please close it before deleting. - + Silmeye çalıştığınız veritabanı şu anda açık. Lütfen silmeden önce kapatın. This deletes the local version of this database with all the changes you have not committed yet. Are you sure you want to delete this database? - + Bu, henüz kaydetmediğiniz tüm değişikliklerle birlikte bu veritabanının yerel sürümünü siler. Bu veritabanını silmek istediğinizden emin misiniz? @@ -6307,32 +6320,32 @@ Are you sure you want to proceed? Name - İsim + İsim Branch - Branch + Dal Last modified - Son değiştirilme + Son değiştirilme Size - Boyut + Boyut Commit - Commit + Commit File - Dosya + Dosya @@ -6340,42 +6353,42 @@ Are you sure you want to proceed? Name - İsim + İsim Commit - Commit + Commit Last modified - Son değiştirilme + Son değiştirilme Size - Boyut + Boyut Size: - + Boyut: Last Modified: - + Son Değiştirilme: Licence: - + Lisans: Default Branch: - + Varsayılan Dal: @@ -6383,47 +6396,48 @@ Are you sure you want to proceed? Choose a location to save the file - + Dosyayı kaydetmek için bir konum seçin Error opening remote file at %1. %2 - %1 adresinde bulunan dosya açılamadı. %2 + %1 adresinde bulunan dosya açılamadı. +%2 Error: Invalid client certificate specified. - Hata: Geçersiz istemci sertifikası belirtildi. + Hata: Geçersiz istemci sertifikası belirtildi. Please enter the passphrase for this client certificate in order to authenticate. - Kimlik doğrulaması için lütfen istemci sertifikasının parolasını girin. + Kimlik doğrulaması için lütfen istemci sertifikasının parolasını girin. Cancel - İptal + İptal Uploading remote database to %1 - Uzak veritabanı karşıya yükleniyor + Uzak veritabanı karşıya yükleniyor %1 Downloading remote database from %1 - Uzaktaki sunucu indiriliyor: + Uzaktaki sunucu indiriliyor: %1 Error: Cannot open the file for sending. - Hata: Dosya gönderim için açılamadı. + Hata: Dosya gönderim için açılamadı. @@ -6431,12 +6445,12 @@ Are you sure you want to proceed? Push database - Veritabanını aktar + Veritabanını zorla Database na&me to push to - Aktarılacak veritaba&nı adı + Gönderilecek veritaba&nı adı @@ -6451,22 +6465,22 @@ Are you sure you want to proceed? Public - Halka açık + Genel Branch - Branch + Dal Force push - Aktarmaya zorla + Zorla itme Username - + Kullanıcı adı @@ -6517,7 +6531,7 @@ Are you sure you want to proceed? Sele&cted - &Seçilen + &Seçili @@ -6540,7 +6554,7 @@ Are you sure you want to proceed? Shift+F3 - + Shift+F3 @@ -6565,12 +6579,12 @@ Are you sure you want to proceed? The found pattern must match in letter case - Bulunan desen büyük küçük harfe duyarlı olmalıdır + Bulunan desen büyük/küçük harfe duyarlı olmalıdır Case Sensitive - Büyük küçük harfe duyarı + Büyük/küçük harfe duyarı @@ -6585,7 +6599,7 @@ Are you sure you want to proceed? F3 - + F3 @@ -6606,7 +6620,7 @@ Are you sure you want to proceed? Close Find Bar - Araba çubuğunu kapat + Bulma Çubuğunu Kapat @@ -6626,17 +6640,17 @@ Are you sure you want to proceed? Ctrl+PgUp - + Ctrl+PgUp Ctrl+PgDown - + Ctrl+PgDown Couldn't read file "%1": %2. - + "%1" dosyası okunamadı: %2. @@ -6657,17 +6671,17 @@ Are you sure you want to proceed? Answer "Yes to All" to reload the file on any external update without further prompting. - + Herhangi bir harici güncellemede fazla soru sormadan dosyayı yeniden yüklemek için "Tümüne Evet" yanıtını verin. Answer "No to All" to ignore any external update without further prompting. - + Herhangi bir harici güncellemeyi fazla uyarı almadan yok saymak için "Tümüne Hayır" cevabını verin. Modifying and saving the file will restore prompting. - + Dosyayı düzenleyip kaydettiğinizde istem tekrar aktif olacak. @@ -6723,7 +6737,7 @@ Are you sure you want to proceed? (X,Y,Z) The iif(X,Y,Z) function returns the value Y if X is true, and Z otherwise. - + (X,Y,Z) The iif(X,Y,Z) fonksiyonu, X doğruysa Y değerini, aksi takdirde Z değerini döndürür. @@ -6970,13 +6984,13 @@ Bu işlevin kullanımına Tercihler'den izin verilmelidir. (expr,offset) If the offset argument is provided, then it must be a non-negative integer. In this case the value returned is the result of evaluating expr against the row offset rows before the current row within the partition. If offset is 0, then expr is evaluated against the current row. If there is no row offset rows before the current row, NULL is returned. - (expr,offset) Uzaklık değişkeni sağlanırsa, negatif olmayan bir tam sayı olmalıdır. Bu durumda, döndürülen değer, bölüm içindeki geçerli satırdan önce satır ofseti satırlarına göre ifade değerlendirmesinin sonucudur. Ofset 0 ise, ifade geçerli satıra göre değerlendirilir. Geçerli satırdan önce satır kaydırma satırları yoksa, NULL döndürülür. + (expr,offset) Uzaklık değişkeni sağlanırsa negatif olmayan bir tam sayı olmalıdır. Bu durumda, döndürülen değer, bölüm içindeki geçerli satırdan önce satır ofseti satırlarına göre ifade değerlendirmesinin sonucudur. Ofset 0 ise ifade geçerli satıra göre değerlendirilir. Geçerli satırdan önce satır kaydırma satırları yoksa NULL döndürülür. (expr,offset,default) If default is also provided, then it is returned instead of NULL if the row identified by offset does not exist. - (expr,offset,default) Varsayılan da sağlanmışsa, ofset ile tanımlanan satır yoksa NULL döndürülür. + (expr,offset,default) Varsayılan da sağlanmışsa ofset ile tanımlanan satır yoksa NULL döndürülür. @@ -6986,7 +7000,7 @@ Bu işlevin kullanımına Tercihler'den izin verilmelidir. (expr,offset) If the offset argument is provided, then it must be a non-negative integer. In this case the value returned is the result of evaluating expr against the row offset rows after the current row within the partition. If offset is 0, then expr is evaluated against the current row. If there is no row offset rows after the current row, NULL is returned. - (expr,offset) Uzaklık değişkeni sağlanırsa, negatif olmayan bir tam sayı olmalıdır. Bu durumda, döndürülen değer, bölüm içindeki geçerli satırdan sonra ifade ofset satırlarına göre ifade değerlendirmesinin sonucudur. Ofset 0 ise, ifade geçerli satıra göre değerlendirilir. Geçerli satırdan sonra satır ofseti satırı yoksa, NULL döndürülür. + (expr,offset) Uzaklık değişkeni sağlanırsa negatif olmayan bir tam sayı olmalıdır. Bu durumda, döndürülen değer, bölüm içindeki geçerli satırdan sonra ifade ofset satırlarına göre ifade değerlendirmesinin sonucudur. Ofset 0 ise ifade geçerli satıra göre değerlendirilir. Geçerli satırdan sonra satır ofseti satırı yoksa NULL döndürülür. @@ -7001,145 +7015,145 @@ Bu işlevin kullanımına Tercihler'den izin verilmelidir. (expr,N) This built-in window function calculates the window frame for each row in the same way as an aggregate window function. It returns the value of expr evaluated against the row N of the window frame. Rows are numbered within the window frame starting from 1 in the order defined by the ORDER BY clause if one is present, or in arbitrary order otherwise. If there is no Nth row in the partition, then NULL is returned. - (expr,N) Bu yerleşik pencere işlevi, her satır için pencere çerçevesini birleştirilmiş pencere işlevi ile aynı şekilde hesaplar. Pencere çerçevesinin N satırına göre değerlendirilen ifade değerini döndürür. Satırlar, pencere çerçevesi içinde 1'den başlayarak, ORDER BY deyimi tarafından varsa veya başka bir şekilde rastgele sırada numaralandırılır. Bölümde N'inci satırı yoksa, NULL döndürülür. + (expr,N) Bu yerleşik pencere işlevi, her satır için pencere çerçevesini birleştirilmiş pencere işlevi ile aynı şekilde hesaplar. Pencere çerçevesinin N satırına göre değerlendirilen ifade değerini döndürür. Satırlar, pencere çerçevesi içinde 1'den başlayarak, ORDER BY deyimi tarafından varsa veya başka bir şekilde rastgele sırada numaralandırılır. Bölümde N'inci satırı yoksa NULL döndürülür. (X) Return the arccosine of X. The result is in radians. - + (X) X'in ark kosinüsünü döndür. Sonuç radyan cinsindendir. (X) Return the hyperbolic arccosine of X. - + (X) X'in hiperbolik ark kosinüsünü döndür. (X) Return the arcsine of X. The result is in radians. - + (X) X'in ark sinüsünü döndürür. Sonuç radyan cinsindendir. (X) Return the hyperbolic arcsine of X. - + (X) X'in hiperbolik ark sinüsünü döndürür. (X) Return the arctangent of X. The result is in radians. - + (X) X'in ark tanjantını döndürür Sonuç radyan cinsindendir. (X,Y) Return the arctangent of Y/X. The result is in radians. The result is placed into correct quadrant depending on the signs of X and Y. - + (X,Y) Y/X'in ark tanjantını döndürür. Sonuç radyan cinsindendir. Sonuç, X ve Y'nin işaretlerine bağlı olarak doğru kadrana yerleştirilir. (X) Return the hyperbolic arctangent of X. - + (X) X'in hiperbolik ark tanjantını döndürür. (X) Return the first representable integer value greater than or equal to X. For positive values of X, this routine rounds away from zero. For negative values of X, this routine rounds toward zero. - + (X) X'ten büyük veya ona eşit ilk temsil edilebilir tam sayı değerini döndürür. X'in pozitif değerleri için bu rutin sıfırdan uzaklaşır. X'in negatif değerleri için bu rutin sıfıra doğru yuvarlar. (X) Return the cosine of X. X is in radians. - + (X) X'in kosinüsünü döndürür. X radyan cinsindendir. (X) Return the hyperbolic cosine of X. - + (X) X'in hiperbolik kosinüsünü döndürür. (X) Convert value X from radians into degrees. - + (X) X değerini radyandan dereceye dönüştürür. (X) Compute e (Euler's number, approximately 2.71828182845905) raised to the power X. - + (X) e'nin (Euler sayısı, yaklaşık 2.71828182845905) X kuvvetine yükseltilmiş halini hesaplayın. (X) Return the first representable integer value less than or equal to X. For positive numbers, this function rounds toward zero. For negative numbers, this function rounds away from zero. - + (X) X'ten küçük veya ona eşit ilk temsil edilebilir tam sayı değerini döndürür. Pozitif sayılar için bu fonksiyon sıfıra doğru yuvarlar. Negatif sayılar için bu fonksiyon sıfırdan uzaklaşır. (X) Return the natural logarithm of X. - + X) X'in doğal logaritmasını döndürür. (B,X) Return the base-B logarithm of X. - + (B,X) X'in B tabanındaki logaritmasını döndürür. (X) Return the base-10 logarithm for X. - + (X) X'in 10 tabanlı logaritmasını döndür. (X) Return the logarithm base-2 for the number X. - + (X) X sayısının 2 tabanındaki logaritmasını döndürür. (X,Y) Return the remainder after dividing X by Y. - + (X,Y) X'i Y'ye böldükten sonra kalanı döndürür. () Return an approximation for π. - + () π için bir yaklaşım değeri döndürür. (X,Y) Compute X raised to the power Y. - + (X,Y) X'in Y'ye yükseltilmiş değerini hesaplayın. (X) Convert X from degrees into radians. - + (X) X'i dereceden radyana dönüştürün. (X) Return the sine of X. X is in radians. - + (X) X'in sinüsünü döndürür. X radyan cinsindendir. (X) Return the hyperbolic sine of X. - + (X) X'in hiperbolik sinüsünü döndürür. (X) Return the square root of X. NULL is returned if X is negative. - + (X) X'in karekökünü döndürür. X negatifse NULL döndürülür. (X) Return the tangent of X. X is in radians. - + (X) X'in tanjantını döndürür. X radyan cinsindendir. (X) Return the hyperbolic tangent of X. - + (X) X'in hiperbolik tanjantını döndürür. (X) Return the representable integer in between X and 0 (inclusive) that is furthest away from zero. Or, in other words, return the integer part of X, rounding toward zero. - + (X) X ile 0 (dahil) arasında sıfırdan en uzak olan gösterilebilir tam sayıyı döndürür. Ya da başka bir deyişle, X'in tam sayı kısmını sıfıra yuvarlayarak döndürür. @@ -7216,12 +7230,12 @@ Buraya atlamak için %3Shift'e basılı tutun ve tıklayın - Ctrl+' for copying the value from the cell above. - Standard selection and copy/paste operations. Bu veritabanı tablosu görünümüdür. Aşağıdaki işlemleri yapabilirsiniz: -  - Satır içi değeri düzenlemek için yazmaya başlayın. +  - Değeri satır içi olarak düzenlemek için yazmaya başlayın.   - İçeriklerini hücre düzenleyici penceresinde düzenlemek için herhangi bir kaydı çift tıklayın. -  - Hücre içeriğini NULL'a dönüştürmek için Alt + Del tuşlarına basın. -  - Geçerli kaydı çoğaltmak için Ctrl + "tuşlarına basın. +  -Hücre içeriğini NULL olarak silmek için Alt+Del tuşlarına basın. +  - Geçerli kaydı kopyalamak için Ctrl + "tuşlarına basın.   - Yukarıdaki hücreden değeri kopyalamak için Ctrl + '. -  - Standart seçim ve kopyalama / yapıştırma işlemleri. +  - Standart seçim ve kopyala / yapıştır işlemleri. @@ -7271,7 +7285,7 @@ Buraya atlamak için %3Shift'e basılı tutun ve tıklayın Case Sensitive - Büyük Küçük Harfe Duyarlı + Büyük/Küçük Harfe Duyarlı @@ -7286,7 +7300,7 @@ Buraya atlamak için %3Shift'e basılı tutun ve tıklayın Interpret search pattern as a regular expression - Arama desenini düzenliifade olarak yorumlama + Arama desenini düzenli ifade olarak yorumla @@ -7338,44 +7352,44 @@ Buraya atlamak için %3Shift'e basılı tutun ve tıklayın Export to &JSON - + &JSON'a aktar Export the filtered data to JSON - + Filtrelenmiş verileri JSON'a aktar This button exports the data of the browsed table as currently displayed (after filters, display formats and order column) as a JSON file. - + Bu buton, taranan tablonun verilerini şu anda görüntülendiği şekilde (filtreler, görüntüleme biçimleri ve sıralama sütunundan sonra) JSON dosyası olarak dışa aktarır. Copy column name - + Sütun adını kopyala Copy the database table column name to your clipboard - + Veritabanı tablosu sütun adını panonuza kopyalayın New Data Browser - + Yeni Veri Tarayıcısı Add a new docked Data Browser - + Yeni bir yerleşik Veri Tarayıcısı ekleyin This button adds a new docked Data Browser, which you can detach and arrange in different layouts. - + Bu düğme, farklı düzenlerde ayırıp düzenleyebileceğiniz yeni bir yerleşik Veri Tarayıcısı ekler. @@ -7435,7 +7449,7 @@ Buraya atlamak için %3Shift'e basılı tutun ve tıklayın <html><head/><body><p>Clicking this button navigates up to the end in the table view above.</p></body></html> - + <html><head/><body><p>Bu düğmeye tıklandığında yukarıdaki tablo görünümünde en sona gidilir.</p></body></html> @@ -7455,7 +7469,7 @@ Buraya atlamak için %3Shift'e basılı tutun ve tıklayın Go to: - Bu kayda gidin: + Git: @@ -7465,7 +7479,7 @@ Buraya atlamak için %3Shift'e basılı tutun ve tıklayın Type a record number in this area and click the Go to: button to display the record in the database view - Bu alana veritabanı görünümünde görüntülemek istediğiniz kayıt numarasını giriniz ve Bu kayda gidin butonuna tıklayınız + Bu alana veritabanı görünümünde görüntülemek istediğiniz kayıt numarasını giriniz ve Git butonuna tıklayınız @@ -7475,7 +7489,7 @@ Buraya atlamak için %3Shift'e basılı tutun ve tıklayın Show rowid column - rowid sütununu göster + Rowid sütununu göster @@ -7609,7 +7623,7 @@ Buraya atlamak için %3Shift'e basılı tutun ve tıklayın Hide selected column(s) - Seçilen sütunları gizle + Seçili sütunları gizle @@ -7625,22 +7639,22 @@ Buraya atlamak için %3Shift'e basılı tutun ve tıklayın Set encoding - Kodlama seç + Kodlamayı ayarla Change the encoding of the text in the table cells - Tablo hücrelerindeki metnin kodlamasını değiştirme + Tablo hücrelerindeki metnin kodlamasını değiştir Set encoding for all tables - Tüm tablolar için kodlama seç + Tüm tablolar için kodlama ayarla Change the default encoding assumed for all tables in the database - Veritabanındaki tüm tablolar için varsayılan kodlamayı değiştirme + Veritabanındaki tüm tablolar için varsayılan kodlamayı değiştir @@ -7687,7 +7701,7 @@ Buraya atlamak için %3Shift'e basılı tutun ve tıklayın Print currently browsed table data. Print selection if more than one cell is selected. - Şu anda görüntülenen tablo verilerini yazdırın. Birden fazla hücre seçilirse seçimi yazdırın. + Şu anda görüntülenen tablo verilerini yazdır. Birden fazla hücre seçilirse seçimi yazdır. @@ -7727,12 +7741,12 @@ Buraya atlamak için %3Shift'e basılı tutun ve tıklayın Freeze columns - + Sütunları dondur Make all columns from the first column up to this column not move when scrolling horizontally - + Yatay kaydırma sırasında ilk sütundan bu sütuna kadar olan tüm sütunların hareket etmemesini sağlayın @@ -7817,7 +7831,7 @@ Buraya atlamak için %3Shift'e basılı tutun ve tıklayın Font Color - Yazı Rengi + Yazı Tipi Rengi @@ -7859,7 +7873,7 @@ Buraya atlamak için %3Shift'e basılı tutun ve tıklayın Filter in any column - + Herhangi bir sütunda filtreleme yapın @@ -7898,17 +7912,17 @@ Buraya atlamak için %3Shift'e basılı tutun ve tıklayın %L1 - %L2 of >= %L3 - + %L1 - %L2 of >= %L3 %L1 - %L2 of %L3 - + %L1 - %L2 of %L3 (clipped at %L1 rows) - + (%L1 satırlarında kesildi) @@ -7933,12 +7947,12 @@ Buraya atlamak için %3Shift'e basılı tutun ve tıklayın Ctrl+" - + Ctrl+" Adjust rows to contents - Satırları içeriklere göre ayarlama + Satırları içeriklere göre ayarla @@ -7950,7 +7964,7 @@ Buraya atlamak için %3Shift'e basılı tutun ve tıklayın Please select a record first - Lütfen öncelikle kaydı seçiniz + Lütfen önce bir kayıt seçin @@ -7985,22 +7999,22 @@ Veritabanı kodlamasını kullanmak için alanı boş bırakın. New Data Browser - + Yeni Veri Tarayıcısı Rename Data Browser - + Veri Tarayıcısını Yeniden Adlandır Close Data Browser - + Veri Tarayıcısını Kapat Set a new name for the data browser. Use the '&&' character to allow using the following character as a keyboard shortcut. - + Veri tarayıcısı için yeni bir ad belirleyin. Aşağıdaki karakteri klavye kısayolu olarak kullanabilmek için '&&' karakterini kullanın. @@ -8018,7 +8032,7 @@ Veritabanı kodlamasını kullanmak için alanı boş bırakın. Please select the databases to co&mpact: - Sıkıştır&mak istediğiniz veritabanını seçiniz: + Sıkıştır&mak istediğiniz veritabanını seçin: diff --git a/tools/linux/sqlcipher_rename.sh b/tools/linux/sqlcipher_rename.sh new file mode 100644 index 0000000000..4bc9dca695 --- /dev/null +++ b/tools/linux/sqlcipher_rename.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +# rename.sh +# A script to revert SQLCipher v.4.7.0+ build artifacts being named 'sqlite3'. + +patch_pkgconfig() { + PC_FILE="${1:-sqlite3.pc}" + + if [[ ! -f "$PC_FILE" ]]; then + echo "Error: file not found: $PC_FILE" >&2 + exit 1 + fi + + tmp="$(mktemp)" + trap 'rm -f "$tmp"' EXIT + + awk ' + NR==6 { + if ($0 !~ /\/sqlcipher$/) $0 = $0 "/sqlcipher" + } + NR==8 { + sub(/^Name:[[:space:]]*SQLite[[:space:]]*$/, "Name: SQLCipher") + } + NR==11 { + gsub(/-lsqlite3/, "-lsqlcipher") + } + { print } + ' "$PC_FILE" > "$tmp" + + cp "$tmp" "$PC_FILE" +} + +VERSION=$(cat VERSION) + +mv ./prefix/bin/sqlite3 ./prefix/bin/sqlcipher +mv ./prefix/lib/libsqlite3.a ./prefix/lib/libsqlcipher.a +mv ./prefix/lib/libsqlite3.so.$VERSION ./prefix/lib/libsqlcipher.so.$VERSION +ln -s "$(pwd)/prefix/lib/libsqlcipher.so.$VERSION" ./prefix/lib/libsqlcipher.so +ln -s "$(pwd)/prefix/lib/libsqlcipher.so.$VERSION" ./prefix/lib/libsqlcipher.so.0 +rm ./prefix/lib/libsqlite* +patch_pkgconfig ./prefix/lib/pkgconfig/sqlite3.pc +mv ./prefix/lib/pkgconfig/sqlite3.pc ./prefix/lib/pkgconfig/sqlcipher.pc +mkdir ./prefix/include/sqlcipher +mv ./prefix/include/*.h ./prefix/include/sqlcipher/ +mv ./prefix/share/man/man1/sqlite3.1 ./prefix/share/man/man1/sqlcipher.1 diff --git a/tools/windows/add_sqlean_arm64_target.patch b/tools/windows/add_sqlean_arm64_target.patch new file mode 100644 index 0000000000..400f00896d --- /dev/null +++ b/tools/windows/add_sqlean_arm64_target.patch @@ -0,0 +1,45 @@ +diff --git a/Makefile b/Makefile +index 5033c1d..ac038f1 100644 +--- a/Makefile ++++ b/Makefile +@@ -10,11 +10,13 @@ SQLITE_BRANCH := 3.36 + SQLEAN_VERSION := '"$(or $(shell git tag --points-at HEAD),main)"' + + CC ?= gcc ++CC_WIN_ARM64 ?= clang --target=aarch64-w64-mingw32 + CFLAGS ?= + COMMON_CFLAGS := -Isrc -DSQLEAN_VERSION=$(SQLEAN_VERSION) + + LINIX_FLAGS := $(CFLAGS) -z now -z relro -Wall -Wsign-compare -Wno-unknown-pragmas -fPIC -shared $(COMMON_CFLAGS) + WINDO_FLAGS := $(CFLAGS) -shared $(COMMON_CFLAGS) ++WINDO_ARM64_FLAGS := $(CFLAGS) -shared $(COMMON_CFLAGS) + MACOS_FLAGS := $(CFLAGS) -Wall -Wsign-compare -fPIC -dynamiclib $(COMMON_CFLAGS) + CTEST_FLAGS := $(CFLAGS) -Wall -Wsign-compare -Wno-unknown-pragmas -Isrc + +@@ -118,8 +120,26 @@ compile-windows: + gcc -O3 $(WINDO_FLAGS) src/sqlite3-vsv.c src/vsv/*.c -o dist/vsv.dll -lm + gcc -O3 $(WINDO_FLAGS) -include src/regexp/constants.h src/sqlite3-sqlean.c src/crypto/*.c src/define/*.c src/fileio/*.c src/fuzzy/*.c src/math/*.c src/regexp/*.c src/regexp/pcre2/*.c src/stats/*.c src/text/*.c src/text/*/*.c src/time/*.c src/unicode/*.c src/uuid/*.c src/vsv/*.c -o dist/sqlean.dll -lm + ++compile-windows-arm64: ++ mkdir -p dist/arm64 ++ $(CC_WIN_ARM64) -O3 $(WINDO_ARM64_FLAGS) src/sqlite3-crypto.c src/crypto/*.c -o dist/arm64/crypto.dll ++ $(CC_WIN_ARM64) -O3 $(WINDO_ARM64_FLAGS) src/sqlite3-define.c src/define/*.c -o dist/arm64/define.dll ++ $(CC_WIN_ARM64) -O3 $(WINDO_ARM64_FLAGS) src/sqlite3-fileio.c src/fileio/*.c -o dist/arm64/fileio.dll ++ $(CC_WIN_ARM64) -O3 $(WINDO_ARM64_FLAGS) src/sqlite3-fuzzy.c src/fuzzy/*.c -o dist/arm64/fuzzy.dll ++ $(CC_WIN_ARM64) -O3 $(WINDO_ARM64_FLAGS) src/sqlite3-ipaddr.c src/ipaddr/*.c -o dist/arm64/ipaddr.dll -lws2_32 ++ $(CC_WIN_ARM64) -O3 $(WINDO_ARM64_FLAGS) src/sqlite3-math.c src/math/*.c -o dist/arm64/math.dll -lm ++ $(CC_WIN_ARM64) -O3 $(WINDO_ARM64_FLAGS) src/sqlite3-regexp.c -include src/regexp/constants.h src/regexp/*.c src/regexp/pcre2/*.c -o dist/arm64/regexp.dll ++ $(CC_WIN_ARM64) -O3 $(WINDO_ARM64_FLAGS) src/sqlite3-stats.c src/stats/*.c -o dist/arm64/stats.dll -lm ++ $(CC_WIN_ARM64) -O3 $(WINDO_ARM64_FLAGS) src/sqlite3-text.c src/text/*.c src/text/*/*.c -o dist/arm64/text.dll ++ $(CC_WIN_ARM64) -O3 $(WINDO_ARM64_FLAGS) src/sqlite3-time.c src/time/*.c -o dist/arm64/time.dll ++ $(CC_WIN_ARM64) -O3 $(WINDO_ARM64_FLAGS) src/sqlite3-unicode.c src/unicode/*.c -o dist/arm64/unicode.dll ++ $(CC_WIN_ARM64) -O3 $(WINDO_ARM64_FLAGS) src/sqlite3-uuid.c src/uuid/*.c -o dist/arm64/uuid.dll ++ $(CC_WIN_ARM64) -O3 $(WINDO_ARM64_FLAGS) src/sqlite3-vsv.c src/vsv/*.c -o dist/arm64/vsv.dll -lm ++ $(CC_WIN_ARM64) -O3 $(WINDO_ARM64_FLAGS) -include src/regexp/constants.h src/sqlite3-sqlean.c src/crypto/*.c src/define/*.c src/fileio/*.c src/fuzzy/*.c src/math/*.c src/regexp/*.c src/regexp/pcre2/*.c src/stats/*.c src/text/*.c src/text/*/*.c src/time/*.c src/unicode/*.c src/uuid/*.c src/vsv/*.c -o dist/arm64/sqlean.dll -lm ++ + pack-windows: + 7z a -tzip dist/sqlean-win-x64.zip ./dist/*.dll ++ 7z a -tzip dist/sqlean-win-arm64.zip ./dist/arm64/*.dll + + compile-macos: + $(CC) -O3 $(MACOS_FLAGS) src/sqlite3-crypto.c src/crypto/*.c -o dist/crypto.dylib diff --git a/tools/windows/download_latest_openssl3.py b/tools/windows/download_latest_openssl3.py new file mode 100644 index 0000000000..74d55cf393 --- /dev/null +++ b/tools/windows/download_latest_openssl3.py @@ -0,0 +1,83 @@ +# SPDX-FileCopyrightText: (C) 2026 SeongTae Jeong +# SPDX-License-Identifier: MIT + +import requests +import hashlib +from packaging.version import Version +from pathlib import Path + +JSON_URL = "https://raw.githubusercontent.com/slproweb/opensslhashes/refs/heads/master/win32_openssl_hashes.json" +TARGET_MAJOR_PREFIX = "3." + + +def get_latest_universal_installer_info(): + response = requests.get(JSON_URL, timeout=30) + response.raise_for_status() + + data = response.json() + files = data.get("files", {}) + + matched = [] + for filename, meta in files.items(): + basever = meta.get("basever", "") + if ( + meta.get("arch") == "Universal" + and meta.get("light") is False + and meta.get("installer") == "exe" + and basever.startswith(TARGET_MAJOR_PREFIX) + ): + matched.append( + { + "filename": filename, + "basever": basever, + "url": meta["url"], + "sha256": meta["sha256"], + } + ) + + if not matched: + raise ValueError( + f"No files matching your criteria were found for {TARGET_MAJOR_PREFIX}x." + ) + + latest = max(matched, key=lambda x: Version(x["basever"])) + return latest + + +def download_file(url, destination): + destination.parent.mkdir(parents=True, exist_ok=True) + + with requests.get(url, stream=True, timeout=30) as response: + response.raise_for_status() + + with destination.open("wb") as file: + for chunk in response.iter_content(chunk_size=8192): + if chunk: + file.write(chunk) + + return destination + + +def verify_sha256(file_path, expected_sha256): + hasher = hashlib.sha256() + + with file_path.open("rb") as file: + for chunk in iter(lambda: file.read(8192), b""): + hasher.update(chunk) + + actual_sha256 = hasher.hexdigest() + if actual_sha256.lower() != expected_sha256.lower(): + raise ValueError( + f"SHA256 mismatch for {file_path}: expected {expected_sha256}, got {actual_sha256}" + ) + + return actual_sha256 + + +if __name__ == "__main__": + latest = get_latest_universal_installer_info() + download_path = Path(latest["filename"]) + saved_path = download_file(latest["url"], download_path) + sha256 = verify_sha256(saved_path, latest["sha256"]) + + print(latest["filename"]) diff --git a/tools/windows/download_latest_sqlite.py b/tools/windows/download_latest_sqlite.py new file mode 100644 index 0000000000..4b37c955d1 --- /dev/null +++ b/tools/windows/download_latest_sqlite.py @@ -0,0 +1,80 @@ +# SPDX-FileCopyrightText: (C) 2026 SeongTae Jeong +# SPDX-License-Identifier: MIT + +import hashlib +import re +from pathlib import Path +from urllib.request import urlopen + +DOWNLOAD_PAGE_URL = "https://sqlite.org/download.html" +DOWNLOAD_BASE_URL = "https://sqlite.org/" +PRODUCT_REGEX = re.compile( + r"PRODUCT,(\d+\.\d+\.\d+),(\d+/sqlite-amalgamation-\d+\.zip),\d+,([0-9a-fA-F]+)" +) + + +def fetch_download_page(): + with urlopen(DOWNLOAD_PAGE_URL, timeout=30) as response: + return response.read().decode("utf-8", errors="replace") + + +def version_key(version): + return tuple(int(part) for part in version.split(".")) + + +def get_latest_amalgamation_info(download_page): + matches = [] + for match in PRODUCT_REGEX.finditer(download_page): + version, relative_url, sha3_256 = match.groups() + matches.append( + { + "version": version, + "relative_url": relative_url, + "url": f"{DOWNLOAD_BASE_URL}{relative_url}", + "filename": Path(relative_url).name, + "sha3_256": sha3_256, + } + ) + + if not matches: + raise ValueError("No SQLite amalgamation download entries were found.") + + return max(matches, key=lambda entry: version_key(entry["version"])) + + +def download_file(url, destination): + destination.parent.mkdir(parents=True, exist_ok=True) + + with urlopen(url, timeout=30) as response, destination.open("wb") as file: + while True: + chunk = response.read(8192) + if not chunk: + break + file.write(chunk) + + return destination + + +def verify_sha3_256(file_path, expected_sha3_256): + hasher = hashlib.sha3_256() + + with file_path.open("rb") as file: + for chunk in iter(lambda: file.read(8192), b""): + hasher.update(chunk) + + actual_sha3_256 = hasher.hexdigest() + if actual_sha3_256.lower() != expected_sha3_256.lower(): + raise ValueError( + f"SHA3-256 mismatch for {file_path}: expected {expected_sha3_256}, got {actual_sha3_256}" + ) + + return actual_sha3_256 + + +if __name__ == "__main__": + latest = get_latest_amalgamation_info(fetch_download_page()) + download_path = Path(latest["filename"]) + saved_path = download_file(latest["url"], download_path) + verify_sha3_256(saved_path, latest["sha3_256"]) + + print(saved_path.name) diff --git a/tools/windows/requirements/download_latest_openssl3.txt b/tools/windows/requirements/download_latest_openssl3.txt new file mode 100644 index 0000000000..1a41ddc4a1 --- /dev/null +++ b/tools/windows/requirements/download_latest_openssl3.txt @@ -0,0 +1,2 @@ +packaging==26.0 +requests==2.32.5