Merge branch 'main' into auth-stream-otpauth
This commit is contained in:
commit
7ced2f5139
180 changed files with 4961 additions and 4962 deletions
6
.github/workflows/auth-release.yml
vendored
6
.github/workflows/auth-release.yml
vendored
|
@ -17,8 +17,8 @@ name: "Release (auth)"
|
|||
# We use a suffix like `-test` to indicate that these are test tags, and that
|
||||
# they belong to a pre-release.
|
||||
#
|
||||
# If you need to do multiple tests, add a +x at the end of the tag. e.g.
|
||||
# `auth-v1.2.3-test+1`.
|
||||
# If you need to do multiple tests, add a .x at the end of the tag. e.g.
|
||||
# `auth-v1.2.3-test.1`.
|
||||
#
|
||||
# Once the testing is done, also delete the tag(s) please.
|
||||
|
||||
|
@ -85,7 +85,7 @@ jobs:
|
|||
- name: Install dependencies for desktop build
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y libsecret-1-dev libsodium-dev libwebkit2gtk-4.0-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm libsqlite3-dev locate appindicator3-0.1 libappindicator3-dev libffi-dev libtiff5
|
||||
sudo apt-get install -y libsecret-1-dev libsodium-dev libwebkit2gtk-4.0-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm patchelf libsqlite3-dev locate appindicator3-0.1 libappindicator3-dev libffi-dev libtiff5
|
||||
sudo updatedb --localpaths='/usr/lib/x86_64-linux-gnu'
|
||||
|
||||
- name: Install appimagetool
|
||||
|
|
30
.github/workflows/desktop-lint.yml
vendored
Normal file
30
.github/workflows/desktop-lint.yml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
name: "Lint (desktop)"
|
||||
|
||||
on:
|
||||
# Run on every push to a branch other than main that changes desktop/
|
||||
push:
|
||||
branches-ignore: [main, "deploy/**"]
|
||||
paths:
|
||||
- "desktop/**"
|
||||
- ".github/workflows/desktop-lint.yml"
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: desktop
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup node and enable yarn caching
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "yarn"
|
||||
cache-dependency-path: "desktop/yarn.lock"
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- run: yarn lint
|
|
@ -54,3 +54,4 @@ jobs:
|
|||
packageName: io.ente.photos
|
||||
releaseFiles: mobile/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab
|
||||
track: internal
|
||||
changesNotSentForReview: true
|
||||
|
|
|
@ -11,7 +11,7 @@ display_name: Auth
|
|||
|
||||
requires:
|
||||
- libsqlite3x
|
||||
- webkit2gtk-4.0
|
||||
- webkit2gtk4.0
|
||||
- libsodium
|
||||
- libsecret
|
||||
- libappindicator
|
||||
|
|
|
@ -293,9 +293,9 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
path: "packages/desktop_webview_window"
|
||||
ref: HEAD
|
||||
resolved-ref: "8cbbf9cd6efcfee5e0f420a36f7f8e7e64b667a1"
|
||||
url: "https://github.com/MixinNetwork/flutter-plugins"
|
||||
ref: fix-webkit-version
|
||||
resolved-ref: fe2223e4edfecdbb3a97bb9e3ced73db4ae9d979
|
||||
url: "https://github.com/ente-io/flutter-desktopwebview-fork"
|
||||
source: git
|
||||
version: "0.2.4"
|
||||
device_info_plus:
|
||||
|
|
|
@ -20,7 +20,8 @@ dependencies:
|
|||
convert: ^3.1.1
|
||||
desktop_webview_window:
|
||||
git:
|
||||
url: https://github.com/MixinNetwork/flutter-plugins
|
||||
url: https://github.com/ente-io/flutter-desktopwebview-fork
|
||||
ref: fix-webkit-version
|
||||
path: packages/desktop_webview_window
|
||||
device_info_plus: ^9.1.1
|
||||
dio: ^5.4.0
|
||||
|
|
|
@ -36,7 +36,8 @@ ente --help
|
|||
|
||||
### Accounts
|
||||
|
||||
If you wish, you can add multiple accounts (your own and that of your family members) and export all data using this tool.
|
||||
If you wish, you can add multiple accounts (your own and that of your family
|
||||
members) and export all data using this tool.
|
||||
|
||||
#### Add an account
|
||||
|
||||
|
@ -44,6 +45,12 @@ If you wish, you can add multiple accounts (your own and that of your family mem
|
|||
ente account add
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> `ente account add` does not create new accounts, it just adds pre-existing
|
||||
> accounts to the list of accounts that the CLI knows about so that you can use
|
||||
> them for other actions.
|
||||
|
||||
#### List accounts
|
||||
|
||||
```shell
|
||||
|
|
|
@ -1,26 +1,36 @@
|
|||
/* eslint-env node */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
/* What we really want eventually */
|
||||
// "plugin:@typescript-eslint/strict-type-checked",
|
||||
// "plugin:@typescript-eslint/stylistic-type-checked",
|
||||
"plugin:@typescript-eslint/strict-type-checked",
|
||||
"plugin:@typescript-eslint/stylistic-type-checked",
|
||||
],
|
||||
/* Temporarily add a global
|
||||
Enhancement: Remove me */
|
||||
globals: {
|
||||
NodeJS: "readonly",
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
root: true,
|
||||
ignorePatterns: [".eslintrc.js", "app", "out", "dist"],
|
||||
env: {
|
||||
es2022: true,
|
||||
node: true,
|
||||
},
|
||||
rules: {
|
||||
/* Allow numbers to be used in template literals */
|
||||
"@typescript-eslint/restrict-template-expressions": [
|
||||
"error",
|
||||
{
|
||||
allowNumber: true,
|
||||
},
|
||||
],
|
||||
/* Allow void expressions as the entire body of an arrow function */
|
||||
"@typescript-eslint/no-confusing-void-expression": [
|
||||
"error",
|
||||
{
|
||||
ignoreArrowShorthand: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
55
desktop/.github/workflows/build.yml
vendored
55
desktop/.github/workflows/build.yml
vendored
|
@ -1,55 +0,0 @@
|
|||
name: Build/release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Node.js, NPM and Yarn
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Prepare for app notarization
|
||||
if: startsWith(matrix.os, 'macos')
|
||||
# Import Apple API key for app notarization on macOS
|
||||
run: |
|
||||
mkdir -p ~/private_keys/
|
||||
echo '${{ secrets.api_key }}' > ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
|
||||
|
||||
- name: Install libarchive-tools for pacman build # Related https://github.com/electron-userland/electron-builder/issues/4181
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
run: sudo apt-get install libarchive-tools
|
||||
|
||||
- name: Ente Electron Builder Action
|
||||
uses: ente-io/action-electron-builder@v1.0.0
|
||||
with:
|
||||
# GitHub token, automatically provided to the action
|
||||
# (No need to define this secret in the repo settings)
|
||||
github_token: ${{ secrets.github_token }}
|
||||
|
||||
# If the commit is tagged with a version (e.g. "v1.0.0"),
|
||||
# release the app after building
|
||||
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
|
||||
mac_certs: ${{ secrets.mac_certs }}
|
||||
mac_certs_password: ${{ secrets.mac_certs_password }}
|
||||
env:
|
||||
# macOS notarization API key
|
||||
API_KEY_ID: ${{ secrets.api_key_id }}
|
||||
API_KEY_ISSUER_ID: ${{ secrets.api_key_issuer_id}}
|
||||
USE_HARD_LINKS: false
|
90
desktop/.github/workflows/desktop-release.yml
vendored
Normal file
90
desktop/.github/workflows/desktop-release.yml
vendored
Normal file
|
@ -0,0 +1,90 @@
|
|||
name: "Release"
|
||||
|
||||
# This will create a new draft release with public artifacts.
|
||||
#
|
||||
# Note that a release will only get created if there is an associated tag
|
||||
# (GitHub releases need a corresponding tag).
|
||||
#
|
||||
# The canonical source for this action is in the repository where we keep the
|
||||
# source code for the Ente Photos desktop app: https://github.com/ente-io/ente
|
||||
#
|
||||
# However, it actually lives and runs in the repository that we use for making
|
||||
# releases: https://github.com/ente-io/photos-desktop
|
||||
#
|
||||
# We need two repositories because Electron updater currently doesn't work well
|
||||
# with monorepos. For more details, see `docs/release.md`.
|
||||
|
||||
on:
|
||||
push:
|
||||
# Run when a tag matching the pattern "v*"" is pushed.
|
||||
#
|
||||
# See: [Note: Testing release workflows that are triggered by tags].
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: desktop
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest]
|
||||
# Commented for testing
|
||||
# os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Checkout the tag photosd-v1.x.x from the source code
|
||||
# repository when we're invoked for tag v1.x.x on the releases
|
||||
# repository.
|
||||
repository: ente-io/ente
|
||||
ref: photosd-${{ github.ref_name }}
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Prepare for app notarization
|
||||
if: startsWith(matrix.os, 'macos')
|
||||
# Import Apple API key for app notarization on macOS
|
||||
run: |
|
||||
mkdir -p ~/private_keys/
|
||||
echo '${{ secrets.API_KEY }}' > ~/private_keys/AuthKey_${{ secrets.API_KEY_ID }}.p8
|
||||
|
||||
- name: Install libarchive-tools for pacman build
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
# See:
|
||||
# https://github.com/electron-userland/electron-builder/issues/4181
|
||||
run: sudo apt-get install libarchive-tools
|
||||
|
||||
- name: Build
|
||||
uses: ente-io/action-electron-builder@v1.0.0
|
||||
with:
|
||||
package_root: desktop
|
||||
|
||||
# GitHub token, automatically provided to the action
|
||||
# (No need to define this secret in the repo settings)
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# If the commit is tagged with a version (e.g. "v1.0.0"),
|
||||
# release the app after building.
|
||||
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
|
||||
mac_certs: ${{ secrets.MAC_CERTS }}
|
||||
mac_certs_password: ${{ secrets.MAC_CERTS_PASSWORD }}
|
||||
env:
|
||||
# macOS notarization API key details
|
||||
API_KEY_ID: ${{ secrets.API_KEY_ID }}
|
||||
API_KEY_ISSUER_ID: ${{ secrets.API_KEY_ISSUER_ID }}
|
||||
USE_HARD_LINKS: false
|
|
@ -1,5 +1,13 @@
|
|||
# CHANGELOG
|
||||
|
||||
## v1.7.0 (Unreleased)
|
||||
|
||||
v1.7 is a major rewrite to improve the security of our app. We have enabled
|
||||
sandboxing and disabled node integration for the renderer process. All this
|
||||
required restructuring our IPC mechanisms, which resulted in a lot of under the
|
||||
hood changes. The outcome is a more secure app that also uses the latest and
|
||||
greatest Electron recommendations.
|
||||
|
||||
## v1.6.63
|
||||
|
||||
### New
|
||||
|
|
|
@ -10,12 +10,6 @@ To know more about Ente, see [our main README](../README.md) or visit
|
|||
|
||||
## Building from source
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> We're improving the security of the desktop app further by migrating to
|
||||
> Electron's sandboxing and contextIsolation. These updates are still WIP and
|
||||
> meanwhile the instructions below might not fully work on the main branch.
|
||||
|
||||
Fetch submodules
|
||||
|
||||
```sh
|
||||
|
|
|
@ -13,7 +13,7 @@ Electron embeds Chromium and Node.js in the generated app's binary. The
|
|||
generated app thus consists of two separate processes - the _main_ process, and
|
||||
a _renderer_ process.
|
||||
|
||||
- The _main_ process is runs the embedded node. This process can deal with the
|
||||
- The _main_ process runs the embedded node. This process can deal with the
|
||||
host OS - it is conceptually like a `node` repl running on your machine. In
|
||||
our case, the TypeScript code (in the `src/` directory) gets transpiled by
|
||||
`tsc` into JavaScript in the `build/app/` directory, which gets bundled in
|
||||
|
@ -90,6 +90,9 @@ Some extra ones specific to the code here are:
|
|||
Unix commands in our `package.json` scripts. This allows us to use the same
|
||||
commands (like `ln`) across different platforms like Linux and Windows.
|
||||
|
||||
- [@tsconfig/recommended](https://github.com/tsconfig/bases) gives us a base
|
||||
tsconfig for the Node.js version that our current Electron version uses.
|
||||
|
||||
## Functionality
|
||||
|
||||
### Format conversion
|
||||
|
|
|
@ -1,43 +1,47 @@
|
|||
## Releases
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> TODO(MR): This document needs to be audited and changed as we do the first
|
||||
> release from this new monorepo.
|
||||
Conceptually, the release is straightforward: We push a tag, a GitHub workflow
|
||||
gets triggered that creates a draft release with artifacts built from that tag.
|
||||
We then publish that release. The download links on our website, and existing
|
||||
apps already know how to check for the latest GitHub release and update
|
||||
accordingly.
|
||||
|
||||
The Github Action that builds the desktop binaries is triggered by pushing a tag
|
||||
matching the pattern `photos-desktop-v1.2.3`. This value should match the
|
||||
version in `package.json`.
|
||||
The complication comes by the fact that Electron Updater (the mechanism that we
|
||||
use for auto updates) doesn't work well with monorepos. So we need to keep a
|
||||
separate (non-mono) repository just for doing releases.
|
||||
|
||||
So the process for doing a release would be.
|
||||
- Source code lives here, in [ente-io/ente](https://github.com/ente-io/ente).
|
||||
|
||||
1. Create a new branch (can be named anything). On this branch, include your
|
||||
changes.
|
||||
- Releases are done from
|
||||
[ente-io/photos-desktop](https://github.com/ente-io/photos-desktop).
|
||||
|
||||
2. Mention the changes in `CHANGELOG.md`.
|
||||
## Workflow
|
||||
|
||||
3. Changing the `version` in `package.json` to `1.x.x`.
|
||||
The workflow is:
|
||||
|
||||
4. Commit and push to remote
|
||||
1. Finalize the changes in the source repo.
|
||||
|
||||
- Update the CHANGELOG.
|
||||
- Update the version in `package.json`
|
||||
- `git commit -m 'Release v1.x.x'`
|
||||
- Open PR, merge into main.
|
||||
|
||||
2. Tag this commit with a tag matching the pattern `photosd-v1.2.3`, where
|
||||
`1.2.3` is the version in `package.json`
|
||||
|
||||
```sh
|
||||
git add package.json && git commit -m 'Release v1.x.x'
|
||||
git tag v1.x.x
|
||||
git push && git push --tags
|
||||
git tag photosd-v1.x.x
|
||||
git push origin photosd-v1.x.x
|
||||
```
|
||||
|
||||
This by itself will already trigger a new release. The GitHub action will create
|
||||
a new draft release that can then be used as descibed below.
|
||||
3. Head over to the releases repository and run the trigger script, passing it
|
||||
the tag _without_ the `photosd-` prefix.
|
||||
|
||||
To wrap up, we also need to merge back these changes into main. So for that,
|
||||
```sh
|
||||
./.github/trigger-release.sh v1.x.x
|
||||
```
|
||||
|
||||
5. Open a PR for the branch that we're working on (where the above tag was
|
||||
pushed from) to get it merged into main.
|
||||
|
||||
6. In this PR, also increase the version number for the next release train. That
|
||||
is, supposed we just released `v4.0.1`. Then we'll change the version number
|
||||
in main to `v4.0.2-next.0`. Each pre-release will modify the `next.0` part.
|
||||
Finally, at the time of the next release, this'll become `v4.0.2`.
|
||||
## Post build
|
||||
|
||||
The GitHub Action runs on Windows, Linux and macOS. It produces the artifacts
|
||||
defined in the `build` value in `package.json`.
|
||||
|
@ -46,29 +50,11 @@ defined in the `build` value in `package.json`.
|
|||
- Linux - An AppImage, and 3 other packages (`.rpm`, `.deb`, `.pacman`)
|
||||
- macOS - A universal DMG
|
||||
|
||||
Additionally, the GitHub action notarizes the macOS DMG. For this it needs
|
||||
credentials provided via GitHub secrets.
|
||||
Additionally, the GitHub action notarizes and signs the macOS DMG (For this it
|
||||
uses credentials provided via GitHub secrets).
|
||||
|
||||
During the build the Sentry webpack plugin checks to see if SENTRY_AUTH_TOKEN is
|
||||
defined. If so, it uploads the sourcemaps for the renderer process to Sentry
|
||||
(For our GitHub action, the SENTRY_AUTH_TOKEN is defined as a GitHub secret).
|
||||
|
||||
The sourcemaps for the main (node) process are currently not sent to Sentry
|
||||
(this works fine in practice since the node process files are not minified, we
|
||||
only run `tsc`).
|
||||
|
||||
Once the build is done, a draft release with all these artifacts attached is
|
||||
created. The build is idempotent, so if something goes wrong and we need to
|
||||
re-run the GitHub action, just delete the draft release (if it got created) and
|
||||
start a new run by pushing a new tag (if some code changes are required).
|
||||
|
||||
If no code changes are required, say the build failed for some transient network
|
||||
or sentry issue, we can even be re-run by the build by going to Github Action
|
||||
age and rerun from there. This will re-trigger for the same tag.
|
||||
|
||||
If everything goes well, we'll have a release on GitHub, and the corresponding
|
||||
source maps for the renderer process uploaded to Sentry. There isn't anything
|
||||
else to do:
|
||||
To rollout the build, we need to publish the draft release. Thereafter,
|
||||
everything is automated:
|
||||
|
||||
- The website automatically redirects to the latest release on GitHub when
|
||||
people try to download.
|
||||
|
@ -76,7 +62,7 @@ else to do:
|
|||
- The file formats with support auto update (Windows `exe`, the Linux AppImage
|
||||
and the macOS DMG) also check the latest GitHub release automatically to
|
||||
download and apply the update (the rest of the formats don't support auto
|
||||
updates).
|
||||
updates yet).
|
||||
|
||||
- We're not putting the desktop app in other stores currently. It is available
|
||||
as a `brew cask`, but we only had to open a PR to add the initial formula,
|
||||
|
@ -87,6 +73,4 @@ else to do:
|
|||
We can also publish the draft releases by checking the "pre-release" option.
|
||||
Such releases don't cause any of the channels (our website, or the desktop app
|
||||
auto updater, or brew) to be notified, instead these are useful for giving links
|
||||
to pre-release builds to customers. Generally, in the version number for these
|
||||
we'll add a label to the version, e.g. the "beta.x" in `1.x.x-beta.x`. This
|
||||
should be done both in `package.json`, and what we tag the commit with.
|
||||
to pre-release builds to customers.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ente",
|
||||
"version": "1.6.63",
|
||||
"version": "1.7.0-beta.0",
|
||||
"private": true,
|
||||
"description": "Desktop client for Ente Photos",
|
||||
"author": "Ente <code@ente.io>",
|
||||
|
@ -15,8 +15,11 @@
|
|||
"dev-main": "tsc && electron app/main.js",
|
||||
"dev-renderer": "cd ../web && yarn install && yarn dev:photos",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"lint": "yarn prettier --check . && eslint --ext .ts src",
|
||||
"lint-fix": "yarn prettier --write . && eslint --fix --ext .ts src"
|
||||
"lint": "yarn prettier --check --log-level warn . && eslint --ext .ts src && yarn tsc",
|
||||
"lint-fix": "yarn prettier --write --log-level warn . && eslint --fix --ext .ts src && yarn tsc"
|
||||
},
|
||||
"resolutions": {
|
||||
"jackspeak": "2.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"any-shell-escape": "^0.1",
|
||||
|
@ -34,12 +37,13 @@
|
|||
"onnxruntime-node": "^1.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/auto-launch": "^5.0",
|
||||
"@types/ffmpeg-static": "^3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7",
|
||||
"@typescript-eslint/parser": "^7",
|
||||
"concurrently": "^8",
|
||||
"electron": "^29",
|
||||
"electron": "^30",
|
||||
"electron-builder": "^24",
|
||||
"electron-builder-notarize": "^1.5",
|
||||
"eslint": "^8",
|
||||
|
|
|
@ -8,18 +8,15 @@
|
|||
*
|
||||
* https://www.electronjs.org/docs/latest/tutorial/process-model#the-main-process
|
||||
*/
|
||||
import { nativeImage } from "electron";
|
||||
import { app, BrowserWindow, Menu, protocol, Tray } from "electron/main";
|
||||
|
||||
import { nativeImage, shell } from "electron/common";
|
||||
import type { WebContents } from "electron/main";
|
||||
import { BrowserWindow, Menu, Tray, app, protocol } from "electron/main";
|
||||
import serveNextAt from "next-electron-server";
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
addAllowOriginHeader,
|
||||
handleDownloads,
|
||||
handleExternalLinks,
|
||||
} from "./main/init";
|
||||
import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc";
|
||||
import log, { initLogging } from "./main/log";
|
||||
import { createApplicationMenu, createTrayContextMenu } from "./main/menu";
|
||||
|
@ -29,12 +26,12 @@ import { createWatcher } from "./main/services/watch";
|
|||
import { userPreferences } from "./main/stores/user-preferences";
|
||||
import { migrateLegacyWatchStoreIfNeeded } from "./main/stores/watch";
|
||||
import { registerStreamProtocol } from "./main/stream";
|
||||
import { isDev } from "./main/utils-electron";
|
||||
import { isDev } from "./main/utils/electron";
|
||||
|
||||
/**
|
||||
* The URL where the renderer HTML is being served from.
|
||||
*/
|
||||
export const rendererURL = "ente://app";
|
||||
const rendererURL = "ente://app";
|
||||
|
||||
/**
|
||||
* We want to hide our window instead of closing it when the user presses the
|
||||
|
@ -130,50 +127,18 @@ const registerPrivilegedSchemes = () => {
|
|||
{
|
||||
scheme: "stream",
|
||||
privileges: {
|
||||
// TODO(MR): Remove the commented bits if we don't end up
|
||||
// needing them by the time the IPC refactoring is done.
|
||||
|
||||
// Prevent the insecure origin issues when fetching this
|
||||
// secure: true,
|
||||
// Allow the web fetch API in the renderer to use this scheme.
|
||||
supportFetchAPI: true,
|
||||
// Allow it to be used with video tags.
|
||||
// stream: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* [Note: Increased disk cache for the desktop app]
|
||||
*
|
||||
* Set the "disk-cache-size" command line flag to ask the Chromium process to
|
||||
* use a larger size for the caches that it keeps on disk. This allows us to use
|
||||
* the web based caching mechanisms on both the web and the desktop app, just
|
||||
* ask the embedded Chromium to be a bit more generous in disk usage when
|
||||
* running as the desktop app.
|
||||
*
|
||||
* The size we provide is in bytes.
|
||||
* https://www.electronjs.org/docs/latest/api/command-line-switches#--disk-cache-sizesize
|
||||
*
|
||||
* Note that increasing the disk cache size does not guarantee that Chromium
|
||||
* will respect in verbatim, it uses its own heuristics atop this hint.
|
||||
* https://superuser.com/questions/378991/what-is-chrome-default-cache-size-limit/1577693#1577693
|
||||
*
|
||||
* See also: [Note: Caching files].
|
||||
*/
|
||||
const increaseDiskCache = () =>
|
||||
app.commandLine.appendSwitch(
|
||||
"disk-cache-size",
|
||||
`${5 * 1024 * 1024 * 1024}`, // 5 GB
|
||||
);
|
||||
|
||||
/**
|
||||
* Create an return the {@link BrowserWindow} that will form our app's UI.
|
||||
*
|
||||
* This window will show the HTML served from {@link rendererURL}.
|
||||
*/
|
||||
const createMainWindow = async () => {
|
||||
const createMainWindow = () => {
|
||||
// Create the main window. This'll show our web content.
|
||||
const window = new BrowserWindow({
|
||||
webPreferences: {
|
||||
|
@ -187,7 +152,7 @@ const createMainWindow = async () => {
|
|||
show: false,
|
||||
});
|
||||
|
||||
const wasAutoLaunched = await autoLauncher.wasAutoLaunched();
|
||||
const wasAutoLaunched = autoLauncher.wasAutoLaunched();
|
||||
if (wasAutoLaunched) {
|
||||
// Don't automatically show the app's window if we were auto-launched.
|
||||
// On macOS, also hide the dock icon on macOS.
|
||||
|
@ -201,7 +166,7 @@ const createMainWindow = async () => {
|
|||
if (isDev) window.webContents.openDevTools();
|
||||
|
||||
window.webContents.on("render-process-gone", (_, details) => {
|
||||
log.error(`render-process-gone: ${details}`);
|
||||
log.error(`render-process-gone: ${details.reason}`);
|
||||
window.webContents.reload();
|
||||
});
|
||||
|
||||
|
@ -209,7 +174,7 @@ const createMainWindow = async () => {
|
|||
// webContents is not responding to input messages for > 30 seconds."
|
||||
window.webContents.on("unresponsive", () => {
|
||||
log.error(
|
||||
"Main window's webContents are unresponsive, will restart the renderer process",
|
||||
"MainWindow's webContents are unresponsive, will restart the renderer process",
|
||||
);
|
||||
window.webContents.forcefullyCrashRenderer();
|
||||
});
|
||||
|
@ -230,7 +195,7 @@ const createMainWindow = async () => {
|
|||
});
|
||||
|
||||
window.on("show", () => {
|
||||
if (process.platform == "darwin") app.dock.show();
|
||||
if (process.platform == "darwin") void app.dock.show();
|
||||
});
|
||||
|
||||
// Let ipcRenderer know when mainWindow is in the foreground so that it can
|
||||
|
@ -240,6 +205,58 @@ const createMainWindow = async () => {
|
|||
return window;
|
||||
};
|
||||
|
||||
/**
|
||||
* Automatically set the save path for user initiated downloads to the system's
|
||||
* "downloads" directory instead of asking the user to select a save location.
|
||||
*/
|
||||
export const setDownloadPath = (webContents: WebContents) => {
|
||||
webContents.session.on("will-download", (_, item) => {
|
||||
item.setSavePath(
|
||||
uniqueSavePath(app.getPath("downloads"), item.getFilename()),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const uniqueSavePath = (dirPath: string, fileName: string) => {
|
||||
const { name, ext } = path.parse(fileName);
|
||||
|
||||
let savePath = path.join(dirPath, fileName);
|
||||
let n = 1;
|
||||
while (existsSync(savePath)) {
|
||||
const suffixedName = [`${name}(${n})`, ext].filter((x) => x).join(".");
|
||||
savePath = path.join(dirPath, suffixedName);
|
||||
n++;
|
||||
}
|
||||
return savePath;
|
||||
};
|
||||
|
||||
/**
|
||||
* Allow opening external links, e.g. when the user clicks on the "Feature
|
||||
* requests" button in the sidebar (to open our GitHub repository), or when they
|
||||
* click the "Support" button to send an email to support.
|
||||
*
|
||||
* @param webContents The renderer to configure.
|
||||
*/
|
||||
export const allowExternalLinks = (webContents: WebContents) => {
|
||||
// By default, if the user were open a link, say
|
||||
// https://github.com/ente-io/ente/discussions, then it would open a _new_
|
||||
// BrowserWindow within our app.
|
||||
//
|
||||
// This is not the behaviour we want; what we want is to ask the system to
|
||||
// handle the link (e.g. open the URL in the default browser, or if it is a
|
||||
// mailto: link, then open the user's mail client).
|
||||
//
|
||||
// Returning `action` "deny" accomplishes this.
|
||||
webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (!url.startsWith(rendererURL)) {
|
||||
void shell.openExternal(url);
|
||||
return { action: "deny" };
|
||||
} else {
|
||||
return { action: "allow" };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an icon for our app in the system tray.
|
||||
*
|
||||
|
@ -272,24 +289,24 @@ const setupTrayItem = (mainWindow: BrowserWindow) => {
|
|||
* Older versions of our app used to maintain a cache dir using the main
|
||||
* process. This has been deprecated in favor of using a normal web cache.
|
||||
*
|
||||
* See [Note: Increased disk cache for the desktop app]
|
||||
*
|
||||
* Delete the old cache dir if it exists. This code was added March 2024, and
|
||||
* can be removed after some time once most people have upgraded to newer
|
||||
* versions.
|
||||
*/
|
||||
const deleteLegacyDiskCacheDirIfExists = async () => {
|
||||
// The existing code was passing "cache" as a parameter to getPath. This is
|
||||
// incorrect if we go by the types - "cache" is not a valid value for the
|
||||
// parameter to `app.getPath`.
|
||||
// The existing code was passing "cache" as a parameter to getPath.
|
||||
//
|
||||
// It might be an issue in the types, since at runtime it seems to work. For
|
||||
// example, on macOS I get `~/Library/Caches`.
|
||||
// However, "cache" is not a valid parameter to getPath. It works! (for
|
||||
// example, on macOS I get `~/Library/Caches`), but it is intentionally not
|
||||
// documented as part of the public API:
|
||||
//
|
||||
// - docs: remove "cache" from app.getPath
|
||||
// https://github.com/electron/electron/pull/33509
|
||||
//
|
||||
// Irrespective, we replicate the original behaviour so that we get back the
|
||||
// same path that the old got was getting.
|
||||
// same path that the old code was getting.
|
||||
//
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error "cache" works but is not part of the public API.
|
||||
const cacheDir = path.join(app.getPath("cache"), "ente");
|
||||
if (existsSync(cacheDir)) {
|
||||
log.info(`Removing legacy disk cache from ${cacheDir}`);
|
||||
|
@ -326,7 +343,6 @@ const main = () => {
|
|||
// The order of the next two calls is important
|
||||
setupRendererServer();
|
||||
registerPrivilegedSchemes();
|
||||
increaseDiskCache();
|
||||
migrateLegacyWatchStoreIfNeeded();
|
||||
|
||||
app.on("second-instance", () => {
|
||||
|
@ -341,32 +357,35 @@ const main = () => {
|
|||
// Emitted once, when Electron has finished initializing.
|
||||
//
|
||||
// Note that some Electron APIs can only be used after this event occurs.
|
||||
app.on("ready", async () => {
|
||||
// Create window and prepare for renderer
|
||||
mainWindow = await createMainWindow();
|
||||
attachIPCHandlers();
|
||||
attachFSWatchIPCHandlers(createWatcher(mainWindow));
|
||||
registerStreamProtocol();
|
||||
handleDownloads(mainWindow);
|
||||
handleExternalLinks(mainWindow);
|
||||
addAllowOriginHeader(mainWindow);
|
||||
void app.whenReady().then(() => {
|
||||
void (async () => {
|
||||
// Create window and prepare for the renderer.
|
||||
mainWindow = createMainWindow();
|
||||
attachIPCHandlers();
|
||||
attachFSWatchIPCHandlers(createWatcher(mainWindow));
|
||||
registerStreamProtocol();
|
||||
|
||||
// Start loading the renderer
|
||||
mainWindow.loadURL(rendererURL);
|
||||
// Configure the renderer's environment.
|
||||
setDownloadPath(mainWindow.webContents);
|
||||
allowExternalLinks(mainWindow.webContents);
|
||||
|
||||
// Continue on with the rest of the startup sequence
|
||||
Menu.setApplicationMenu(await createApplicationMenu(mainWindow));
|
||||
setupTrayItem(mainWindow);
|
||||
if (!isDev) setupAutoUpdater(mainWindow);
|
||||
// Start loading the renderer.
|
||||
void mainWindow.loadURL(rendererURL);
|
||||
|
||||
try {
|
||||
deleteLegacyDiskCacheDirIfExists();
|
||||
deleteLegacyKeysStoreIfExists();
|
||||
} catch (e) {
|
||||
// Log but otherwise ignore errors during non-critical startup
|
||||
// actions.
|
||||
log.error("Ignoring startup error", e);
|
||||
}
|
||||
// Continue on with the rest of the startup sequence.
|
||||
Menu.setApplicationMenu(await createApplicationMenu(mainWindow));
|
||||
setupTrayItem(mainWindow);
|
||||
if (!isDev) setupAutoUpdater(mainWindow);
|
||||
|
||||
try {
|
||||
await deleteLegacyDiskCacheDirIfExists();
|
||||
await deleteLegacyKeysStoreIfExists();
|
||||
} catch (e) {
|
||||
// Log but otherwise ignore errors during non-critical startup
|
||||
// actions.
|
||||
log.error("Ignoring startup error", e);
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
// This is a macOS only event. Show our window when the user activates the
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
import { dialog } from "electron/main";
|
||||
import path from "node:path";
|
||||
import type { ElectronFile } from "../types/ipc";
|
||||
import { getDirFilePaths, getElectronFile } from "./services/fs";
|
||||
import { getElectronFilesFromGoogleZip } from "./services/upload";
|
||||
|
||||
export const selectDirectory = async () => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ["openDirectory"],
|
||||
});
|
||||
if (result.filePaths && result.filePaths.length > 0) {
|
||||
return result.filePaths[0]?.split(path.sep)?.join(path.posix.sep);
|
||||
}
|
||||
};
|
||||
|
||||
export const showUploadFilesDialog = async () => {
|
||||
const selectedFiles = await dialog.showOpenDialog({
|
||||
properties: ["openFile", "multiSelections"],
|
||||
});
|
||||
const filePaths = selectedFiles.filePaths;
|
||||
return await Promise.all(filePaths.map(getElectronFile));
|
||||
};
|
||||
|
||||
export const showUploadDirsDialog = async () => {
|
||||
const dir = await dialog.showOpenDialog({
|
||||
properties: ["openDirectory", "multiSelections"],
|
||||
});
|
||||
|
||||
let filePaths: string[] = [];
|
||||
for (const dirPath of dir.filePaths) {
|
||||
filePaths = [...filePaths, ...(await getDirFilePaths(dirPath))];
|
||||
}
|
||||
|
||||
return await Promise.all(filePaths.map(getElectronFile));
|
||||
};
|
||||
|
||||
export const showUploadZipDialog = async () => {
|
||||
const selectedFiles = await dialog.showOpenDialog({
|
||||
properties: ["openFile", "multiSelections"],
|
||||
filters: [{ name: "Zip File", extensions: ["zip"] }],
|
||||
});
|
||||
const filePaths = selectedFiles.filePaths;
|
||||
|
||||
let files: ElectronFile[] = [];
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
files = [...files, ...(await getElectronFilesFromGoogleZip(filePath))];
|
||||
}
|
||||
|
||||
return {
|
||||
zipPaths: filePaths,
|
||||
files,
|
||||
};
|
||||
};
|
|
@ -1,31 +0,0 @@
|
|||
/**
|
||||
* @file file system related functions exposed over the context bridge.
|
||||
*/
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
|
||||
export const fsExists = (path: string) => existsSync(path);
|
||||
|
||||
export const fsRename = (oldPath: string, newPath: string) =>
|
||||
fs.rename(oldPath, newPath);
|
||||
|
||||
export const fsMkdirIfNeeded = (dirPath: string) =>
|
||||
fs.mkdir(dirPath, { recursive: true });
|
||||
|
||||
export const fsRmdir = (path: string) => fs.rmdir(path);
|
||||
|
||||
export const fsRm = (path: string) => fs.rm(path);
|
||||
|
||||
export const fsReadTextFile = async (filePath: string) =>
|
||||
fs.readFile(filePath, "utf-8");
|
||||
|
||||
export const fsWriteFile = (path: string, contents: string) =>
|
||||
fs.writeFile(path, contents);
|
||||
|
||||
export const fsIsDir = async (dirPath: string) => {
|
||||
if (!existsSync(dirPath)) return false;
|
||||
const stat = await fs.stat(dirPath);
|
||||
return stat.isDirectory();
|
||||
};
|
||||
|
||||
export const fsSize = (path: string) => fs.stat(path).then((s) => s.size);
|
|
@ -1,63 +0,0 @@
|
|||
import { BrowserWindow, app, shell } from "electron";
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { rendererURL } from "../main";
|
||||
|
||||
export function handleDownloads(mainWindow: BrowserWindow) {
|
||||
mainWindow.webContents.session.on("will-download", (_, item) => {
|
||||
item.setSavePath(
|
||||
getUniqueSavePath(item.getFilename(), app.getPath("downloads")),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function getUniqueSavePath(filename: string, directory: string): string {
|
||||
let uniqueFileSavePath = path.join(directory, filename);
|
||||
const { name: filenameWithoutExtension, ext: extension } =
|
||||
path.parse(filename);
|
||||
let n = 0;
|
||||
while (existsSync(uniqueFileSavePath)) {
|
||||
n++;
|
||||
// filter need to remove undefined extension from the array
|
||||
// else [`${fileName}`, undefined].join(".") will lead to `${fileName}.` as joined string
|
||||
const fileNameWithNumberedSuffix = [
|
||||
`${filenameWithoutExtension}(${n})`,
|
||||
extension,
|
||||
]
|
||||
.filter((x) => x) // filters out undefined/null values
|
||||
.join("");
|
||||
uniqueFileSavePath = path.join(directory, fileNameWithNumberedSuffix);
|
||||
}
|
||||
return uniqueFileSavePath;
|
||||
}
|
||||
|
||||
export function handleExternalLinks(mainWindow: BrowserWindow) {
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (!url.startsWith(rendererURL)) {
|
||||
shell.openExternal(url);
|
||||
return { action: "deny" };
|
||||
} else {
|
||||
return { action: "allow" };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function addAllowOriginHeader(mainWindow: BrowserWindow) {
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived(
|
||||
(details, callback) => {
|
||||
details.responseHeaders = lowerCaseHeaders(details.responseHeaders);
|
||||
details.responseHeaders["access-control-allow-origin"] = ["*"];
|
||||
callback({
|
||||
responseHeaders: details.responseHeaders,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function lowerCaseHeaders(responseHeaders: Record<string, string[]>) {
|
||||
const headers: Record<string, string[]> = {};
|
||||
for (const key of Object.keys(responseHeaders)) {
|
||||
headers[key.toLowerCase()] = responseHeaders[key];
|
||||
}
|
||||
return headers;
|
||||
}
|
|
@ -14,13 +14,21 @@ import type {
|
|||
CollectionMapping,
|
||||
FolderWatch,
|
||||
PendingUploads,
|
||||
ZipItem,
|
||||
} from "../types/ipc";
|
||||
import { logToDisk } from "./log";
|
||||
import {
|
||||
appVersion,
|
||||
skipAppUpdate,
|
||||
updateAndRestart,
|
||||
updateOnNextRestart,
|
||||
} from "./services/app-update";
|
||||
import {
|
||||
openDirectory,
|
||||
openLogDirectory,
|
||||
selectDirectory,
|
||||
showUploadDirsDialog,
|
||||
showUploadFilesDialog,
|
||||
showUploadZipDialog,
|
||||
} from "./dialogs";
|
||||
} from "./services/dir";
|
||||
import { ffmpegExec } from "./services/ffmpeg";
|
||||
import {
|
||||
fsExists,
|
||||
fsIsDir,
|
||||
|
@ -29,18 +37,8 @@ import {
|
|||
fsRename,
|
||||
fsRm,
|
||||
fsRmdir,
|
||||
fsSize,
|
||||
fsWriteFile,
|
||||
} from "./fs";
|
||||
import { logToDisk } from "./log";
|
||||
import {
|
||||
appVersion,
|
||||
skipAppUpdate,
|
||||
updateAndRestart,
|
||||
updateOnNextRestart,
|
||||
} from "./services/app-update";
|
||||
import { ffmpegExec } from "./services/ffmpeg";
|
||||
import { getDirFiles } from "./services/fs";
|
||||
} from "./services/fs";
|
||||
import { convertToJPEG, generateImageThumbnail } from "./services/image";
|
||||
import {
|
||||
clipImageEmbedding,
|
||||
|
@ -53,20 +51,23 @@ import {
|
|||
saveEncryptionKey,
|
||||
} from "./services/store";
|
||||
import {
|
||||
getElectronFilesFromGoogleZip,
|
||||
clearPendingUploads,
|
||||
listZipItems,
|
||||
markUploadedFiles,
|
||||
markUploadedZipItems,
|
||||
pathOrZipItemSize,
|
||||
pendingUploads,
|
||||
setPendingUploadCollection,
|
||||
setPendingUploadFiles,
|
||||
setPendingUploads,
|
||||
} from "./services/upload";
|
||||
import {
|
||||
watchAdd,
|
||||
watchFindFiles,
|
||||
watchGet,
|
||||
watchRemove,
|
||||
watchReset,
|
||||
watchUpdateIgnoredFiles,
|
||||
watchUpdateSyncedFiles,
|
||||
} from "./services/watch";
|
||||
import { openDirectory, openLogDirectory } from "./utils-electron";
|
||||
|
||||
/**
|
||||
* Listen for IPC events sent/invoked by the renderer process, and route them to
|
||||
|
@ -93,16 +94,20 @@ export const attachIPCHandlers = () => {
|
|||
|
||||
ipcMain.handle("appVersion", () => appVersion());
|
||||
|
||||
ipcMain.handle("openDirectory", (_, dirPath) => openDirectory(dirPath));
|
||||
ipcMain.handle("openDirectory", (_, dirPath: string) =>
|
||||
openDirectory(dirPath),
|
||||
);
|
||||
|
||||
ipcMain.handle("openLogDirectory", () => openLogDirectory());
|
||||
|
||||
// See [Note: Catching exception during .send/.on]
|
||||
ipcMain.on("logToDisk", (_, message) => logToDisk(message));
|
||||
ipcMain.on("logToDisk", (_, message: string) => logToDisk(message));
|
||||
|
||||
ipcMain.handle("selectDirectory", () => selectDirectory());
|
||||
|
||||
ipcMain.on("clearStores", () => clearStores());
|
||||
|
||||
ipcMain.handle("saveEncryptionKey", (_, encryptionKey) =>
|
||||
ipcMain.handle("saveEncryptionKey", (_, encryptionKey: string) =>
|
||||
saveEncryptionKey(encryptionKey),
|
||||
);
|
||||
|
||||
|
@ -112,21 +117,23 @@ export const attachIPCHandlers = () => {
|
|||
|
||||
ipcMain.on("updateAndRestart", () => updateAndRestart());
|
||||
|
||||
ipcMain.on("updateOnNextRestart", (_, version) =>
|
||||
ipcMain.on("updateOnNextRestart", (_, version: string) =>
|
||||
updateOnNextRestart(version),
|
||||
);
|
||||
|
||||
ipcMain.on("skipAppUpdate", (_, version) => skipAppUpdate(version));
|
||||
ipcMain.on("skipAppUpdate", (_, version: string) => skipAppUpdate(version));
|
||||
|
||||
// - FS
|
||||
|
||||
ipcMain.handle("fsExists", (_, path) => fsExists(path));
|
||||
ipcMain.handle("fsExists", (_, path: string) => fsExists(path));
|
||||
|
||||
ipcMain.handle("fsRename", (_, oldPath: string, newPath: string) =>
|
||||
fsRename(oldPath, newPath),
|
||||
);
|
||||
|
||||
ipcMain.handle("fsMkdirIfNeeded", (_, dirPath) => fsMkdirIfNeeded(dirPath));
|
||||
ipcMain.handle("fsMkdirIfNeeded", (_, dirPath: string) =>
|
||||
fsMkdirIfNeeded(dirPath),
|
||||
);
|
||||
|
||||
ipcMain.handle("fsRmdir", (_, path: string) => fsRmdir(path));
|
||||
|
||||
|
@ -140,8 +147,6 @@ export const attachIPCHandlers = () => {
|
|||
|
||||
ipcMain.handle("fsIsDir", (_, dirPath: string) => fsIsDir(dirPath));
|
||||
|
||||
ipcMain.handle("fsSize", (_, path: string) => fsSize(path));
|
||||
|
||||
// - Conversion
|
||||
|
||||
ipcMain.handle("convertToJPEG", (_, imageData: Uint8Array) =>
|
||||
|
@ -152,10 +157,10 @@ export const attachIPCHandlers = () => {
|
|||
"generateImageThumbnail",
|
||||
(
|
||||
_,
|
||||
dataOrPath: Uint8Array | string,
|
||||
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
|
||||
maxDimension: number,
|
||||
maxSize: number,
|
||||
) => generateImageThumbnail(dataOrPath, maxDimension, maxSize),
|
||||
) => generateImageThumbnail(dataOrPathOrZipItem, maxDimension, maxSize),
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
|
@ -163,10 +168,16 @@ export const attachIPCHandlers = () => {
|
|||
(
|
||||
_,
|
||||
command: string[],
|
||||
dataOrPath: Uint8Array | string,
|
||||
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
|
||||
outputFileExtension: string,
|
||||
timeoutMS: number,
|
||||
) => ffmpegExec(command, dataOrPath, outputFileExtension, timeoutMS),
|
||||
) =>
|
||||
ffmpegExec(
|
||||
command,
|
||||
dataOrPathOrZipItem,
|
||||
outputFileExtension,
|
||||
timeoutMS,
|
||||
),
|
||||
);
|
||||
|
||||
// - ML
|
||||
|
@ -187,37 +198,33 @@ export const attachIPCHandlers = () => {
|
|||
faceEmbedding(input),
|
||||
);
|
||||
|
||||
// - File selection
|
||||
|
||||
ipcMain.handle("selectDirectory", () => selectDirectory());
|
||||
|
||||
ipcMain.handle("showUploadFilesDialog", () => showUploadFilesDialog());
|
||||
|
||||
ipcMain.handle("showUploadDirsDialog", () => showUploadDirsDialog());
|
||||
|
||||
ipcMain.handle("showUploadZipDialog", () => showUploadZipDialog());
|
||||
|
||||
// - Upload
|
||||
|
||||
ipcMain.handle("listZipItems", (_, zipPath: string) =>
|
||||
listZipItems(zipPath),
|
||||
);
|
||||
|
||||
ipcMain.handle("pathOrZipItemSize", (_, pathOrZipItem: string | ZipItem) =>
|
||||
pathOrZipItemSize(pathOrZipItem),
|
||||
);
|
||||
|
||||
ipcMain.handle("pendingUploads", () => pendingUploads());
|
||||
|
||||
ipcMain.handle("setPendingUploadCollection", (_, collectionName: string) =>
|
||||
setPendingUploadCollection(collectionName),
|
||||
ipcMain.handle("setPendingUploads", (_, pendingUploads: PendingUploads) =>
|
||||
setPendingUploads(pendingUploads),
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
"setPendingUploadFiles",
|
||||
(_, type: PendingUploads["type"], filePaths: string[]) =>
|
||||
setPendingUploadFiles(type, filePaths),
|
||||
"markUploadedFiles",
|
||||
(_, paths: PendingUploads["filePaths"]) => markUploadedFiles(paths),
|
||||
);
|
||||
|
||||
// -
|
||||
|
||||
ipcMain.handle("getElectronFilesFromGoogleZip", (_, filePath: string) =>
|
||||
getElectronFilesFromGoogleZip(filePath),
|
||||
ipcMain.handle(
|
||||
"markUploadedZipItems",
|
||||
(_, items: PendingUploads["zipItems"]) => markUploadedZipItems(items),
|
||||
);
|
||||
|
||||
ipcMain.handle("getDirFiles", (_, dirPath: string) => getDirFiles(dirPath));
|
||||
ipcMain.handle("clearPendingUploads", () => clearPendingUploads());
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -257,4 +264,6 @@ export const attachFSWatchIPCHandlers = (watcher: FSWatcher) => {
|
|||
ipcMain.handle("watchFindFiles", (_, folderPath: string) =>
|
||||
watchFindFiles(folderPath),
|
||||
);
|
||||
|
||||
ipcMain.handle("watchReset", () => watchReset(watcher));
|
||||
};
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import log from "electron-log";
|
||||
import util from "node:util";
|
||||
import { isDev } from "./utils-electron";
|
||||
import { isDev } from "./utils/electron";
|
||||
|
||||
/**
|
||||
* Initialize logging in the main process.
|
||||
*
|
||||
* This will set our underlying logger up to log to a file named `ente.log`,
|
||||
*
|
||||
* - on Linux at ~/.config/ente/logs/main.log
|
||||
* - on macOS at ~/Library/Logs/ente/main.log
|
||||
* - on Windows at %USERPROFILE%\AppData\Roaming\ente\logs\main.log
|
||||
* - on Linux at ~/.config/ente/logs/ente.log
|
||||
* - on macOS at ~/Library/Logs/ente/ente.log
|
||||
* - on Windows at %USERPROFILE%\AppData\Roaming\ente\logs\ente.log
|
||||
*
|
||||
* On dev builds, it will also log to the console.
|
||||
*/
|
||||
|
@ -65,7 +65,7 @@ const logError_ = (message: string) => {
|
|||
if (isDev) console.error(`[error] ${message}`);
|
||||
};
|
||||
|
||||
const logInfo = (...params: any[]) => {
|
||||
const logInfo = (...params: unknown[]) => {
|
||||
const message = params
|
||||
.map((p) => (typeof p == "string" ? p : util.inspect(p)))
|
||||
.join(" ");
|
||||
|
@ -73,7 +73,7 @@ const logInfo = (...params: any[]) => {
|
|||
if (isDev) console.log(`[info] ${message}`);
|
||||
};
|
||||
|
||||
const logDebug = (param: () => any) => {
|
||||
const logDebug = (param: () => unknown) => {
|
||||
if (isDev) {
|
||||
const p = param();
|
||||
console.log(`[debug] ${typeof p == "string" ? p : util.inspect(p)}`);
|
||||
|
|
|
@ -8,8 +8,9 @@ import {
|
|||
import { allowWindowClose } from "../main";
|
||||
import { forceCheckForAppUpdates } from "./services/app-update";
|
||||
import autoLauncher from "./services/auto-launcher";
|
||||
import { openLogDirectory } from "./services/dir";
|
||||
import { userPreferences } from "./stores/user-preferences";
|
||||
import { isDev, openLogDirectory } from "./utils-electron";
|
||||
import { isDev } from "./utils/electron";
|
||||
|
||||
/** Create and return the entries in the app's main menu bar */
|
||||
export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
|
||||
|
@ -18,7 +19,7 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
|
|||
// Whenever the menu is redrawn the current value of these variables is used
|
||||
// to set the checked state for the various settings checkboxes.
|
||||
let isAutoLaunchEnabled = await autoLauncher.isEnabled();
|
||||
let shouldHideDockIcon = userPreferences.get("hideDockIcon");
|
||||
let shouldHideDockIcon = !!userPreferences.get("hideDockIcon");
|
||||
|
||||
const macOSOnly = (options: MenuItemConstructorOptions[]) =>
|
||||
process.platform == "darwin" ? options : [];
|
||||
|
@ -29,12 +30,12 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
|
|||
const handleCheckForUpdates = () => forceCheckForAppUpdates(mainWindow);
|
||||
|
||||
const handleViewChangelog = () =>
|
||||
shell.openExternal(
|
||||
void shell.openExternal(
|
||||
"https://github.com/ente-io/ente/blob/main/desktop/CHANGELOG.md",
|
||||
);
|
||||
|
||||
const toggleAutoLaunch = () => {
|
||||
autoLauncher.toggleAutoLaunch();
|
||||
void autoLauncher.toggleAutoLaunch();
|
||||
isAutoLaunchEnabled = !isAutoLaunchEnabled;
|
||||
};
|
||||
|
||||
|
@ -45,13 +46,15 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
|
|||
shouldHideDockIcon = !shouldHideDockIcon;
|
||||
};
|
||||
|
||||
const handleHelp = () => shell.openExternal("https://help.ente.io/photos/");
|
||||
const handleHelp = () =>
|
||||
void shell.openExternal("https://help.ente.io/photos/");
|
||||
|
||||
const handleSupport = () => shell.openExternal("mailto:support@ente.io");
|
||||
const handleSupport = () =>
|
||||
void shell.openExternal("mailto:support@ente.io");
|
||||
|
||||
const handleBlog = () => shell.openExternal("https://ente.io/blog/");
|
||||
const handleBlog = () => void shell.openExternal("https://ente.io/blog/");
|
||||
|
||||
const handleViewLogs = openLogDirectory;
|
||||
const handleViewLogs = () => void openLogDirectory();
|
||||
|
||||
return Menu.buildFromTemplate([
|
||||
{
|
||||
|
|
|
@ -12,8 +12,8 @@ export const setupAutoUpdater = (mainWindow: BrowserWindow) => {
|
|||
autoUpdater.autoDownload = false;
|
||||
|
||||
const oneDay = 1 * 24 * 60 * 60 * 1000;
|
||||
setInterval(() => checkForUpdatesAndNotify(mainWindow), oneDay);
|
||||
checkForUpdatesAndNotify(mainWindow);
|
||||
setInterval(() => void checkForUpdatesAndNotify(mainWindow), oneDay);
|
||||
void checkForUpdatesAndNotify(mainWindow);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -22,7 +22,7 @@ export const setupAutoUpdater = (mainWindow: BrowserWindow) => {
|
|||
export const forceCheckForAppUpdates = (mainWindow: BrowserWindow) => {
|
||||
userPreferences.delete("skipAppVersion");
|
||||
userPreferences.delete("muteUpdateNotificationVersion");
|
||||
checkForUpdatesAndNotify(mainWindow);
|
||||
void checkForUpdatesAndNotify(mainWindow);
|
||||
};
|
||||
|
||||
const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => {
|
||||
|
@ -36,18 +36,21 @@ const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => {
|
|||
|
||||
log.debug(() => `Update check found version ${version}`);
|
||||
|
||||
if (!version)
|
||||
throw new Error("Unexpected empty version obtained from auto-updater");
|
||||
|
||||
if (compareVersions(version, app.getVersion()) <= 0) {
|
||||
log.debug(() => "Skipping update, already at latest version");
|
||||
return;
|
||||
}
|
||||
|
||||
if (version === userPreferences.get("skipAppVersion")) {
|
||||
if (version == userPreferences.get("skipAppVersion")) {
|
||||
log.info(`User chose to skip version ${version}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const mutedVersion = userPreferences.get("muteUpdateNotificationVersion");
|
||||
if (version === mutedVersion) {
|
||||
if (version == mutedVersion) {
|
||||
log.info(`User has muted update notifications for version ${version}`);
|
||||
return;
|
||||
}
|
||||
|
@ -56,7 +59,7 @@ const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => {
|
|||
mainWindow.webContents.send("appUpdateAvailable", update);
|
||||
|
||||
log.debug(() => "Attempting auto update");
|
||||
autoUpdater.downloadUpdate();
|
||||
await autoUpdater.downloadUpdate();
|
||||
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
const fiveMinutes = 5 * 60 * 1000;
|
||||
|
|
|
@ -38,7 +38,7 @@ class AutoLauncher {
|
|||
}
|
||||
}
|
||||
|
||||
async wasAutoLaunched() {
|
||||
wasAutoLaunched() {
|
||||
if (this.autoLaunch) {
|
||||
return app.commandLine.hasSwitch("hidden");
|
||||
} else {
|
||||
|
|
51
desktop/src/main/services/dir.ts
Normal file
51
desktop/src/main/services/dir.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { shell } from "electron/common";
|
||||
import { app, dialog } from "electron/main";
|
||||
import path from "node:path";
|
||||
import { posixPath } from "../utils/electron";
|
||||
|
||||
export const selectDirectory = async () => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ["openDirectory"],
|
||||
});
|
||||
const dirPath = result.filePaths[0];
|
||||
return dirPath ? posixPath(dirPath) : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the given {@link dirPath} in the system's folder viewer.
|
||||
*
|
||||
* For example, on macOS this'll open {@link dirPath} in Finder.
|
||||
*/
|
||||
export const openDirectory = async (dirPath: string) => {
|
||||
// We need to use `path.normalize` because `shell.openPath; does not support
|
||||
// POSIX path, it needs to be a platform specific path:
|
||||
// https://github.com/electron/electron/issues/28831#issuecomment-826370589
|
||||
const res = await shell.openPath(path.normalize(dirPath));
|
||||
// `shell.openPath` resolves with a string containing the error message
|
||||
// corresponding to the failure if a failure occurred, otherwise "".
|
||||
if (res) throw new Error(`Failed to open directory ${dirPath}: res`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the app's log directory in the system's folder viewer.
|
||||
*
|
||||
* @see {@link openDirectory}
|
||||
*/
|
||||
export const openLogDirectory = () => openDirectory(logDirectoryPath());
|
||||
|
||||
/**
|
||||
* Return the path where the logs for the app are saved.
|
||||
*
|
||||
* [Note: Electron app paths]
|
||||
*
|
||||
* By default, these paths are at the following locations:
|
||||
*
|
||||
* - macOS: `~/Library/Application Support/ente`
|
||||
* - Linux: `~/.config/ente`
|
||||
* - Windows: `%APPDATA%`, e.g. `C:\Users\<username>\AppData\Local\ente`
|
||||
* - Windows: C:\Users\<you>\AppData\Local\<Your App Name>
|
||||
*
|
||||
* https://www.electronjs.org/docs/latest/api/app
|
||||
*
|
||||
*/
|
||||
const logDirectoryPath = () => app.getPath("logs");
|
|
@ -1,9 +1,14 @@
|
|||
import pathToFfmpeg from "ffmpeg-static";
|
||||
import fs from "node:fs/promises";
|
||||
import type { ZipItem } from "../../types/ipc";
|
||||
import log from "../log";
|
||||
import { withTimeout } from "../utils";
|
||||
import { execAsync } from "../utils-electron";
|
||||
import { deleteTempFile, makeTempFilePath } from "../utils-temp";
|
||||
import { ensure, withTimeout } from "../utils/common";
|
||||
import { execAsync } from "../utils/electron";
|
||||
import {
|
||||
deleteTempFile,
|
||||
makeFileForDataOrPathOrZipItem,
|
||||
makeTempFilePath,
|
||||
} from "../utils/temp";
|
||||
|
||||
/* Duplicated in the web app's code (used by the WASM FFmpeg implementation). */
|
||||
const ffmpegPathPlaceholder = "FFMPEG";
|
||||
|
@ -39,28 +44,24 @@ const outputPathPlaceholder = "OUTPUT";
|
|||
*/
|
||||
export const ffmpegExec = async (
|
||||
command: string[],
|
||||
dataOrPath: Uint8Array | string,
|
||||
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
|
||||
outputFileExtension: string,
|
||||
timeoutMS: number,
|
||||
): Promise<Uint8Array> => {
|
||||
// TODO (MR): This currently copies files for both input and output. This
|
||||
// needs to be tested extremely large video files when invoked downstream of
|
||||
// `convertToMP4` in the web code.
|
||||
// TODO (MR): This currently copies files for both input (when
|
||||
// dataOrPathOrZipItem is data) and output. This needs to be tested
|
||||
// extremely large video files when invoked downstream of `convertToMP4` in
|
||||
// the web code.
|
||||
|
||||
let inputFilePath: string;
|
||||
let isInputFileTemporary: boolean;
|
||||
if (dataOrPath instanceof Uint8Array) {
|
||||
inputFilePath = await makeTempFilePath();
|
||||
isInputFileTemporary = true;
|
||||
} else {
|
||||
inputFilePath = dataOrPath;
|
||||
isInputFileTemporary = false;
|
||||
}
|
||||
const {
|
||||
path: inputFilePath,
|
||||
isFileTemporary: isInputFileTemporary,
|
||||
writeToTemporaryFile: writeToTemporaryInputFile,
|
||||
} = await makeFileForDataOrPathOrZipItem(dataOrPathOrZipItem);
|
||||
|
||||
const outputFilePath = await makeTempFilePath(outputFileExtension);
|
||||
try {
|
||||
if (dataOrPath instanceof Uint8Array)
|
||||
await fs.writeFile(inputFilePath, dataOrPath);
|
||||
await writeToTemporaryInputFile();
|
||||
|
||||
const cmd = substitutePlaceholders(
|
||||
command,
|
||||
|
@ -109,5 +110,5 @@ const ffmpegBinaryPath = () => {
|
|||
// This substitution of app.asar by app.asar.unpacked is suggested by the
|
||||
// ffmpeg-static library author themselves:
|
||||
// https://github.com/eugeneware/ffmpeg-static/issues/16
|
||||
return pathToFfmpeg.replace("app.asar", "app.asar.unpacked");
|
||||
return ensure(pathToFfmpeg).replace("app.asar", "app.asar.unpacked");
|
||||
};
|
||||
|
|
|
@ -1,177 +1,30 @@
|
|||
import StreamZip from "node-stream-zip";
|
||||
/**
|
||||
* @file file system related functions exposed over the context bridge.
|
||||
*/
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { ElectronFile } from "../../types/ipc";
|
||||
import log from "../log";
|
||||
|
||||
const FILE_STREAM_CHUNK_SIZE: number = 4 * 1024 * 1024;
|
||||
export const fsExists = (path: string) => existsSync(path);
|
||||
|
||||
export async function getDirFiles(dirPath: string) {
|
||||
const files = await getDirFilePaths(dirPath);
|
||||
const electronFiles = await Promise.all(files.map(getElectronFile));
|
||||
return electronFiles;
|
||||
}
|
||||
export const fsRename = (oldPath: string, newPath: string) =>
|
||||
fs.rename(oldPath, newPath);
|
||||
|
||||
// https://stackoverflow.com/a/63111390
|
||||
export const getDirFilePaths = async (dirPath: string) => {
|
||||
if (!(await fs.stat(dirPath)).isDirectory()) {
|
||||
return [dirPath];
|
||||
}
|
||||
export const fsMkdirIfNeeded = (dirPath: string) =>
|
||||
fs.mkdir(dirPath, { recursive: true });
|
||||
|
||||
let files: string[] = [];
|
||||
const filePaths = await fs.readdir(dirPath);
|
||||
export const fsRmdir = (path: string) => fs.rmdir(path);
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
const absolute = path.join(dirPath, filePath);
|
||||
files = [...files, ...(await getDirFilePaths(absolute))];
|
||||
}
|
||||
export const fsRm = (path: string) => fs.rm(path);
|
||||
|
||||
return files;
|
||||
};
|
||||
|
||||
const getFileStream = async (filePath: string) => {
|
||||
const file = await fs.open(filePath, "r");
|
||||
let offset = 0;
|
||||
const readableStream = new ReadableStream<Uint8Array>({
|
||||
async pull(controller) {
|
||||
try {
|
||||
const buff = new Uint8Array(FILE_STREAM_CHUNK_SIZE);
|
||||
const bytesRead = (await file.read(
|
||||
buff,
|
||||
0,
|
||||
FILE_STREAM_CHUNK_SIZE,
|
||||
offset,
|
||||
)) as unknown as number;
|
||||
offset += bytesRead;
|
||||
if (bytesRead === 0) {
|
||||
controller.close();
|
||||
await file.close();
|
||||
} else {
|
||||
controller.enqueue(buff.slice(0, bytesRead));
|
||||
}
|
||||
} catch (e) {
|
||||
await file.close();
|
||||
}
|
||||
},
|
||||
async cancel() {
|
||||
await file.close();
|
||||
},
|
||||
});
|
||||
return readableStream;
|
||||
};
|
||||
|
||||
export async function getElectronFile(filePath: string): Promise<ElectronFile> {
|
||||
const fileStats = await fs.stat(filePath);
|
||||
return {
|
||||
path: filePath.split(path.sep).join(path.posix.sep),
|
||||
name: path.basename(filePath),
|
||||
size: fileStats.size,
|
||||
lastModified: fileStats.mtime.valueOf(),
|
||||
stream: async () => {
|
||||
if (!existsSync(filePath)) {
|
||||
throw new Error("electronFile does not exist");
|
||||
}
|
||||
return await getFileStream(filePath);
|
||||
},
|
||||
blob: async () => {
|
||||
if (!existsSync(filePath)) {
|
||||
throw new Error("electronFile does not exist");
|
||||
}
|
||||
const blob = await fs.readFile(filePath);
|
||||
return new Blob([new Uint8Array(blob)]);
|
||||
},
|
||||
arrayBuffer: async () => {
|
||||
if (!existsSync(filePath)) {
|
||||
throw new Error("electronFile does not exist");
|
||||
}
|
||||
const blob = await fs.readFile(filePath);
|
||||
return new Uint8Array(blob);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const getZipFileStream = async (
|
||||
zip: StreamZip.StreamZipAsync,
|
||||
filePath: string,
|
||||
) => {
|
||||
const stream = await zip.stream(filePath);
|
||||
const done = {
|
||||
current: false,
|
||||
};
|
||||
const inProgress = {
|
||||
current: false,
|
||||
};
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let resolveObj: (value?: any) => void = null;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let rejectObj: (reason?: any) => void = null;
|
||||
stream.on("readable", () => {
|
||||
try {
|
||||
if (resolveObj) {
|
||||
inProgress.current = true;
|
||||
const chunk = stream.read(FILE_STREAM_CHUNK_SIZE) as Buffer;
|
||||
if (chunk) {
|
||||
resolveObj(new Uint8Array(chunk));
|
||||
resolveObj = null;
|
||||
}
|
||||
inProgress.current = false;
|
||||
}
|
||||
} catch (e) {
|
||||
rejectObj(e);
|
||||
}
|
||||
});
|
||||
stream.on("end", () => {
|
||||
try {
|
||||
done.current = true;
|
||||
if (resolveObj && !inProgress.current) {
|
||||
resolveObj(null);
|
||||
resolveObj = null;
|
||||
}
|
||||
} catch (e) {
|
||||
rejectObj(e);
|
||||
}
|
||||
});
|
||||
stream.on("error", (e) => {
|
||||
try {
|
||||
done.current = true;
|
||||
if (rejectObj) {
|
||||
rejectObj(e);
|
||||
rejectObj = null;
|
||||
}
|
||||
} catch (e) {
|
||||
rejectObj(e);
|
||||
}
|
||||
});
|
||||
|
||||
const readStreamData = async () => {
|
||||
return new Promise<Uint8Array>((resolve, reject) => {
|
||||
const chunk = stream.read(FILE_STREAM_CHUNK_SIZE) as Buffer;
|
||||
|
||||
if (chunk || done.current) {
|
||||
resolve(chunk);
|
||||
} else {
|
||||
resolveObj = resolve;
|
||||
rejectObj = reject;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const readableStream = new ReadableStream<Uint8Array>({
|
||||
async pull(controller) {
|
||||
try {
|
||||
const data = await readStreamData();
|
||||
|
||||
if (data) {
|
||||
controller.enqueue(data);
|
||||
} else {
|
||||
controller.close();
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("Failed to pull from readableStream", e);
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
return readableStream;
|
||||
export const fsReadTextFile = async (filePath: string) =>
|
||||
fs.readFile(filePath, "utf-8");
|
||||
|
||||
export const fsWriteFile = (path: string, contents: string) =>
|
||||
fs.writeFile(path, contents);
|
||||
|
||||
export const fsIsDir = async (dirPath: string) => {
|
||||
if (!existsSync(dirPath)) return false;
|
||||
const stat = await fs.stat(dirPath);
|
||||
return stat.isDirectory();
|
||||
};
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
/** @file Image format conversions and thumbnail generation */
|
||||
|
||||
import fs from "node:fs/promises";
|
||||
import path from "path";
|
||||
import { CustomErrorMessage } from "../../types/ipc";
|
||||
import path from "node:path";
|
||||
import { CustomErrorMessage, type ZipItem } from "../../types/ipc";
|
||||
import log from "../log";
|
||||
import { execAsync, isDev } from "../utils-electron";
|
||||
import { deleteTempFile, makeTempFilePath } from "../utils-temp";
|
||||
import { execAsync, isDev } from "../utils/electron";
|
||||
import {
|
||||
deleteTempFile,
|
||||
makeFileForDataOrPathOrZipItem,
|
||||
makeTempFilePath,
|
||||
} from "../utils/temp";
|
||||
|
||||
export const convertToJPEG = async (imageData: Uint8Array) => {
|
||||
const inputFilePath = await makeTempFilePath();
|
||||
|
@ -63,19 +67,15 @@ const imageMagickPath = () =>
|
|||
path.join(isDev ? "build" : process.resourcesPath, "image-magick");
|
||||
|
||||
export const generateImageThumbnail = async (
|
||||
dataOrPath: Uint8Array | string,
|
||||
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
|
||||
maxDimension: number,
|
||||
maxSize: number,
|
||||
): Promise<Uint8Array> => {
|
||||
let inputFilePath: string;
|
||||
let isInputFileTemporary: boolean;
|
||||
if (dataOrPath instanceof Uint8Array) {
|
||||
inputFilePath = await makeTempFilePath();
|
||||
isInputFileTemporary = true;
|
||||
} else {
|
||||
inputFilePath = dataOrPath;
|
||||
isInputFileTemporary = false;
|
||||
}
|
||||
const {
|
||||
path: inputFilePath,
|
||||
isFileTemporary: isInputFileTemporary,
|
||||
writeToTemporaryFile: writeToTemporaryInputFile,
|
||||
} = await makeFileForDataOrPathOrZipItem(dataOrPathOrZipItem);
|
||||
|
||||
const outputFilePath = await makeTempFilePath("jpeg");
|
||||
|
||||
|
@ -89,8 +89,7 @@ export const generateImageThumbnail = async (
|
|||
);
|
||||
|
||||
try {
|
||||
if (dataOrPath instanceof Uint8Array)
|
||||
await fs.writeFile(inputFilePath, dataOrPath);
|
||||
await writeToTemporaryInputFile();
|
||||
|
||||
let thumbnail: Uint8Array;
|
||||
do {
|
||||
|
|
|
@ -11,7 +11,8 @@ import * as ort from "onnxruntime-node";
|
|||
import Tokenizer from "../../thirdparty/clip-bpe-ts/mod";
|
||||
import log from "../log";
|
||||
import { writeStream } from "../stream";
|
||||
import { deleteTempFile, makeTempFilePath } from "../utils-temp";
|
||||
import { ensure } from "../utils/common";
|
||||
import { deleteTempFile, makeTempFilePath } from "../utils/temp";
|
||||
import { makeCachedInferenceSession } from "./ml";
|
||||
|
||||
const cachedCLIPImageSession = makeCachedInferenceSession(
|
||||
|
@ -22,7 +23,7 @@ const cachedCLIPImageSession = makeCachedInferenceSession(
|
|||
export const clipImageEmbedding = async (jpegImageData: Uint8Array) => {
|
||||
const tempFilePath = await makeTempFilePath();
|
||||
const imageStream = new Response(jpegImageData.buffer).body;
|
||||
await writeStream(tempFilePath, imageStream);
|
||||
await writeStream(tempFilePath, ensure(imageStream));
|
||||
try {
|
||||
return await clipImageEmbedding_(tempFilePath);
|
||||
} finally {
|
||||
|
@ -44,30 +45,30 @@ const clipImageEmbedding_ = async (jpegFilePath: string) => {
|
|||
`onnx/clip image embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`,
|
||||
);
|
||||
/* Need these model specific casts to type the result */
|
||||
const imageEmbedding = results["output"].data as Float32Array;
|
||||
const imageEmbedding = ensure(results.output).data as Float32Array;
|
||||
return normalizeEmbedding(imageEmbedding);
|
||||
};
|
||||
|
||||
const getRGBData = async (jpegFilePath: string) => {
|
||||
const getRGBData = async (jpegFilePath: string): Promise<number[]> => {
|
||||
const jpegData = await fs.readFile(jpegFilePath);
|
||||
const rawImageData = jpeg.decode(jpegData, {
|
||||
useTArray: true,
|
||||
formatAsRGBA: false,
|
||||
});
|
||||
|
||||
const nx: number = rawImageData.width;
|
||||
const ny: number = rawImageData.height;
|
||||
const inputImage: Uint8Array = rawImageData.data;
|
||||
const nx = rawImageData.width;
|
||||
const ny = rawImageData.height;
|
||||
const inputImage = rawImageData.data;
|
||||
|
||||
const nx2: number = 224;
|
||||
const ny2: number = 224;
|
||||
const totalSize: number = 3 * nx2 * ny2;
|
||||
const nx2 = 224;
|
||||
const ny2 = 224;
|
||||
const totalSize = 3 * nx2 * ny2;
|
||||
|
||||
const result: number[] = Array(totalSize).fill(0);
|
||||
const scale: number = Math.max(nx, ny) / 224;
|
||||
const result = Array<number>(totalSize).fill(0);
|
||||
const scale = Math.max(nx, ny) / 224;
|
||||
|
||||
const nx3: number = Math.round(nx / scale);
|
||||
const ny3: number = Math.round(ny / scale);
|
||||
const nx3 = Math.round(nx / scale);
|
||||
const ny3 = Math.round(ny / scale);
|
||||
|
||||
const mean: number[] = [0.48145466, 0.4578275, 0.40821073];
|
||||
const std: number[] = [0.26862954, 0.26130258, 0.27577711];
|
||||
|
@ -76,40 +77,40 @@ const getRGBData = async (jpegFilePath: string) => {
|
|||
for (let x = 0; x < nx3; x++) {
|
||||
for (let c = 0; c < 3; c++) {
|
||||
// Linear interpolation
|
||||
const sx: number = (x + 0.5) * scale - 0.5;
|
||||
const sy: number = (y + 0.5) * scale - 0.5;
|
||||
const sx = (x + 0.5) * scale - 0.5;
|
||||
const sy = (y + 0.5) * scale - 0.5;
|
||||
|
||||
const x0: number = Math.max(0, Math.floor(sx));
|
||||
const y0: number = Math.max(0, Math.floor(sy));
|
||||
const x0 = Math.max(0, Math.floor(sx));
|
||||
const y0 = Math.max(0, Math.floor(sy));
|
||||
|
||||
const x1: number = Math.min(x0 + 1, nx - 1);
|
||||
const y1: number = Math.min(y0 + 1, ny - 1);
|
||||
const x1 = Math.min(x0 + 1, nx - 1);
|
||||
const y1 = Math.min(y0 + 1, ny - 1);
|
||||
|
||||
const dx: number = sx - x0;
|
||||
const dy: number = sy - y0;
|
||||
const dx = sx - x0;
|
||||
const dy = sy - y0;
|
||||
|
||||
const j00: number = 3 * (y0 * nx + x0) + c;
|
||||
const j01: number = 3 * (y0 * nx + x1) + c;
|
||||
const j10: number = 3 * (y1 * nx + x0) + c;
|
||||
const j11: number = 3 * (y1 * nx + x1) + c;
|
||||
const j00 = 3 * (y0 * nx + x0) + c;
|
||||
const j01 = 3 * (y0 * nx + x1) + c;
|
||||
const j10 = 3 * (y1 * nx + x0) + c;
|
||||
const j11 = 3 * (y1 * nx + x1) + c;
|
||||
|
||||
const v00: number = inputImage[j00];
|
||||
const v01: number = inputImage[j01];
|
||||
const v10: number = inputImage[j10];
|
||||
const v11: number = inputImage[j11];
|
||||
const v00 = inputImage[j00] ?? 0;
|
||||
const v01 = inputImage[j01] ?? 0;
|
||||
const v10 = inputImage[j10] ?? 0;
|
||||
const v11 = inputImage[j11] ?? 0;
|
||||
|
||||
const v0: number = v00 * (1 - dx) + v01 * dx;
|
||||
const v1: number = v10 * (1 - dx) + v11 * dx;
|
||||
const v0 = v00 * (1 - dx) + v01 * dx;
|
||||
const v1 = v10 * (1 - dx) + v11 * dx;
|
||||
|
||||
const v: number = v0 * (1 - dy) + v1 * dy;
|
||||
const v = v0 * (1 - dy) + v1 * dy;
|
||||
|
||||
const v2: number = Math.min(Math.max(Math.round(v), 0), 255);
|
||||
const v2 = Math.min(Math.max(Math.round(v), 0), 255);
|
||||
|
||||
// createTensorWithDataList is dumb compared to reshape and
|
||||
// hence has to be given with one channel after another
|
||||
const i: number = y * nx3 + x + (c % 3) * 224 * 224;
|
||||
const i = y * nx3 + x + (c % 3) * 224 * 224;
|
||||
|
||||
result[i] = (v2 / 255 - mean[c]) / std[c];
|
||||
result[i] = (v2 / 255 - (mean[c] ?? 0)) / (std[c] ?? 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -119,13 +120,12 @@ const getRGBData = async (jpegFilePath: string) => {
|
|||
|
||||
const normalizeEmbedding = (embedding: Float32Array) => {
|
||||
let normalization = 0;
|
||||
for (let index = 0; index < embedding.length; index++) {
|
||||
normalization += embedding[index] * embedding[index];
|
||||
}
|
||||
for (const v of embedding) normalization += v * v;
|
||||
|
||||
const sqrtNormalization = Math.sqrt(normalization);
|
||||
for (let index = 0; index < embedding.length; index++) {
|
||||
embedding[index] = embedding[index] / sqrtNormalization;
|
||||
}
|
||||
for (let index = 0; index < embedding.length; index++)
|
||||
embedding[index] = ensure(embedding[index]) / sqrtNormalization;
|
||||
|
||||
return embedding;
|
||||
};
|
||||
|
||||
|
@ -134,11 +134,9 @@ const cachedCLIPTextSession = makeCachedInferenceSession(
|
|||
64173509 /* 61.2 MB */,
|
||||
);
|
||||
|
||||
let _tokenizer: Tokenizer = null;
|
||||
let _tokenizer: Tokenizer | undefined;
|
||||
const getTokenizer = () => {
|
||||
if (!_tokenizer) {
|
||||
_tokenizer = new Tokenizer();
|
||||
}
|
||||
if (!_tokenizer) _tokenizer = new Tokenizer();
|
||||
return _tokenizer;
|
||||
};
|
||||
|
||||
|
@ -150,7 +148,7 @@ export const clipTextEmbeddingIfAvailable = async (text: string) => {
|
|||
|
||||
// Don't wait for the download to complete
|
||||
if (typeof sessionOrStatus == "string") {
|
||||
console.log(
|
||||
log.info(
|
||||
"Ignoring CLIP text embedding request because model download is pending",
|
||||
);
|
||||
return undefined;
|
||||
|
@ -169,6 +167,6 @@ export const clipTextEmbeddingIfAvailable = async (text: string) => {
|
|||
() =>
|
||||
`onnx/clip text embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`,
|
||||
);
|
||||
const textEmbedding = results["output"].data as Float32Array;
|
||||
const textEmbedding = ensure(results.output).data as Float32Array;
|
||||
return normalizeEmbedding(textEmbedding);
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
*/
|
||||
import * as ort from "onnxruntime-node";
|
||||
import log from "../log";
|
||||
import { ensure } from "../utils/common";
|
||||
import { makeCachedInferenceSession } from "./ml";
|
||||
|
||||
const cachedFaceDetectionSession = makeCachedInferenceSession(
|
||||
|
@ -23,7 +24,7 @@ export const detectFaces = async (input: Float32Array) => {
|
|||
};
|
||||
const results = await session.run(feeds);
|
||||
log.debug(() => `onnx/yolo face detection took ${Date.now() - t} ms`);
|
||||
return results["output"].data;
|
||||
return ensure(results.output).data;
|
||||
};
|
||||
|
||||
const cachedFaceEmbeddingSession = makeCachedInferenceSession(
|
||||
|
@ -46,5 +47,6 @@ export const faceEmbedding = async (input: Float32Array) => {
|
|||
const results = await session.run(feeds);
|
||||
log.debug(() => `onnx/yolo face embedding took ${Date.now() - t} ms`);
|
||||
/* Need these model specific casts to extract and type the result */
|
||||
return (results.embeddings as unknown as any)["cpuData"] as Float32Array;
|
||||
return (results.embeddings as unknown as Record<string, unknown>)
|
||||
.cpuData as Float32Array;
|
||||
};
|
||||
|
|
|
@ -34,6 +34,7 @@ import { writeStream } from "../stream";
|
|||
* actively trigger a download until the returned function is called.
|
||||
*
|
||||
* @param modelName The name of the model to download.
|
||||
*
|
||||
* @param modelByteSize The size in bytes that we expect the model to have. If
|
||||
* the size of the downloaded model does not match the expected size, then we
|
||||
* will redownload it.
|
||||
|
@ -99,13 +100,15 @@ const downloadModel = async (saveLocation: string, name: string) => {
|
|||
// `mkdir -p` the directory where we want to save the model.
|
||||
const saveDir = path.dirname(saveLocation);
|
||||
await fs.mkdir(saveDir, { recursive: true });
|
||||
// Download
|
||||
// Download.
|
||||
log.info(`Downloading ML model from ${name}`);
|
||||
const url = `https://models.ente.io/${name}`;
|
||||
const res = await net.fetch(url);
|
||||
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
|
||||
// Save
|
||||
await writeStream(saveLocation, res.body);
|
||||
const body = res.body;
|
||||
if (!body) throw new Error(`Received an null response for ${url}`);
|
||||
// Save.
|
||||
await writeStream(saveLocation, body);
|
||||
log.info(`Downloaded CLIP model ${name}`);
|
||||
};
|
||||
|
||||
|
@ -114,9 +117,9 @@ const downloadModel = async (saveLocation: string, name: string) => {
|
|||
*/
|
||||
const createInferenceSession = async (modelPath: string) => {
|
||||
return await ort.InferenceSession.create(modelPath, {
|
||||
// Restrict the number of threads to 1
|
||||
// Restrict the number of threads to 1.
|
||||
intraOpNumThreads: 1,
|
||||
// Be more conservative with RAM usage
|
||||
// Be more conservative with RAM usage.
|
||||
enableCpuMemArena: false,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -9,20 +9,20 @@ import { watchStore } from "../stores/watch";
|
|||
* This is useful to reset state when the user logs out.
|
||||
*/
|
||||
export const clearStores = () => {
|
||||
uploadStatusStore.clear();
|
||||
safeStorageStore.clear();
|
||||
uploadStatusStore.clear();
|
||||
watchStore.clear();
|
||||
};
|
||||
|
||||
export const saveEncryptionKey = async (encryptionKey: string) => {
|
||||
const encryptedKey: Buffer = await safeStorage.encryptString(encryptionKey);
|
||||
export const saveEncryptionKey = (encryptionKey: string) => {
|
||||
const encryptedKey = safeStorage.encryptString(encryptionKey);
|
||||
const b64EncryptedKey = Buffer.from(encryptedKey).toString("base64");
|
||||
safeStorageStore.set("encryptionKey", b64EncryptedKey);
|
||||
};
|
||||
|
||||
export const encryptionKey = async (): Promise<string | undefined> => {
|
||||
export const encryptionKey = (): string | undefined => {
|
||||
const b64EncryptedKey = safeStorageStore.get("encryptionKey");
|
||||
if (!b64EncryptedKey) return undefined;
|
||||
const keyBuffer = Buffer.from(b64EncryptedKey, "base64");
|
||||
return await safeStorage.decryptString(keyBuffer);
|
||||
return safeStorage.decryptString(keyBuffer);
|
||||
};
|
||||
|
|
|
@ -1,116 +1,149 @@
|
|||
import StreamZip from "node-stream-zip";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { existsSync } from "original-fs";
|
||||
import path from "path";
|
||||
import { ElectronFile, type PendingUploads } from "../../types/ipc";
|
||||
import {
|
||||
uploadStatusStore,
|
||||
type UploadStatusStore,
|
||||
} from "../stores/upload-status";
|
||||
import { getElectronFile, getZipFileStream } from "./fs";
|
||||
import type { PendingUploads, ZipItem } from "../../types/ipc";
|
||||
import { uploadStatusStore } from "../stores/upload-status";
|
||||
|
||||
export const pendingUploads = async () => {
|
||||
const collectionName = uploadStatusStore.get("collectionName");
|
||||
const filePaths = validSavedPaths("files");
|
||||
const zipPaths = validSavedPaths("zips");
|
||||
|
||||
let files: ElectronFile[] = [];
|
||||
let type: PendingUploads["type"];
|
||||
|
||||
if (zipPaths.length) {
|
||||
type = "zips";
|
||||
for (const zipPath of zipPaths) {
|
||||
files = [
|
||||
...files,
|
||||
...(await getElectronFilesFromGoogleZip(zipPath)),
|
||||
];
|
||||
}
|
||||
const pendingFilePaths = new Set(filePaths);
|
||||
files = files.filter((file) => pendingFilePaths.has(file.path));
|
||||
} else if (filePaths.length) {
|
||||
type = "files";
|
||||
files = await Promise.all(filePaths.map(getElectronFile));
|
||||
}
|
||||
|
||||
return {
|
||||
files,
|
||||
collectionName,
|
||||
type,
|
||||
};
|
||||
};
|
||||
|
||||
export const validSavedPaths = (type: PendingUploads["type"]) => {
|
||||
const key = storeKey(type);
|
||||
const savedPaths = (uploadStatusStore.get(key) as string[]) ?? [];
|
||||
const paths = savedPaths.filter((p) => existsSync(p));
|
||||
uploadStatusStore.set(key, paths);
|
||||
return paths;
|
||||
};
|
||||
|
||||
export const setPendingUploadCollection = (collectionName: string) => {
|
||||
if (collectionName) uploadStatusStore.set("collectionName", collectionName);
|
||||
else uploadStatusStore.delete("collectionName");
|
||||
};
|
||||
|
||||
export const setPendingUploadFiles = (
|
||||
type: PendingUploads["type"],
|
||||
filePaths: string[],
|
||||
) => {
|
||||
const key = storeKey(type);
|
||||
if (filePaths) uploadStatusStore.set(key, filePaths);
|
||||
else uploadStatusStore.delete(key);
|
||||
};
|
||||
|
||||
const storeKey = (type: PendingUploads["type"]): keyof UploadStatusStore => {
|
||||
switch (type) {
|
||||
case "zips":
|
||||
return "zipPaths";
|
||||
case "files":
|
||||
return "filePaths";
|
||||
}
|
||||
};
|
||||
|
||||
export const getElectronFilesFromGoogleZip = async (filePath: string) => {
|
||||
const zip = new StreamZip.async({
|
||||
file: filePath,
|
||||
});
|
||||
const zipName = path.basename(filePath, ".zip");
|
||||
export const listZipItems = async (zipPath: string): Promise<ZipItem[]> => {
|
||||
const zip = new StreamZip.async({ file: zipPath });
|
||||
|
||||
const entries = await zip.entries();
|
||||
const files: ElectronFile[] = [];
|
||||
const entryNames: string[] = [];
|
||||
|
||||
for (const entry of Object.values(entries)) {
|
||||
const basename = path.basename(entry.name);
|
||||
if (entry.isFile && basename.length > 0 && basename[0] !== ".") {
|
||||
files.push(await getZipEntryAsElectronFile(zipName, zip, entry));
|
||||
// Ignore "hidden" files (files whose names begins with a dot).
|
||||
if (entry.isFile && !basename.startsWith(".")) {
|
||||
// `entry.name` is the path within the zip.
|
||||
entryNames.push(entry.name);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
await zip.close();
|
||||
|
||||
return entryNames.map((entryName) => [zipPath, entryName]);
|
||||
};
|
||||
|
||||
export async function getZipEntryAsElectronFile(
|
||||
zipName: string,
|
||||
zip: StreamZip.StreamZipAsync,
|
||||
entry: StreamZip.ZipEntry,
|
||||
): Promise<ElectronFile> {
|
||||
export const pathOrZipItemSize = async (
|
||||
pathOrZipItem: string | ZipItem,
|
||||
): Promise<number> => {
|
||||
if (typeof pathOrZipItem == "string") {
|
||||
const stat = await fs.stat(pathOrZipItem);
|
||||
return stat.size;
|
||||
} else {
|
||||
const [zipPath, entryName] = pathOrZipItem;
|
||||
const zip = new StreamZip.async({ file: zipPath });
|
||||
const entry = await zip.entry(entryName);
|
||||
if (!entry)
|
||||
throw new Error(
|
||||
`An entry with name ${entryName} does not exist in the zip file at ${zipPath}`,
|
||||
);
|
||||
const size = entry.size;
|
||||
await zip.close();
|
||||
return size;
|
||||
}
|
||||
};
|
||||
|
||||
export const pendingUploads = async (): Promise<PendingUploads | undefined> => {
|
||||
const collectionName = uploadStatusStore.get("collectionName") ?? undefined;
|
||||
|
||||
const allFilePaths = uploadStatusStore.get("filePaths") ?? [];
|
||||
const filePaths = allFilePaths.filter((f) => existsSync(f));
|
||||
|
||||
const allZipItems = uploadStatusStore.get("zipItems");
|
||||
let zipItems: typeof allZipItems;
|
||||
|
||||
// Migration code - May 2024. Remove after a bit.
|
||||
//
|
||||
// The older store formats will not have zipItems and instead will have
|
||||
// zipPaths. If we find such a case, read the zipPaths and enqueue all of
|
||||
// their files as zipItems in the result.
|
||||
//
|
||||
// This potentially can be cause us to try reuploading an already uploaded
|
||||
// file, but the dedup logic will kick in at that point so no harm will come
|
||||
// of it.
|
||||
if (allZipItems === undefined) {
|
||||
const allZipPaths = uploadStatusStore.get("filePaths") ?? [];
|
||||
const zipPaths = allZipPaths.filter((f) => existsSync(f));
|
||||
zipItems = [];
|
||||
for (const zip of zipPaths)
|
||||
zipItems = zipItems.concat(await listZipItems(zip));
|
||||
} else {
|
||||
zipItems = allZipItems.filter(([z]) => existsSync(z));
|
||||
}
|
||||
|
||||
if (filePaths.length == 0 && zipItems.length == 0) return undefined;
|
||||
|
||||
return {
|
||||
path: path
|
||||
.join(zipName, entry.name)
|
||||
.split(path.sep)
|
||||
.join(path.posix.sep),
|
||||
name: path.basename(entry.name),
|
||||
size: entry.size,
|
||||
lastModified: entry.time,
|
||||
stream: async () => {
|
||||
return await getZipFileStream(zip, entry.name);
|
||||
},
|
||||
blob: async () => {
|
||||
const buffer = await zip.entryData(entry.name);
|
||||
return new Blob([new Uint8Array(buffer)]);
|
||||
},
|
||||
arrayBuffer: async () => {
|
||||
const buffer = await zip.entryData(entry.name);
|
||||
return new Uint8Array(buffer);
|
||||
},
|
||||
collectionName,
|
||||
filePaths,
|
||||
zipItems,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* [Note: Missing values in electron-store]
|
||||
*
|
||||
* Suppose we were to create a store like this:
|
||||
*
|
||||
* const store = new Store({
|
||||
* schema: {
|
||||
* foo: { type: "string" },
|
||||
* bars: { type: "array", items: { type: "string" } },
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* If we fetch `store.get("foo")` or `store.get("bars")`, we get `undefined`.
|
||||
* But if we try to set these back to `undefined`, say `store.set("foo",
|
||||
* someUndefValue)`, we get asked to
|
||||
*
|
||||
* TypeError: Use `delete()` to clear values
|
||||
*
|
||||
* This happens even if we do bulk object updates, e.g. with a JS object that
|
||||
* has undefined keys:
|
||||
*
|
||||
* > TypeError: Setting a value of type `undefined` for key `collectionName` is
|
||||
* > not allowed as it's not supported by JSON
|
||||
*
|
||||
* So what should the TypeScript type for "foo" be?
|
||||
*
|
||||
* If it is were to not include the possibility of `undefined`, then the type
|
||||
* would lie because `store.get("foo")` can indeed be `undefined. But if we were
|
||||
* to include the possibility of `undefined`, then trying to `store.set("foo",
|
||||
* someUndefValue)` will throw.
|
||||
*
|
||||
* The approach we take is to rely on false-y values (empty strings and empty
|
||||
* arrays) to indicate missing values, and then converting those to `undefined`
|
||||
* when reading from the store, and converting `undefined` to the corresponding
|
||||
* false-y value when writing.
|
||||
*/
|
||||
export const setPendingUploads = ({
|
||||
collectionName,
|
||||
filePaths,
|
||||
zipItems,
|
||||
}: PendingUploads) => {
|
||||
uploadStatusStore.set({
|
||||
collectionName: collectionName ?? "",
|
||||
filePaths: filePaths,
|
||||
zipItems: zipItems,
|
||||
});
|
||||
};
|
||||
|
||||
export const markUploadedFiles = (paths: string[]) => {
|
||||
const existing = uploadStatusStore.get("filePaths") ?? [];
|
||||
const updated = existing.filter((p) => !paths.includes(p));
|
||||
uploadStatusStore.set("filePaths", updated);
|
||||
};
|
||||
|
||||
export const markUploadedZipItems = (
|
||||
items: [zipPath: string, entryName: string][],
|
||||
) => {
|
||||
const existing = uploadStatusStore.get("zipItems") ?? [];
|
||||
const updated = existing.filter(
|
||||
(z) => !items.some((e) => z[0] == e[0] && z[1] == e[1]),
|
||||
);
|
||||
uploadStatusStore.set("zipItems", updated);
|
||||
};
|
||||
|
||||
export const clearPendingUploads = () => uploadStatusStore.clear();
|
||||
|
|
|
@ -3,9 +3,10 @@ import { BrowserWindow } from "electron/main";
|
|||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { FolderWatch, type CollectionMapping } from "../../types/ipc";
|
||||
import { fsIsDir } from "../fs";
|
||||
import log from "../log";
|
||||
import { watchStore } from "../stores/watch";
|
||||
import { posixPath } from "../utils/electron";
|
||||
import { fsIsDir } from "./fs";
|
||||
|
||||
/**
|
||||
* Create and return a new file system watcher.
|
||||
|
@ -34,8 +35,8 @@ export const createWatcher = (mainWindow: BrowserWindow) => {
|
|||
return watcher;
|
||||
};
|
||||
|
||||
const eventData = (path: string): [string, FolderWatch] => {
|
||||
path = posixPath(path);
|
||||
const eventData = (platformPath: string): [string, FolderWatch] => {
|
||||
const path = posixPath(platformPath);
|
||||
|
||||
const watch = folderWatches().find((watch) =>
|
||||
path.startsWith(watch.folderPath + "/"),
|
||||
|
@ -46,23 +47,15 @@ const eventData = (path: string): [string, FolderWatch] => {
|
|||
return [path, watch];
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a file system {@link filePath} that uses the local system specific
|
||||
* path separators into a path that uses POSIX file separators.
|
||||
*/
|
||||
const posixPath = (filePath: string) =>
|
||||
filePath.split(path.sep).join(path.posix.sep);
|
||||
|
||||
export const watchGet = (watcher: FSWatcher) => {
|
||||
const [valid, deleted] = folderWatches().reduce(
|
||||
([valid, deleted], watch) => {
|
||||
(fsIsDir(watch.folderPath) ? valid : deleted).push(watch);
|
||||
return [valid, deleted];
|
||||
},
|
||||
[[], []],
|
||||
);
|
||||
if (deleted.length) {
|
||||
for (const watch of deleted) watchRemove(watcher, watch.folderPath);
|
||||
export const watchGet = async (watcher: FSWatcher): Promise<FolderWatch[]> => {
|
||||
const valid: FolderWatch[] = [];
|
||||
const deletedPaths: string[] = [];
|
||||
for (const watch of folderWatches()) {
|
||||
if (await fsIsDir(watch.folderPath)) valid.push(watch);
|
||||
else deletedPaths.push(watch.folderPath);
|
||||
}
|
||||
if (deletedPaths.length) {
|
||||
await Promise.all(deletedPaths.map((p) => watchRemove(watcher, p)));
|
||||
setFolderWatches(valid);
|
||||
}
|
||||
return valid;
|
||||
|
@ -80,7 +73,7 @@ export const watchAdd = async (
|
|||
) => {
|
||||
const watches = folderWatches();
|
||||
|
||||
if (!fsIsDir(folderPath))
|
||||
if (!(await fsIsDir(folderPath)))
|
||||
throw new Error(
|
||||
`Attempting to add a folder watch for a folder path ${folderPath} that is not an existing directory`,
|
||||
);
|
||||
|
@ -104,7 +97,7 @@ export const watchAdd = async (
|
|||
return watches;
|
||||
};
|
||||
|
||||
export const watchRemove = async (watcher: FSWatcher, folderPath: string) => {
|
||||
export const watchRemove = (watcher: FSWatcher, folderPath: string) => {
|
||||
const watches = folderWatches();
|
||||
const filtered = watches.filter((watch) => watch.folderPath != folderPath);
|
||||
if (watches.length == filtered.length)
|
||||
|
@ -157,3 +150,7 @@ export const watchFindFiles = async (dirPath: string) => {
|
|||
}
|
||||
return paths;
|
||||
};
|
||||
|
||||
export const watchReset = (watcher: FSWatcher) => {
|
||||
watcher.unwatch(folderWatches().map((watch) => watch.folderPath));
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Store, { Schema } from "electron-store";
|
||||
|
||||
interface SafeStorageStore {
|
||||
encryptionKey: string;
|
||||
encryptionKey?: string;
|
||||
}
|
||||
|
||||
const safeStorageSchema: Schema<SafeStorageStore> = {
|
||||
|
|
|
@ -1,27 +1,51 @@
|
|||
import Store, { Schema } from "electron-store";
|
||||
|
||||
export interface UploadStatusStore {
|
||||
filePaths: string[];
|
||||
zipPaths: string[];
|
||||
collectionName: string;
|
||||
/**
|
||||
* The collection to which we're uploading, or the root collection.
|
||||
*
|
||||
* Not all pending uploads will have an associated collection.
|
||||
*/
|
||||
collectionName?: string;
|
||||
/**
|
||||
* Paths to regular files that are pending upload.
|
||||
*/
|
||||
filePaths?: string[];
|
||||
/**
|
||||
* Each item is the path to a zip file and the name of an entry within it.
|
||||
*/
|
||||
zipItems?: [zipPath: string, entryName: string][];
|
||||
/**
|
||||
* @deprecated Legacy paths to zip files, now subsumed into zipItems.
|
||||
*/
|
||||
zipPaths?: string[];
|
||||
}
|
||||
|
||||
const uploadStatusSchema: Schema<UploadStatusStore> = {
|
||||
collectionName: {
|
||||
type: "string",
|
||||
},
|
||||
filePaths: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
zipItems: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
zipPaths: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
collectionName: {
|
||||
type: "string",
|
||||
},
|
||||
};
|
||||
|
||||
export const uploadStatusStore = new Store({
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Store, { Schema } from "electron-store";
|
||||
|
||||
interface UserPreferences {
|
||||
hideDockIcon: boolean;
|
||||
hideDockIcon?: boolean;
|
||||
skipAppVersion?: string;
|
||||
muteUpdateNotificationVersion?: string;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { type FolderWatch } from "../../types/ipc";
|
|||
import log from "../log";
|
||||
|
||||
interface WatchStore {
|
||||
mappings: FolderWatchWithLegacyFields[];
|
||||
mappings?: FolderWatchWithLegacyFields[];
|
||||
}
|
||||
|
||||
type FolderWatchWithLegacyFields = FolderWatch & {
|
||||
|
@ -54,8 +54,12 @@ export const watchStore = new Store({
|
|||
*/
|
||||
export const migrateLegacyWatchStoreIfNeeded = () => {
|
||||
let needsUpdate = false;
|
||||
const watches = watchStore.get("mappings")?.map((watch) => {
|
||||
const updatedWatches = [];
|
||||
for (const watch of watchStore.get("mappings") ?? []) {
|
||||
let collectionMapping = watch.collectionMapping;
|
||||
// The required type defines the latest schema, but before migration
|
||||
// this'll be undefined, so tell ESLint to calm down.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!collectionMapping) {
|
||||
collectionMapping = watch.uploadStrategy == 1 ? "parent" : "root";
|
||||
needsUpdate = true;
|
||||
|
@ -64,10 +68,10 @@ export const migrateLegacyWatchStoreIfNeeded = () => {
|
|||
delete watch.rootFolderName;
|
||||
needsUpdate = true;
|
||||
}
|
||||
return { ...watch, collectionMapping };
|
||||
});
|
||||
updatedWatches.push({ ...watch, collectionMapping });
|
||||
}
|
||||
if (needsUpdate) {
|
||||
watchStore.set("mappings", watches);
|
||||
watchStore.set("mappings", updatedWatches);
|
||||
log.info("Migrated legacy watch store data to new schema");
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,11 +2,14 @@
|
|||
* @file stream data to-from renderer using a custom protocol handler.
|
||||
*/
|
||||
import { net, protocol } from "electron/main";
|
||||
import StreamZip from "node-stream-zip";
|
||||
import { createWriteStream, existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { Readable } from "node:stream";
|
||||
import { ReadableStream } from "node:stream/web";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import log from "./log";
|
||||
import { ensure } from "./utils/common";
|
||||
|
||||
/**
|
||||
* Register a protocol handler that we use for streaming large files between the
|
||||
|
@ -34,19 +37,18 @@ export const registerStreamProtocol = () => {
|
|||
protocol.handle("stream", async (request: Request) => {
|
||||
const url = request.url;
|
||||
// The request URL contains the command to run as the host, and the
|
||||
// pathname of the file as the path. For example,
|
||||
//
|
||||
// stream://write/path/to/file
|
||||
// host-pathname-----
|
||||
//
|
||||
const { host, pathname } = new URL(url);
|
||||
// Convert e.g. "%20" to spaces.
|
||||
const path = decodeURIComponent(pathname);
|
||||
// pathname of the file(s) as the search params.
|
||||
const { host, searchParams } = new URL(url);
|
||||
switch (host) {
|
||||
case "read":
|
||||
return handleRead(path);
|
||||
return handleRead(ensure(searchParams.get("path")));
|
||||
case "read-zip":
|
||||
return handleReadZip(
|
||||
ensure(searchParams.get("zipPath")),
|
||||
ensure(searchParams.get("entryName")),
|
||||
);
|
||||
case "write":
|
||||
return handleWrite(path, request);
|
||||
return handleWrite(ensure(searchParams.get("path")), request);
|
||||
default:
|
||||
return new Response("", { status: 404 });
|
||||
}
|
||||
|
@ -57,10 +59,17 @@ const handleRead = async (path: string) => {
|
|||
try {
|
||||
const res = await net.fetch(pathToFileURL(path).toString());
|
||||
if (res.ok) {
|
||||
// net.fetch defaults to text/plain, which might be fine
|
||||
// in practice, but as an extra precaution indicate that
|
||||
// this is binary data.
|
||||
res.headers.set("Content-Type", "application/octet-stream");
|
||||
// net.fetch already seems to add "Content-Type" and "Last-Modified"
|
||||
// headers, but I couldn't find documentation for this. In any case,
|
||||
// since we already are stat-ting the file for the "Content-Length",
|
||||
// we explicitly add the "X-Last-Modified-Ms" too,
|
||||
//
|
||||
// 1. Guaranteeing its presence,
|
||||
//
|
||||
// 2. Having it be in the exact format we want (no string <-> date
|
||||
// conversions),
|
||||
//
|
||||
// 3. Retaining milliseconds.
|
||||
|
||||
const stat = await fs.stat(path);
|
||||
|
||||
|
@ -75,7 +84,54 @@ const handleRead = async (path: string) => {
|
|||
return res;
|
||||
} catch (e) {
|
||||
log.error(`Failed to read stream at ${path}`, e);
|
||||
return new Response(`Failed to read stream: ${e.message}`, {
|
||||
return new Response(`Failed to read stream: ${String(e)}`, {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleReadZip = async (zipPath: string, entryName: string) => {
|
||||
try {
|
||||
const zip = new StreamZip.async({ file: zipPath });
|
||||
const entry = await zip.entry(entryName);
|
||||
if (!entry) return new Response("", { status: 404 });
|
||||
|
||||
// This returns an "old style" NodeJS.ReadableStream.
|
||||
const stream = await zip.stream(entry);
|
||||
// Convert it into a new style NodeJS.Readable.
|
||||
const nodeReadable = new Readable().wrap(stream);
|
||||
// Then convert it into a Web stream.
|
||||
const webReadableStreamAny = Readable.toWeb(nodeReadable);
|
||||
// However, we get a ReadableStream<any> now. This doesn't go into the
|
||||
// `BodyInit` expected by the Response constructor, which wants a
|
||||
// ReadableStream<Uint8Array>. Force a cast.
|
||||
const webReadableStream =
|
||||
webReadableStreamAny as ReadableStream<Uint8Array>;
|
||||
|
||||
// Close the zip handle when the underlying stream closes.
|
||||
stream.on("end", () => void zip.close());
|
||||
|
||||
return new Response(webReadableStream, {
|
||||
headers: {
|
||||
// We don't know the exact type, but it doesn't really matter,
|
||||
// just set it to a generic binary content-type so that the
|
||||
// browser doesn't tinker with it thinking of it as text.
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Length": `${entry.size}`,
|
||||
// While it is documented that entry.time is the modification
|
||||
// time, the units are not mentioned. By seeing the source code,
|
||||
// we can verify that it is indeed epoch milliseconds. See
|
||||
// `parseZipTime` in the node-stream-zip source,
|
||||
// https://github.com/antelle/node-stream-zip/blob/master/node_stream_zip.js
|
||||
"X-Last-Modified-Ms": `${entry.time}`,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
log.error(
|
||||
`Failed to read entry ${entryName} from zip file at ${zipPath}`,
|
||||
e,
|
||||
);
|
||||
return new Response(`Failed to read stream: ${String(e)}`, {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
|
@ -83,11 +139,11 @@ const handleRead = async (path: string) => {
|
|||
|
||||
const handleWrite = async (path: string, request: Request) => {
|
||||
try {
|
||||
await writeStream(path, request.body);
|
||||
await writeStream(path, ensure(request.body));
|
||||
return new Response("", { status: 200 });
|
||||
} catch (e) {
|
||||
log.error(`Failed to write stream to ${path}`, e);
|
||||
return new Response(`Failed to write stream: ${e.message}`, {
|
||||
return new Response(`Failed to write stream: ${String(e)}`, {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
|
@ -99,59 +155,29 @@ const handleWrite = async (path: string, request: Request) => {
|
|||
* The returned promise resolves when the write completes.
|
||||
*
|
||||
* @param filePath The local filesystem path where the file should be written.
|
||||
* @param readableStream A [web
|
||||
* ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
|
||||
*
|
||||
* @param readableStream A web
|
||||
* [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).
|
||||
*/
|
||||
export const writeStream = (filePath: string, readableStream: ReadableStream) =>
|
||||
writeNodeStream(filePath, convertWebReadableStreamToNode(readableStream));
|
||||
writeNodeStream(filePath, Readable.fromWeb(readableStream));
|
||||
|
||||
/**
|
||||
* Convert a Web ReadableStream into a Node.js ReadableStream
|
||||
*
|
||||
* This can be used to, for example, write a ReadableStream obtained via
|
||||
* `net.fetch` into a file using the Node.js `fs` APIs
|
||||
*/
|
||||
const convertWebReadableStreamToNode = (readableStream: ReadableStream) => {
|
||||
const reader = readableStream.getReader();
|
||||
const rs = new Readable();
|
||||
|
||||
rs._read = async () => {
|
||||
try {
|
||||
const result = await reader.read();
|
||||
|
||||
if (!result.done) {
|
||||
rs.push(Buffer.from(result.value));
|
||||
} else {
|
||||
rs.push(null);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
rs.emit("error", e);
|
||||
}
|
||||
};
|
||||
|
||||
return rs;
|
||||
};
|
||||
|
||||
const writeNodeStream = async (
|
||||
filePath: string,
|
||||
fileStream: NodeJS.ReadableStream,
|
||||
) => {
|
||||
const writeNodeStream = async (filePath: string, fileStream: Readable) => {
|
||||
const writeable = createWriteStream(filePath);
|
||||
|
||||
fileStream.on("error", (error) => {
|
||||
writeable.destroy(error); // Close the writable stream with an error
|
||||
fileStream.on("error", (err) => {
|
||||
writeable.destroy(err); // Close the writable stream with an error
|
||||
});
|
||||
|
||||
fileStream.pipe(writeable);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
writeable.on("finish", resolve);
|
||||
writeable.on("error", async (e: unknown) => {
|
||||
writeable.on("error", (err) => {
|
||||
if (existsSync(filePath)) {
|
||||
await fs.unlink(filePath);
|
||||
void fs.unlink(filePath);
|
||||
}
|
||||
reject(e);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
import { app } from "electron/main";
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* Our very own directory within the system temp directory. Go crazy, but
|
||||
* remember to clean up, especially in exception handlers.
|
||||
*/
|
||||
const enteTempDirPath = async () => {
|
||||
const result = path.join(app.getPath("temp"), "ente");
|
||||
await fs.mkdir(result, { recursive: true });
|
||||
return result;
|
||||
};
|
||||
|
||||
/** Generate a random string suitable for being used as a file name prefix */
|
||||
const randomPrefix = () => {
|
||||
const alphabet =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
let result = "";
|
||||
for (let i = 0; i < 10; i++)
|
||||
result += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the path to a temporary file with the given {@link suffix}.
|
||||
*
|
||||
* The function returns the path to a file in the system temp directory (in an
|
||||
* Ente specific folder therin) with a random prefix and an (optional)
|
||||
* {@link extension}.
|
||||
*
|
||||
* It ensures that there is no existing item with the same name already.
|
||||
*
|
||||
* Use {@link deleteTempFile} to remove this file when you're done.
|
||||
*/
|
||||
export const makeTempFilePath = async (extension?: string) => {
|
||||
const tempDir = await enteTempDirPath();
|
||||
const suffix = extension ? "." + extension : "";
|
||||
let result: string;
|
||||
do {
|
||||
result = path.join(tempDir, randomPrefix() + suffix);
|
||||
} while (existsSync(result));
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a temporary file at the given path if it exists.
|
||||
*
|
||||
* This is the same as a vanilla {@link fs.rm}, except it first checks that the
|
||||
* given path is within the Ente specific directory in the system temp
|
||||
* directory. This acts as an additional safety check.
|
||||
*
|
||||
* @param tempFilePath The path to the temporary file to delete. This path
|
||||
* should've been previously created using {@link makeTempFilePath}.
|
||||
*/
|
||||
export const deleteTempFile = async (tempFilePath: string) => {
|
||||
const tempDir = await enteTempDirPath();
|
||||
if (!tempFilePath.startsWith(tempDir))
|
||||
throw new Error(`Attempting to delete a non-temp file ${tempFilePath}`);
|
||||
await fs.rm(tempFilePath, { force: true });
|
||||
};
|
|
@ -1,10 +1,19 @@
|
|||
/**
|
||||
* @file grab bag of utitity functions.
|
||||
* @file grab bag of utility functions.
|
||||
*
|
||||
* Many of these are verbatim copies of functions from web code since there
|
||||
* isn't currently a common package that both of them share.
|
||||
* These are verbatim copies of functions from web code since there isn't
|
||||
* currently a common package that both of them share.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Throw an exception if the given value is `null` or `undefined`.
|
||||
*/
|
||||
export const ensure = <T>(v: T | null | undefined): T => {
|
||||
if (v === null) throw new Error("Required value was null");
|
||||
if (v === undefined) throw new Error("Required value was not found");
|
||||
return v;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wait for {@link ms} milliseconds
|
||||
*
|
|
@ -1,14 +1,35 @@
|
|||
import shellescape from "any-shell-escape";
|
||||
import { shell } from "electron"; /* TODO(MR): Why is this not in /main? */
|
||||
import { app } from "electron/main";
|
||||
import { exec } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import log from "./log";
|
||||
import log from "../log";
|
||||
|
||||
/** `true` if the app is running in development mode. */
|
||||
export const isDev = !app.isPackaged;
|
||||
|
||||
/**
|
||||
* Convert a file system {@link platformPath} that uses the local system
|
||||
* specific path separators into a path that uses POSIX file separators.
|
||||
*
|
||||
* For all paths that we persist or pass over the IPC boundary, we always use
|
||||
* POSIX paths, even on Windows.
|
||||
*
|
||||
* Windows recognizes both forward and backslashes. This also works with drive
|
||||
* names. c:\foo\bar and c:/foo/bar are both valid.
|
||||
*
|
||||
* > Almost all paths passed to Windows APIs are normalized. During
|
||||
* > normalization, Windows performs the following steps: ... All forward
|
||||
* > slashes (/) are converted into the standard Windows separator, the back
|
||||
* > slash (\).
|
||||
* >
|
||||
* > https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
|
||||
*/
|
||||
export const posixPath = (platformPath: string) =>
|
||||
path.sep == path.posix.sep
|
||||
? platformPath
|
||||
: platformPath.split(path.sep).join(path.posix.sep);
|
||||
|
||||
/**
|
||||
* Run a shell command asynchronously.
|
||||
*
|
||||
|
@ -33,49 +54,11 @@ export const execAsync = (command: string | string[]) => {
|
|||
? shellescape(command)
|
||||
: command;
|
||||
const startTime = Date.now();
|
||||
log.debug(() => `Running shell command: ${escapedCommand}`);
|
||||
const result = execAsync_(escapedCommand);
|
||||
log.debug(
|
||||
() =>
|
||||
`Completed in ${Math.round(Date.now() - startTime)} ms (${escapedCommand})`,
|
||||
() => `${escapedCommand} (${Math.round(Date.now() - startTime)} ms)`,
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
const execAsync_ = promisify(exec);
|
||||
|
||||
/**
|
||||
* Open the given {@link dirPath} in the system's folder viewer.
|
||||
*
|
||||
* For example, on macOS this'll open {@link dirPath} in Finder.
|
||||
*/
|
||||
export const openDirectory = async (dirPath: string) => {
|
||||
const res = await shell.openPath(path.normalize(dirPath));
|
||||
// shell.openPath resolves with a string containing the error message
|
||||
// corresponding to the failure if a failure occurred, otherwise "".
|
||||
if (res) throw new Error(`Failed to open directory ${dirPath}: res`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the app's log directory in the system's folder viewer.
|
||||
*
|
||||
* @see {@link openDirectory}
|
||||
*/
|
||||
export const openLogDirectory = () => openDirectory(logDirectoryPath());
|
||||
|
||||
/**
|
||||
* Return the path where the logs for the app are saved.
|
||||
*
|
||||
* [Note: Electron app paths]
|
||||
*
|
||||
* By default, these paths are at the following locations:
|
||||
*
|
||||
* - macOS: `~/Library/Application Support/ente`
|
||||
* - Linux: `~/.config/ente`
|
||||
* - Windows: `%APPDATA%`, e.g. `C:\Users\<username>\AppData\Local\ente`
|
||||
* - Windows: C:\Users\<you>\AppData\Local\<Your App Name>
|
||||
*
|
||||
* https://www.electronjs.org/docs/latest/api/app
|
||||
*
|
||||
*/
|
||||
const logDirectoryPath = () => app.getPath("logs");
|
125
desktop/src/main/utils/temp.ts
Normal file
125
desktop/src/main/utils/temp.ts
Normal file
|
@ -0,0 +1,125 @@
|
|||
import { app } from "electron/main";
|
||||
import StreamZip from "node-stream-zip";
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { ZipItem } from "../../types/ipc";
|
||||
import { ensure } from "./common";
|
||||
|
||||
/**
|
||||
* Our very own directory within the system temp directory. Go crazy, but
|
||||
* remember to clean up, especially in exception handlers.
|
||||
*/
|
||||
const enteTempDirPath = async () => {
|
||||
const result = path.join(app.getPath("temp"), "ente");
|
||||
await fs.mkdir(result, { recursive: true });
|
||||
return result;
|
||||
};
|
||||
|
||||
/** Generate a random string suitable for being used as a file name prefix */
|
||||
const randomPrefix = () => {
|
||||
const ch = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const randomChar = () => ensure(ch[Math.floor(Math.random() * ch.length)]);
|
||||
|
||||
return Array(10).fill("").map(randomChar).join("");
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the path to a temporary file with the given {@link suffix}.
|
||||
*
|
||||
* The function returns the path to a file in the system temp directory (in an
|
||||
* Ente specific folder therin) with a random prefix and an (optional)
|
||||
* {@link extension}.
|
||||
*
|
||||
* It ensures that there is no existing item with the same name already.
|
||||
*
|
||||
* Use {@link deleteTempFile} to remove this file when you're done.
|
||||
*/
|
||||
export const makeTempFilePath = async (extension?: string) => {
|
||||
const tempDir = await enteTempDirPath();
|
||||
const suffix = extension ? "." + extension : "";
|
||||
let result: string;
|
||||
do {
|
||||
result = path.join(tempDir, randomPrefix() + suffix);
|
||||
} while (existsSync(result));
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a temporary file at the given path if it exists.
|
||||
*
|
||||
* This is the same as a vanilla {@link fs.rm}, except it first checks that the
|
||||
* given path is within the Ente specific directory in the system temp
|
||||
* directory. This acts as an additional safety check.
|
||||
*
|
||||
* @param tempFilePath The path to the temporary file to delete. This path
|
||||
* should've been previously created using {@link makeTempFilePath}.
|
||||
*/
|
||||
export const deleteTempFile = async (tempFilePath: string) => {
|
||||
const tempDir = await enteTempDirPath();
|
||||
if (!tempFilePath.startsWith(tempDir))
|
||||
throw new Error(`Attempting to delete a non-temp file ${tempFilePath}`);
|
||||
await fs.rm(tempFilePath, { force: true });
|
||||
};
|
||||
|
||||
/** The result of {@link makeFileForDataOrPathOrZipItem}. */
|
||||
interface FileForDataOrPathOrZipItem {
|
||||
/**
|
||||
* The path to the file (possibly temporary).
|
||||
*/
|
||||
path: string;
|
||||
/**
|
||||
* `true` if {@link path} points to a temporary file which should be deleted
|
||||
* once we are done processing.
|
||||
*/
|
||||
isFileTemporary: boolean;
|
||||
/**
|
||||
* A function that can be called to actually write the contents of the
|
||||
* source `Uint8Array | string | ZipItem` into the file at {@link path}.
|
||||
*
|
||||
* It will do nothing in the case when the source is already a path. In the
|
||||
* other two cases this function will write the data or zip item into the
|
||||
* file at {@link path}.
|
||||
*/
|
||||
writeToTemporaryFile: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the path to a file, a boolean indicating if this is a temporary path
|
||||
* that needs to be deleted after processing, and a function to write the given
|
||||
* {@link dataOrPathOrZipItem} into that temporary file if needed.
|
||||
*
|
||||
* @param dataOrPathOrZipItem The contents of the file, or the path to an
|
||||
* existing file, or a (path to a zip file, name of an entry within that zip
|
||||
* file) tuple.
|
||||
*/
|
||||
export const makeFileForDataOrPathOrZipItem = async (
|
||||
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
|
||||
): Promise<FileForDataOrPathOrZipItem> => {
|
||||
let path: string;
|
||||
let isFileTemporary: boolean;
|
||||
let writeToTemporaryFile = async () => {
|
||||
/* no-op */
|
||||
};
|
||||
|
||||
if (typeof dataOrPathOrZipItem == "string") {
|
||||
path = dataOrPathOrZipItem;
|
||||
isFileTemporary = false;
|
||||
} else {
|
||||
path = await makeTempFilePath();
|
||||
isFileTemporary = true;
|
||||
if (dataOrPathOrZipItem instanceof Uint8Array) {
|
||||
writeToTemporaryFile = () =>
|
||||
fs.writeFile(path, dataOrPathOrZipItem);
|
||||
} else {
|
||||
writeToTemporaryFile = async () => {
|
||||
const [zipPath, entryName] = dataOrPathOrZipItem;
|
||||
const zip = new StreamZip.async({ file: zipPath });
|
||||
await zip.extract(entryName, path);
|
||||
await zip.close();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { path, isFileTemporary, writeToTemporaryFile };
|
||||
};
|
|
@ -37,37 +37,37 @@
|
|||
* - [main] desktop/src/main/ipc.ts contains impl
|
||||
*/
|
||||
|
||||
import { contextBridge, ipcRenderer } from "electron/renderer";
|
||||
import { contextBridge, ipcRenderer, webUtils } from "electron/renderer";
|
||||
|
||||
// While we can't import other code, we can import types since they're just
|
||||
// needed when compiling and will not be needed or looked around for at runtime.
|
||||
import type {
|
||||
AppUpdate,
|
||||
CollectionMapping,
|
||||
ElectronFile,
|
||||
FolderWatch,
|
||||
PendingUploads,
|
||||
ZipItem,
|
||||
} from "./types/ipc";
|
||||
|
||||
// - General
|
||||
|
||||
const appVersion = (): Promise<string> => ipcRenderer.invoke("appVersion");
|
||||
const appVersion = () => ipcRenderer.invoke("appVersion");
|
||||
|
||||
const logToDisk = (message: string): void =>
|
||||
ipcRenderer.send("logToDisk", message);
|
||||
|
||||
const openDirectory = (dirPath: string): Promise<void> =>
|
||||
const openDirectory = (dirPath: string) =>
|
||||
ipcRenderer.invoke("openDirectory", dirPath);
|
||||
|
||||
const openLogDirectory = (): Promise<void> =>
|
||||
ipcRenderer.invoke("openLogDirectory");
|
||||
const openLogDirectory = () => ipcRenderer.invoke("openLogDirectory");
|
||||
|
||||
const selectDirectory = () => ipcRenderer.invoke("selectDirectory");
|
||||
|
||||
const clearStores = () => ipcRenderer.send("clearStores");
|
||||
|
||||
const encryptionKey = (): Promise<string | undefined> =>
|
||||
ipcRenderer.invoke("encryptionKey");
|
||||
const encryptionKey = () => ipcRenderer.invoke("encryptionKey");
|
||||
|
||||
const saveEncryptionKey = (encryptionKey: string): Promise<void> =>
|
||||
const saveEncryptionKey = (encryptionKey: string) =>
|
||||
ipcRenderer.invoke("saveEncryptionKey", encryptionKey);
|
||||
|
||||
const onMainWindowFocus = (cb?: () => void) => {
|
||||
|
@ -99,121 +99,90 @@ const skipAppUpdate = (version: string) => {
|
|||
|
||||
// - FS
|
||||
|
||||
const fsExists = (path: string): Promise<boolean> =>
|
||||
ipcRenderer.invoke("fsExists", path);
|
||||
const fsExists = (path: string) => ipcRenderer.invoke("fsExists", path);
|
||||
|
||||
const fsMkdirIfNeeded = (dirPath: string): Promise<void> =>
|
||||
const fsMkdirIfNeeded = (dirPath: string) =>
|
||||
ipcRenderer.invoke("fsMkdirIfNeeded", dirPath);
|
||||
|
||||
const fsRename = (oldPath: string, newPath: string): Promise<void> =>
|
||||
const fsRename = (oldPath: string, newPath: string) =>
|
||||
ipcRenderer.invoke("fsRename", oldPath, newPath);
|
||||
|
||||
const fsRmdir = (path: string): Promise<void> =>
|
||||
ipcRenderer.invoke("fsRmdir", path);
|
||||
const fsRmdir = (path: string) => ipcRenderer.invoke("fsRmdir", path);
|
||||
|
||||
const fsRm = (path: string): Promise<void> => ipcRenderer.invoke("fsRm", path);
|
||||
const fsRm = (path: string) => ipcRenderer.invoke("fsRm", path);
|
||||
|
||||
const fsReadTextFile = (path: string): Promise<string> =>
|
||||
const fsReadTextFile = (path: string) =>
|
||||
ipcRenderer.invoke("fsReadTextFile", path);
|
||||
|
||||
const fsWriteFile = (path: string, contents: string): Promise<void> =>
|
||||
const fsWriteFile = (path: string, contents: string) =>
|
||||
ipcRenderer.invoke("fsWriteFile", path, contents);
|
||||
|
||||
const fsIsDir = (dirPath: string): Promise<boolean> =>
|
||||
ipcRenderer.invoke("fsIsDir", dirPath);
|
||||
|
||||
const fsSize = (path: string): Promise<number> =>
|
||||
ipcRenderer.invoke("fsSize", path);
|
||||
const fsIsDir = (dirPath: string) => ipcRenderer.invoke("fsIsDir", dirPath);
|
||||
|
||||
// - Conversion
|
||||
|
||||
const convertToJPEG = (imageData: Uint8Array): Promise<Uint8Array> =>
|
||||
const convertToJPEG = (imageData: Uint8Array) =>
|
||||
ipcRenderer.invoke("convertToJPEG", imageData);
|
||||
|
||||
const generateImageThumbnail = (
|
||||
dataOrPath: Uint8Array | string,
|
||||
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
|
||||
maxDimension: number,
|
||||
maxSize: number,
|
||||
): Promise<Uint8Array> =>
|
||||
) =>
|
||||
ipcRenderer.invoke(
|
||||
"generateImageThumbnail",
|
||||
dataOrPath,
|
||||
dataOrPathOrZipItem,
|
||||
maxDimension,
|
||||
maxSize,
|
||||
);
|
||||
|
||||
const ffmpegExec = (
|
||||
command: string[],
|
||||
dataOrPath: Uint8Array | string,
|
||||
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
|
||||
outputFileExtension: string,
|
||||
timeoutMS: number,
|
||||
): Promise<Uint8Array> =>
|
||||
) =>
|
||||
ipcRenderer.invoke(
|
||||
"ffmpegExec",
|
||||
command,
|
||||
dataOrPath,
|
||||
dataOrPathOrZipItem,
|
||||
outputFileExtension,
|
||||
timeoutMS,
|
||||
);
|
||||
|
||||
// - ML
|
||||
|
||||
const clipImageEmbedding = (jpegImageData: Uint8Array): Promise<Float32Array> =>
|
||||
const clipImageEmbedding = (jpegImageData: Uint8Array) =>
|
||||
ipcRenderer.invoke("clipImageEmbedding", jpegImageData);
|
||||
|
||||
const clipTextEmbeddingIfAvailable = (
|
||||
text: string,
|
||||
): Promise<Float32Array | undefined> =>
|
||||
const clipTextEmbeddingIfAvailable = (text: string) =>
|
||||
ipcRenderer.invoke("clipTextEmbeddingIfAvailable", text);
|
||||
|
||||
const detectFaces = (input: Float32Array): Promise<Float32Array> =>
|
||||
const detectFaces = (input: Float32Array) =>
|
||||
ipcRenderer.invoke("detectFaces", input);
|
||||
|
||||
const faceEmbedding = (input: Float32Array): Promise<Float32Array> =>
|
||||
const faceEmbedding = (input: Float32Array) =>
|
||||
ipcRenderer.invoke("faceEmbedding", input);
|
||||
|
||||
// - File selection
|
||||
|
||||
// TODO: Deprecated - use dialogs on the renderer process itself
|
||||
|
||||
const selectDirectory = (): Promise<string> =>
|
||||
ipcRenderer.invoke("selectDirectory");
|
||||
|
||||
const showUploadFilesDialog = (): Promise<ElectronFile[]> =>
|
||||
ipcRenderer.invoke("showUploadFilesDialog");
|
||||
|
||||
const showUploadDirsDialog = (): Promise<ElectronFile[]> =>
|
||||
ipcRenderer.invoke("showUploadDirsDialog");
|
||||
|
||||
const showUploadZipDialog = (): Promise<{
|
||||
zipPaths: string[];
|
||||
files: ElectronFile[];
|
||||
}> => ipcRenderer.invoke("showUploadZipDialog");
|
||||
|
||||
// - Watch
|
||||
|
||||
const watchGet = (): Promise<FolderWatch[]> => ipcRenderer.invoke("watchGet");
|
||||
const watchGet = () => ipcRenderer.invoke("watchGet");
|
||||
|
||||
const watchAdd = (
|
||||
folderPath: string,
|
||||
collectionMapping: CollectionMapping,
|
||||
): Promise<FolderWatch[]> =>
|
||||
const watchAdd = (folderPath: string, collectionMapping: CollectionMapping) =>
|
||||
ipcRenderer.invoke("watchAdd", folderPath, collectionMapping);
|
||||
|
||||
const watchRemove = (folderPath: string): Promise<FolderWatch[]> =>
|
||||
const watchRemove = (folderPath: string) =>
|
||||
ipcRenderer.invoke("watchRemove", folderPath);
|
||||
|
||||
const watchUpdateSyncedFiles = (
|
||||
syncedFiles: FolderWatch["syncedFiles"],
|
||||
folderPath: string,
|
||||
): Promise<void> =>
|
||||
ipcRenderer.invoke("watchUpdateSyncedFiles", syncedFiles, folderPath);
|
||||
) => ipcRenderer.invoke("watchUpdateSyncedFiles", syncedFiles, folderPath);
|
||||
|
||||
const watchUpdateIgnoredFiles = (
|
||||
ignoredFiles: FolderWatch["ignoredFiles"],
|
||||
folderPath: string,
|
||||
): Promise<void> =>
|
||||
ipcRenderer.invoke("watchUpdateIgnoredFiles", ignoredFiles, folderPath);
|
||||
) => ipcRenderer.invoke("watchUpdateIgnoredFiles", ignoredFiles, folderPath);
|
||||
|
||||
const watchOnAddFile = (f: (path: string, watch: FolderWatch) => void) => {
|
||||
ipcRenderer.removeAllListeners("watchAddFile");
|
||||
|
@ -236,33 +205,56 @@ const watchOnRemoveDir = (f: (path: string, watch: FolderWatch) => void) => {
|
|||
);
|
||||
};
|
||||
|
||||
const watchFindFiles = (folderPath: string): Promise<string[]> =>
|
||||
const watchFindFiles = (folderPath: string) =>
|
||||
ipcRenderer.invoke("watchFindFiles", folderPath);
|
||||
|
||||
const watchReset = async () => {
|
||||
ipcRenderer.removeAllListeners("watchAddFile");
|
||||
ipcRenderer.removeAllListeners("watchRemoveFile");
|
||||
ipcRenderer.removeAllListeners("watchRemoveDir");
|
||||
await ipcRenderer.invoke("watchReset");
|
||||
};
|
||||
|
||||
// - Upload
|
||||
|
||||
const pendingUploads = (): Promise<PendingUploads | undefined> =>
|
||||
ipcRenderer.invoke("pendingUploads");
|
||||
const pathForFile = (file: File) => {
|
||||
const path = webUtils.getPathForFile(file);
|
||||
// The path that we get back from `webUtils.getPathForFile` on Windows uses
|
||||
// "/" as the path separator. Convert them to POSIX separators.
|
||||
//
|
||||
// Note that we do not have access to the path or the os module in the
|
||||
// preload script, thus this hand rolled transformation.
|
||||
|
||||
const setPendingUploadCollection = (collectionName: string): Promise<void> =>
|
||||
ipcRenderer.invoke("setPendingUploadCollection", collectionName);
|
||||
// However that makes TypeScript fidgety since we it cannot find navigator,
|
||||
// as we haven't included "lib": ["dom"] in our tsconfig to avoid making DOM
|
||||
// APIs available to our main Node.js code. We could create a separate
|
||||
// tsconfig just for the preload script, but for now let's go with a cast.
|
||||
//
|
||||
// @ts-expect-error navigator is not defined.
|
||||
const platform = (navigator as { platform: string }).platform;
|
||||
return platform.toLowerCase().includes("win")
|
||||
? path.split("\\").join("/")
|
||||
: path;
|
||||
};
|
||||
|
||||
const setPendingUploadFiles = (
|
||||
type: PendingUploads["type"],
|
||||
filePaths: string[],
|
||||
): Promise<void> =>
|
||||
ipcRenderer.invoke("setPendingUploadFiles", type, filePaths);
|
||||
const listZipItems = (zipPath: string) =>
|
||||
ipcRenderer.invoke("listZipItems", zipPath);
|
||||
|
||||
// - TODO: AUDIT below this
|
||||
// -
|
||||
const pathOrZipItemSize = (pathOrZipItem: string | ZipItem) =>
|
||||
ipcRenderer.invoke("pathOrZipItemSize", pathOrZipItem);
|
||||
|
||||
const getElectronFilesFromGoogleZip = (
|
||||
filePath: string,
|
||||
): Promise<ElectronFile[]> =>
|
||||
ipcRenderer.invoke("getElectronFilesFromGoogleZip", filePath);
|
||||
const pendingUploads = () => ipcRenderer.invoke("pendingUploads");
|
||||
|
||||
const getDirFiles = (dirPath: string): Promise<ElectronFile[]> =>
|
||||
ipcRenderer.invoke("getDirFiles", dirPath);
|
||||
const setPendingUploads = (pendingUploads: PendingUploads) =>
|
||||
ipcRenderer.invoke("setPendingUploads", pendingUploads);
|
||||
|
||||
const markUploadedFiles = (paths: PendingUploads["filePaths"]) =>
|
||||
ipcRenderer.invoke("markUploadedFiles", paths);
|
||||
|
||||
const markUploadedZipItems = (items: PendingUploads["zipItems"]) =>
|
||||
ipcRenderer.invoke("markUploadedZipItems", items);
|
||||
|
||||
const clearPendingUploads = () => ipcRenderer.invoke("clearPendingUploads");
|
||||
|
||||
/**
|
||||
* These objects exposed here will become available to the JS code in our
|
||||
|
@ -311,6 +303,7 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
logToDisk,
|
||||
openDirectory,
|
||||
openLogDirectory,
|
||||
selectDirectory,
|
||||
clearStores,
|
||||
encryptionKey,
|
||||
saveEncryptionKey,
|
||||
|
@ -334,7 +327,6 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
readTextFile: fsReadTextFile,
|
||||
writeFile: fsWriteFile,
|
||||
isDir: fsIsDir,
|
||||
size: fsSize,
|
||||
},
|
||||
|
||||
// - Conversion
|
||||
|
@ -350,35 +342,29 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
detectFaces,
|
||||
faceEmbedding,
|
||||
|
||||
// - File selection
|
||||
|
||||
selectDirectory,
|
||||
showUploadFilesDialog,
|
||||
showUploadDirsDialog,
|
||||
showUploadZipDialog,
|
||||
|
||||
// - Watch
|
||||
|
||||
watch: {
|
||||
get: watchGet,
|
||||
add: watchAdd,
|
||||
remove: watchRemove,
|
||||
updateSyncedFiles: watchUpdateSyncedFiles,
|
||||
updateIgnoredFiles: watchUpdateIgnoredFiles,
|
||||
onAddFile: watchOnAddFile,
|
||||
onRemoveFile: watchOnRemoveFile,
|
||||
onRemoveDir: watchOnRemoveDir,
|
||||
findFiles: watchFindFiles,
|
||||
updateSyncedFiles: watchUpdateSyncedFiles,
|
||||
updateIgnoredFiles: watchUpdateIgnoredFiles,
|
||||
reset: watchReset,
|
||||
},
|
||||
|
||||
// - Upload
|
||||
|
||||
pathForFile,
|
||||
listZipItems,
|
||||
pathOrZipItemSize,
|
||||
pendingUploads,
|
||||
setPendingUploadCollection,
|
||||
setPendingUploadFiles,
|
||||
|
||||
// -
|
||||
|
||||
getElectronFilesFromGoogleZip,
|
||||
getDirFiles,
|
||||
setPendingUploads,
|
||||
markUploadedFiles,
|
||||
markUploadedZipItems,
|
||||
clearPendingUploads,
|
||||
});
|
||||
|
|
5
desktop/src/thirdparty/clip-bpe-ts/mod.ts
vendored
5
desktop/src/thirdparty/clip-bpe-ts/mod.ts
vendored
|
@ -1,3 +1,5 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import * as htmlEntities from "html-entities";
|
||||
import bpeVocabData from "./bpe_simple_vocab_16e6";
|
||||
// import ftfy from "https://deno.land/x/ftfy_pyodide@v0.1.1/mod.js";
|
||||
|
@ -410,6 +412,7 @@ export default class {
|
|||
newWord.push(first + second);
|
||||
i += 2;
|
||||
} else {
|
||||
// @ts-expect-error "Array indexing can return undefined but not modifying thirdparty code"
|
||||
newWord.push(word[i]);
|
||||
i += 1;
|
||||
}
|
||||
|
@ -434,6 +437,7 @@ export default class {
|
|||
.map((b) => this.byteEncoder[b.charCodeAt(0) as number])
|
||||
.join("");
|
||||
bpeTokens.push(
|
||||
// @ts-expect-error "Array indexing can return undefined but not modifying thirdparty code"
|
||||
...this.bpe(token)
|
||||
.split(" ")
|
||||
.map((bpeToken: string) => this.encoder[bpeToken]),
|
||||
|
@ -458,6 +462,7 @@ export default class {
|
|||
.join("");
|
||||
text = [...text]
|
||||
.map((c) => this.byteDecoder[c])
|
||||
// @ts-expect-error "Array indexing can return undefined but not modifying thirdparty code"
|
||||
.map((v) => String.fromCharCode(v))
|
||||
.join("")
|
||||
.replace(/<\/w>/g, " ");
|
||||
|
|
|
@ -25,10 +25,12 @@ export interface FolderWatchSyncedFile {
|
|||
collectionID: number;
|
||||
}
|
||||
|
||||
export type ZipItem = [zipPath: string, entryName: string];
|
||||
|
||||
export interface PendingUploads {
|
||||
collectionName: string;
|
||||
type: "files" | "zips";
|
||||
files: ElectronFile[];
|
||||
collectionName: string | undefined;
|
||||
filePaths: string[];
|
||||
zipItems: ZipItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -40,25 +42,3 @@ export interface PendingUploads {
|
|||
export const CustomErrorMessage = {
|
||||
NotAvailable: "This feature in not available on the current OS/arch",
|
||||
};
|
||||
|
||||
/**
|
||||
* Deprecated - Use File + webUtils.getPathForFile instead
|
||||
*
|
||||
* Electron used to augment the standard web
|
||||
* [File](https://developer.mozilla.org/en-US/docs/Web/API/File) object with an
|
||||
* additional `path` property. This is now deprecated, and will be removed in a
|
||||
* future release.
|
||||
* https://www.electronjs.org/docs/latest/api/file-object
|
||||
*
|
||||
* The alternative to the `path` property is to use `webUtils.getPathForFile`
|
||||
* https://www.electronjs.org/docs/latest/api/web-utils
|
||||
*/
|
||||
export interface ElectronFile {
|
||||
name: string;
|
||||
path: string;
|
||||
size: number;
|
||||
lastModified: number;
|
||||
stream: () => Promise<ReadableStream<Uint8Array>>;
|
||||
blob: () => Promise<Blob>;
|
||||
arrayBuffer: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
|
|
@ -3,71 +3,34 @@
|
|||
into JavaScript that'll then be loaded and run by the main (node) process
|
||||
of our Electron app. */
|
||||
|
||||
/*
|
||||
* Recommended target, lib and other settings for code running in the
|
||||
* version of Node.js bundled with Electron.
|
||||
*
|
||||
* Currently, with Electron 30, this is Node.js 20.11.1.
|
||||
* https://www.electronjs.org/blog/electron-30-0
|
||||
*/
|
||||
"extends": "@tsconfig/node20/tsconfig.json",
|
||||
|
||||
/* TSConfig docs: https://aka.ms/tsconfig.json */
|
||||
|
||||
"compilerOptions": {
|
||||
/* Recommended target, lib and other settings for code running in the
|
||||
version of Node.js bundled with Electron.
|
||||
|
||||
Currently, with Electron 29, this is Node.js 20.9
|
||||
https://www.electronjs.org/blog/electron-29-0
|
||||
|
||||
Note that we cannot do
|
||||
|
||||
"extends": "@tsconfig/node20/tsconfig.json",
|
||||
|
||||
because that sets "lib": ["es2023"]. However (and I don't fully
|
||||
understand what's going on here), that breaks our compilation since
|
||||
tsc can then not find type definitions of things like ReadableStream.
|
||||
|
||||
Adding "dom" to "lib" (e.g. `"lib": ["es2023", "dom"]`) fixes the
|
||||
issue, but that doesn't sound correct - the main Electron process
|
||||
isn't running in a browser context.
|
||||
|
||||
It is possible that we're using some of the types incorrectly. For
|
||||
now, we just omit the "lib" definition and rely on the defaults for
|
||||
the "target" we've chosen. This is also what the current
|
||||
electron-forge starter does:
|
||||
|
||||
yarn create electron-app electron-forge-starter -- --template=webpack-typescript
|
||||
|
||||
Enhancement: Can revisit this later.
|
||||
|
||||
Refs:
|
||||
- https://github.com/electron/electron/issues/27092
|
||||
- https://github.com/electron/electron/issues/16146
|
||||
*/
|
||||
|
||||
"target": "es2022",
|
||||
"module": "node16",
|
||||
|
||||
/* Enable various workarounds to play better with CJS libraries */
|
||||
"esModuleInterop": true,
|
||||
/* Speed things up by not type checking `node_modules` */
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Emit the generated JS into `app/` */
|
||||
"outDir": "app",
|
||||
|
||||
/* Temporary overrides to get things to compile with the older config */
|
||||
"strict": false,
|
||||
"noImplicitAny": true
|
||||
|
||||
/* Below is the state we want */
|
||||
/* Enable these one by one */
|
||||
// "strict": true,
|
||||
|
||||
/* Require the `type` modifier when importing types */
|
||||
// "verbatimModuleSyntax": true
|
||||
/* We want this, but it causes "ESM syntax is not allowed in a CommonJS
|
||||
module when 'verbatimModuleSyntax' is enabled" currently */
|
||||
/* "verbatimModuleSyntax": true, */
|
||||
|
||||
"strict": true,
|
||||
/* Stricter than strict */
|
||||
// "noImplicitReturns": true,
|
||||
// "noUnusedParameters": true,
|
||||
// "noUnusedLocals": true,
|
||||
// "noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedParameters": true,
|
||||
"noUnusedLocals": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
/* e.g. makes array indexing returns undefined */
|
||||
// "noUncheckedIndexedAccess": true,
|
||||
// "exactOptionalPropertyTypes": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true
|
||||
},
|
||||
/* Transpile all `.ts` files in `src/` */
|
||||
"include": ["src/**/*.ts"]
|
||||
|
|
|
@ -7,11 +7,6 @@
|
|||
resolved "https://registry.yarnpkg.com/7zip-bin/-/7zip-bin-5.2.0.tgz#7a03314684dd6572b7dfa89e68ce31d60286854d"
|
||||
integrity sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==
|
||||
|
||||
"@aashutoshrathi/word-wrap@^1.2.3":
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf"
|
||||
integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==
|
||||
|
||||
"@babel/code-frame@^7.0.0":
|
||||
version "7.24.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae"
|
||||
|
@ -20,25 +15,25 @@
|
|||
"@babel/highlight" "^7.24.2"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
"@babel/helper-validator-identifier@^7.22.20":
|
||||
version "7.22.20"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
|
||||
integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
|
||||
"@babel/helper-validator-identifier@^7.24.5":
|
||||
version "7.24.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz#918b1a7fa23056603506370089bd990d8720db62"
|
||||
integrity sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==
|
||||
|
||||
"@babel/highlight@^7.24.2":
|
||||
version "7.24.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.2.tgz#3f539503efc83d3c59080a10e6634306e0370d26"
|
||||
integrity sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==
|
||||
version "7.24.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.5.tgz#bc0613f98e1dd0720e99b2a9ee3760194a704b6e"
|
||||
integrity sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.22.20"
|
||||
"@babel/helper-validator-identifier" "^7.24.5"
|
||||
chalk "^2.4.2"
|
||||
js-tokens "^4.0.0"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
"@babel/runtime@^7.21.0":
|
||||
version "7.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e"
|
||||
integrity sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==
|
||||
version "7.24.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.5.tgz#230946857c053a36ccc66e1dd03b17dd0c4ed02c"
|
||||
integrity sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
|
@ -165,21 +160,16 @@
|
|||
integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
|
||||
|
||||
"@humanwhocodes/object-schema@^2.0.2":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917"
|
||||
integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
|
||||
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
|
||||
|
||||
"@isaacs/cliui@^8.0.2":
|
||||
version "8.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
|
||||
integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==
|
||||
"@isaacs/fs-minipass@^4.0.0":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz#2d59ae3ab4b38fb4270bfa23d30f8e2e86c7fe32"
|
||||
integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==
|
||||
dependencies:
|
||||
string-width "^5.1.2"
|
||||
string-width-cjs "npm:string-width@^4.2.0"
|
||||
strip-ansi "^7.0.1"
|
||||
strip-ansi-cjs "npm:strip-ansi@^6.0.1"
|
||||
wrap-ansi "^8.1.0"
|
||||
wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
|
||||
minipass "^7.0.4"
|
||||
|
||||
"@malept/cross-spawn-promise@^1.1.0":
|
||||
version "1.1.1"
|
||||
|
@ -246,6 +236,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
|
||||
integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
|
||||
|
||||
"@tsconfig/node20@^20.1.4":
|
||||
version "20.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.4.tgz#3457d42eddf12d3bde3976186ab0cd22b85df928"
|
||||
integrity sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==
|
||||
|
||||
"@types/auto-launch@^5.0":
|
||||
version "5.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/auto-launch/-/auto-launch-5.0.5.tgz#439ed36aaaea501e2e2cfbddd8a20c366c34863b"
|
||||
|
@ -352,15 +347,15 @@
|
|||
"@types/node" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^7":
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.6.0.tgz#1f5df5cda490a0bcb6fbdd3382e19f1241024242"
|
||||
integrity sha512-gKmTNwZnblUdnTIJu3e9kmeRRzV2j1a/LUO27KNNAnIC5zjy1aSvXSRp4rVNlmAoHlQ7HzX42NbKpcSr4jF80A==
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz#c78e309fe967cb4de05b85cdc876fb95f8e01b6f"
|
||||
integrity sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.10.0"
|
||||
"@typescript-eslint/scope-manager" "7.6.0"
|
||||
"@typescript-eslint/type-utils" "7.6.0"
|
||||
"@typescript-eslint/utils" "7.6.0"
|
||||
"@typescript-eslint/visitor-keys" "7.6.0"
|
||||
"@typescript-eslint/scope-manager" "7.8.0"
|
||||
"@typescript-eslint/type-utils" "7.8.0"
|
||||
"@typescript-eslint/utils" "7.8.0"
|
||||
"@typescript-eslint/visitor-keys" "7.8.0"
|
||||
debug "^4.3.4"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^5.3.1"
|
||||
|
@ -369,46 +364,46 @@
|
|||
ts-api-utils "^1.3.0"
|
||||
|
||||
"@typescript-eslint/parser@^7":
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.6.0.tgz#0aca5de3045d68b36e88903d15addaf13d040a95"
|
||||
integrity sha512-usPMPHcwX3ZoPWnBnhhorc14NJw9J4HpSXQX4urF2TPKG0au0XhJoZyX62fmvdHONUkmyUe74Hzm1//XA+BoYg==
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.8.0.tgz#1e1db30c8ab832caffee5f37e677dbcb9357ddc8"
|
||||
integrity sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "7.6.0"
|
||||
"@typescript-eslint/types" "7.6.0"
|
||||
"@typescript-eslint/typescript-estree" "7.6.0"
|
||||
"@typescript-eslint/visitor-keys" "7.6.0"
|
||||
"@typescript-eslint/scope-manager" "7.8.0"
|
||||
"@typescript-eslint/types" "7.8.0"
|
||||
"@typescript-eslint/typescript-estree" "7.8.0"
|
||||
"@typescript-eslint/visitor-keys" "7.8.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@7.6.0":
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.6.0.tgz#1e9972f654210bd7500b31feadb61a233f5b5e9d"
|
||||
integrity sha512-ngttyfExA5PsHSx0rdFgnADMYQi+Zkeiv4/ZxGYUWd0nLs63Ha0ksmp8VMxAIC0wtCFxMos7Lt3PszJssG/E6w==
|
||||
"@typescript-eslint/scope-manager@7.8.0":
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz#bb19096d11ec6b87fb6640d921df19b813e02047"
|
||||
integrity sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "7.6.0"
|
||||
"@typescript-eslint/visitor-keys" "7.6.0"
|
||||
"@typescript-eslint/types" "7.8.0"
|
||||
"@typescript-eslint/visitor-keys" "7.8.0"
|
||||
|
||||
"@typescript-eslint/type-utils@7.6.0":
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.6.0.tgz#644f75075f379827d25fe0713e252ccd4e4a428c"
|
||||
integrity sha512-NxAfqAPNLG6LTmy7uZgpK8KcuiS2NZD/HlThPXQRGwz6u7MDBWRVliEEl1Gj6U7++kVJTpehkhZzCJLMK66Scw==
|
||||
"@typescript-eslint/type-utils@7.8.0":
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz#9de166f182a6e4d1c5da76e94880e91831e3e26f"
|
||||
integrity sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "7.6.0"
|
||||
"@typescript-eslint/utils" "7.6.0"
|
||||
"@typescript-eslint/typescript-estree" "7.8.0"
|
||||
"@typescript-eslint/utils" "7.8.0"
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^1.3.0"
|
||||
|
||||
"@typescript-eslint/types@7.6.0":
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.6.0.tgz#53dba7c30c87e5f10a731054266dd905f1fbae38"
|
||||
integrity sha512-h02rYQn8J+MureCvHVVzhl69/GAfQGPQZmOMjG1KfCl7o3HtMSlPaPUAPu6lLctXI5ySRGIYk94clD/AUMCUgQ==
|
||||
"@typescript-eslint/types@7.8.0":
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.8.0.tgz#1fd2577b3ad883b769546e2d1ef379f929a7091d"
|
||||
integrity sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==
|
||||
|
||||
"@typescript-eslint/typescript-estree@7.6.0":
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.6.0.tgz#112a3775563799fd3f011890ac8322f80830ac17"
|
||||
integrity sha512-+7Y/GP9VuYibecrCQWSKgl3GvUM5cILRttpWtnAu8GNL9j11e4tbuGZmZjJ8ejnKYyBRb2ddGQ3rEFCq3QjMJw==
|
||||
"@typescript-eslint/typescript-estree@7.8.0":
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz#b028a9226860b66e623c1ee55cc2464b95d2987c"
|
||||
integrity sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "7.6.0"
|
||||
"@typescript-eslint/visitor-keys" "7.6.0"
|
||||
"@typescript-eslint/types" "7.8.0"
|
||||
"@typescript-eslint/visitor-keys" "7.8.0"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
is-glob "^4.0.3"
|
||||
|
@ -416,25 +411,25 @@
|
|||
semver "^7.6.0"
|
||||
ts-api-utils "^1.3.0"
|
||||
|
||||
"@typescript-eslint/utils@7.6.0":
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.6.0.tgz#e400d782280b6f724c8a1204269d984c79202282"
|
||||
integrity sha512-x54gaSsRRI+Nwz59TXpCsr6harB98qjXYzsRxGqvA5Ue3kQH+FxS7FYU81g/omn22ML2pZJkisy6Q+ElK8pBCA==
|
||||
"@typescript-eslint/utils@7.8.0":
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.8.0.tgz#57a79f9c0c0740ead2f622e444cfaeeb9fd047cd"
|
||||
integrity sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.4.0"
|
||||
"@types/json-schema" "^7.0.15"
|
||||
"@types/semver" "^7.5.8"
|
||||
"@typescript-eslint/scope-manager" "7.6.0"
|
||||
"@typescript-eslint/types" "7.6.0"
|
||||
"@typescript-eslint/typescript-estree" "7.6.0"
|
||||
"@typescript-eslint/scope-manager" "7.8.0"
|
||||
"@typescript-eslint/types" "7.8.0"
|
||||
"@typescript-eslint/typescript-estree" "7.8.0"
|
||||
semver "^7.6.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@7.6.0":
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.6.0.tgz#d1ce13145844379021e1f9bd102c1d78946f4e76"
|
||||
integrity sha512-4eLB7t+LlNUmXzfOu1VAIAdkjbu5xNSerURS9X/S5TUKWFRpXRQZbmtPqgKmYx8bj3J0irtQXSiWAOY82v+cgw==
|
||||
"@typescript-eslint/visitor-keys@7.8.0":
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz#7285aab991da8bee411a42edbd5db760d22fdd91"
|
||||
integrity sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "7.6.0"
|
||||
"@typescript-eslint/types" "7.8.0"
|
||||
eslint-visitor-keys "^3.4.3"
|
||||
|
||||
"@ungap/structured-clone@^1.2.0":
|
||||
|
@ -487,25 +482,20 @@ ajv@^6.10.0, ajv@^6.12.0, ajv@^6.12.4:
|
|||
uri-js "^4.2.2"
|
||||
|
||||
ajv@^8.0.0, ajv@^8.6.3:
|
||||
version "8.12.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1"
|
||||
integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==
|
||||
version "8.13.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.13.0.tgz#a3939eaec9fb80d217ddf0c3376948c023f28c91"
|
||||
integrity sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.1"
|
||||
fast-deep-equal "^3.1.3"
|
||||
json-schema-traverse "^1.0.0"
|
||||
require-from-string "^2.0.2"
|
||||
uri-js "^4.2.2"
|
||||
uri-js "^4.4.1"
|
||||
|
||||
ansi-regex@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
||||
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
|
||||
|
||||
ansi-regex@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a"
|
||||
integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==
|
||||
|
||||
ansi-styles@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||
|
@ -520,11 +510,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
|
|||
dependencies:
|
||||
color-convert "^2.0.1"
|
||||
|
||||
ansi-styles@^6.1.0:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
|
||||
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
|
||||
|
||||
any-shell-escape@^0.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/any-shell-escape/-/any-shell-escape-0.1.1.tgz#d55ab972244c71a9a5e1ab0879f30bf110806959"
|
||||
|
@ -824,6 +809,11 @@ chownr@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
|
||||
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
|
||||
|
||||
chownr@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4"
|
||||
integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==
|
||||
|
||||
chromium-pickle-js@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205"
|
||||
|
@ -1026,7 +1016,7 @@ define-data-property@^1.0.1:
|
|||
es-errors "^1.3.0"
|
||||
gopd "^1.0.1"
|
||||
|
||||
define-properties@^1.1.3:
|
||||
define-properties@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c"
|
||||
integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==
|
||||
|
@ -1127,15 +1117,10 @@ dotenv@^9.0.2:
|
|||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-9.0.2.tgz#dacc20160935a37dea6364aa1bef819fb9b6ab05"
|
||||
integrity sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==
|
||||
|
||||
eastasianwidth@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
|
||||
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
|
||||
|
||||
ejs@^3.1.8:
|
||||
version "3.1.9"
|
||||
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361"
|
||||
integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==
|
||||
version "3.1.10"
|
||||
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b"
|
||||
integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==
|
||||
dependencies:
|
||||
jake "^10.8.5"
|
||||
|
||||
|
@ -1214,10 +1199,10 @@ electron-updater@^6.1:
|
|||
semver "^7.3.8"
|
||||
tiny-typed-emitter "^2.1.0"
|
||||
|
||||
electron@^29:
|
||||
version "29.3.0"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-29.3.0.tgz#8e65cb08e9c0952c66d3196e1b5c811c43b8c5b0"
|
||||
integrity sha512-ZxFKm0/v48GSoBuO3DdnMlCYXefEUKUHLMsKxyXY4nZGgzbBKpF/X8haZa2paNj23CLfsCKBOtfc2vsEQiOOsA==
|
||||
electron@^30:
|
||||
version "30.0.2"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-30.0.2.tgz#95ba019216bf8be9f3097580123e33ea37497733"
|
||||
integrity sha512-zv7T+GG89J/hyWVkQsLH4Y/rVEfqJG5M/wOBIGNaDdqd8UV9/YZPdS7CuFeaIj0H9LhCt95xkIQNpYB/3svOkQ==
|
||||
dependencies:
|
||||
"@electron/get" "^2.0.0"
|
||||
"@types/node" "^20.9.0"
|
||||
|
@ -1228,11 +1213,6 @@ emoji-regex@^8.0.0:
|
|||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
||||
|
||||
emoji-regex@^9.2.2:
|
||||
version "9.2.2"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
|
||||
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
|
||||
|
||||
end-of-stream@^1.1.0:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
|
||||
|
@ -1495,17 +1475,18 @@ find-up@^5.0.0:
|
|||
path-exists "^4.0.0"
|
||||
|
||||
flat-cache@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
|
||||
integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee"
|
||||
integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==
|
||||
dependencies:
|
||||
flatted "^3.1.0"
|
||||
flatted "^3.2.9"
|
||||
keyv "^4.5.3"
|
||||
rimraf "^3.0.2"
|
||||
|
||||
flatted@^3.1.0:
|
||||
version "3.2.6"
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.6.tgz#022e9218c637f9f3fc9c35ab9c9193f05add60b2"
|
||||
integrity sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==
|
||||
flatted@^3.2.9:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a"
|
||||
integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==
|
||||
|
||||
foreground-child@^3.1.0:
|
||||
version "3.1.1"
|
||||
|
@ -1621,16 +1602,16 @@ glob-parent@^6.0.2:
|
|||
dependencies:
|
||||
is-glob "^4.0.3"
|
||||
|
||||
glob@^10.3.10:
|
||||
version "10.3.10"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b"
|
||||
integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==
|
||||
glob@^10.3.10, glob@^10.3.7:
|
||||
version "10.3.12"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.12.tgz#3a65c363c2e9998d220338e88a5f6ac97302960b"
|
||||
integrity sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==
|
||||
dependencies:
|
||||
foreground-child "^3.1.0"
|
||||
jackspeak "^2.3.5"
|
||||
jackspeak "^2.3.6"
|
||||
minimatch "^9.0.1"
|
||||
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
path-scurry "^1.10.1"
|
||||
minipass "^7.0.4"
|
||||
path-scurry "^1.10.2"
|
||||
|
||||
glob@^7.0.0, glob@^7.1.3, glob@^7.1.6:
|
||||
version "7.2.3"
|
||||
|
@ -1664,11 +1645,12 @@ globals@^13.19.0:
|
|||
type-fest "^0.20.2"
|
||||
|
||||
globalthis@^1.0.1:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf"
|
||||
integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236"
|
||||
integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==
|
||||
dependencies:
|
||||
define-properties "^1.1.3"
|
||||
define-properties "^1.2.1"
|
||||
gopd "^1.0.1"
|
||||
|
||||
globby@^11.1.0:
|
||||
version "11.1.0"
|
||||
|
@ -1949,12 +1931,12 @@ isexe@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
||||
|
||||
jackspeak@^2.3.5:
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8"
|
||||
integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==
|
||||
jackspeak@2.1.1, jackspeak@^2.3.6:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.1.1.tgz#2a42db4cfbb7e55433c28b6f75d8b796af9669cd"
|
||||
integrity sha512-juf9stUEwUaILepraGOWIJTLwg48bUnBmRqd2ln2Os1sW987zeoj/hzhbvRB95oMuS2ZTpjULmdwHNX4rzZIZw==
|
||||
dependencies:
|
||||
"@isaacs/cliui" "^8.0.2"
|
||||
cliui "^8.0.1"
|
||||
optionalDependencies:
|
||||
"@pkgjs/parseargs" "^0.11.0"
|
||||
|
||||
|
@ -2049,7 +2031,7 @@ jsonfile@^6.0.1:
|
|||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
keyv@^4.0.0:
|
||||
keyv@^4.0.0, keyv@^4.5.3:
|
||||
version "4.5.4"
|
||||
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
|
||||
integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
|
||||
|
@ -2121,6 +2103,11 @@ lowercase-keys@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
|
||||
integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
|
||||
|
||||
lru-cache@^10.2.0:
|
||||
version "10.2.2"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878"
|
||||
integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
|
@ -2128,11 +2115,6 @@ lru-cache@^6.0.0:
|
|||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
"lru-cache@^9.1.1 || ^10.0.0":
|
||||
version "10.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3"
|
||||
integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==
|
||||
|
||||
matcher@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca"
|
||||
|
@ -2204,14 +2186,7 @@ minimatch@^5.0.1, minimatch@^5.1.1:
|
|||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimatch@^9.0.1:
|
||||
version "9.0.3"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825"
|
||||
integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimatch@^9.0.4:
|
||||
minimatch@^9.0.1, minimatch@^9.0.4:
|
||||
version "9.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51"
|
||||
integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==
|
||||
|
@ -2235,7 +2210,7 @@ minipass@^5.0.0:
|
|||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d"
|
||||
integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==
|
||||
|
||||
"minipass@^5.0.0 || ^6.0.2 || ^7.0.0":
|
||||
"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.4:
|
||||
version "7.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c"
|
||||
integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==
|
||||
|
@ -2248,6 +2223,14 @@ minizlib@^2.1.1:
|
|||
minipass "^3.0.0"
|
||||
yallist "^4.0.0"
|
||||
|
||||
minizlib@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.0.1.tgz#46d5329d1eb3c83924eff1d3b858ca0a31581012"
|
||||
integrity sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==
|
||||
dependencies:
|
||||
minipass "^7.0.4"
|
||||
rimraf "^5.0.5"
|
||||
|
||||
mkdirp@^0.5.1:
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
|
||||
|
@ -2260,6 +2243,11 @@ mkdirp@^1.0.3:
|
|||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||
|
||||
mkdirp@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50"
|
||||
integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==
|
||||
|
||||
ms@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
|
@ -2324,29 +2312,30 @@ onetime@^5.1.2:
|
|||
dependencies:
|
||||
mimic-fn "^2.1.0"
|
||||
|
||||
onnxruntime-common@1.17.0:
|
||||
version "1.17.0"
|
||||
resolved "https://registry.yarnpkg.com/onnxruntime-common/-/onnxruntime-common-1.17.0.tgz#b2534ce021b1c1b19182bec39aaea8d547d2013e"
|
||||
integrity sha512-Vq1remJbCPITjDMJ04DA7AklUTnbYUp4vbnm6iL7ukSt+7VErH0NGYfekRSTjxxurEtX7w41PFfnQlE6msjPJw==
|
||||
onnxruntime-common@1.17.3:
|
||||
version "1.17.3"
|
||||
resolved "https://registry.yarnpkg.com/onnxruntime-common/-/onnxruntime-common-1.17.3.tgz#aadc456477873a540ee3d611ae9cd4f3de7c43e5"
|
||||
integrity sha512-IkbaDelNVX8cBfHFgsNADRIq2TlXMFWW+nG55mwWvQT4i0NZb32Jf35Pf6h9yjrnK78RjcnlNYaI37w394ovMw==
|
||||
|
||||
onnxruntime-node@^1.17:
|
||||
version "1.17.0"
|
||||
resolved "https://registry.yarnpkg.com/onnxruntime-node/-/onnxruntime-node-1.17.0.tgz#38af0ba527cb44c1afb639bdcb4e549edba029a1"
|
||||
integrity sha512-pRxdqSP3a6wtiFVkVX1V3/gsEMwBRUA9D2oYmcN3cjF+j+ILS+SIY2L7KxdWapsG6z64i5rUn8ijFZdIvbojBg==
|
||||
version "1.17.3"
|
||||
resolved "https://registry.yarnpkg.com/onnxruntime-node/-/onnxruntime-node-1.17.3.tgz#53b8b7ef68bf3834bba9d7be592e4c2d718d2018"
|
||||
integrity sha512-NtbN1pfApTSEjVq46LrJ396aPP2Gjhy+oYZi5Bu1leDXAEvVap/BQ8CZELiLs7z0UnXy3xjJW23HiB4P3//FIw==
|
||||
dependencies:
|
||||
onnxruntime-common "1.17.0"
|
||||
onnxruntime-common "1.17.3"
|
||||
tar "^7.0.1"
|
||||
|
||||
optionator@^0.9.3:
|
||||
version "0.9.3"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64"
|
||||
integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==
|
||||
version "0.9.4"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734"
|
||||
integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==
|
||||
dependencies:
|
||||
"@aashutoshrathi/word-wrap" "^1.2.3"
|
||||
deep-is "^0.1.3"
|
||||
fast-levenshtein "^2.0.6"
|
||||
levn "^0.4.1"
|
||||
prelude-ls "^1.2.1"
|
||||
type-check "^0.4.0"
|
||||
word-wrap "^1.2.5"
|
||||
|
||||
p-cancelable@^2.0.0:
|
||||
version "2.1.1"
|
||||
|
@ -2440,12 +2429,12 @@ path-parse@^1.0.7:
|
|||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
||||
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
||||
|
||||
path-scurry@^1.10.1:
|
||||
version "1.10.1"
|
||||
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698"
|
||||
integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==
|
||||
path-scurry@^1.10.2:
|
||||
version "1.10.2"
|
||||
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.2.tgz#8f6357eb1239d5fa1da8b9f70e9c080675458ba7"
|
||||
integrity sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==
|
||||
dependencies:
|
||||
lru-cache "^9.1.1 || ^10.0.0"
|
||||
lru-cache "^10.2.0"
|
||||
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
|
||||
path-type@^4.0.0:
|
||||
|
@ -2655,6 +2644,13 @@ rimraf@^3.0.2:
|
|||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rimraf@^5.0.5:
|
||||
version "5.0.5"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.5.tgz#9be65d2d6e683447d2e9013da2bf451139a61ccf"
|
||||
integrity sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==
|
||||
dependencies:
|
||||
glob "^10.3.7"
|
||||
|
||||
roarr@^2.15.3:
|
||||
version "2.15.4"
|
||||
resolved "https://registry.yarnpkg.com/roarr/-/roarr-2.15.4.tgz#f5fe795b7b838ccfe35dc608e0282b9eba2e7afd"
|
||||
|
@ -2880,7 +2876,7 @@ stat-mode@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-1.0.0.tgz#68b55cb61ea639ff57136f36b216a291800d1465"
|
||||
integrity sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
|
@ -2889,15 +2885,6 @@ stat-mode@^1.0.0:
|
|||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^5.0.1, string-width@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
|
||||
integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
|
||||
dependencies:
|
||||
eastasianwidth "^0.2.0"
|
||||
emoji-regex "^9.2.2"
|
||||
strip-ansi "^7.0.1"
|
||||
|
||||
string_decoder@^1.1.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
|
||||
|
@ -2905,20 +2892,13 @@ string_decoder@^1.1.1:
|
|||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^7.0.1:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
|
||||
integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
|
||||
dependencies:
|
||||
ansi-regex "^6.0.1"
|
||||
|
||||
strip-json-comments@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||
|
@ -2977,6 +2957,18 @@ tar@^6.1.12:
|
|||
mkdirp "^1.0.3"
|
||||
yallist "^4.0.0"
|
||||
|
||||
tar@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/tar/-/tar-7.0.1.tgz#8f6ccebcd91b69e9767a6fc4892799e8b0e606d5"
|
||||
integrity sha512-IjMhdQMZFpKsHEQT3woZVxBtCQY+0wk3CVxdRkGXEgyGa0dNS/ehPvOMr2nmfC7x5Zj2N+l6yZUpmICjLGS35w==
|
||||
dependencies:
|
||||
"@isaacs/fs-minipass" "^4.0.0"
|
||||
chownr "^3.0.0"
|
||||
minipass "^5.0.0"
|
||||
minizlib "^3.0.1"
|
||||
mkdirp "^3.0.1"
|
||||
yallist "^5.0.0"
|
||||
|
||||
temp-file@^3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/temp-file/-/temp-file-3.4.0.tgz#766ea28911c683996c248ef1a20eea04d51652c7"
|
||||
|
@ -3031,12 +3023,7 @@ ts-api-utils@^1.3.0:
|
|||
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1"
|
||||
integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==
|
||||
|
||||
tslib@^2.1.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
|
||||
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
|
||||
|
||||
tslib@^2.6.2:
|
||||
tslib@^2.1.0, tslib@^2.6.2:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||
|
@ -3079,9 +3066,9 @@ typedarray@^0.0.6:
|
|||
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
|
||||
|
||||
typescript@^5, typescript@^5.3.3:
|
||||
version "5.4.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.3.tgz#5c6fedd4c87bee01cd7a528a30145521f8e0feff"
|
||||
integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==
|
||||
version "5.4.5"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611"
|
||||
integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==
|
||||
|
||||
undici-types@~5.26.4:
|
||||
version "5.26.5"
|
||||
|
@ -3103,7 +3090,7 @@ untildify@^3.0.2:
|
|||
resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9"
|
||||
integrity sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==
|
||||
|
||||
uri-js@^4.2.2:
|
||||
uri-js@^4.2.2, uri-js@^4.4.1:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
|
||||
integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
|
||||
|
@ -3149,7 +3136,12 @@ winreg@1.2.4:
|
|||
resolved "https://registry.yarnpkg.com/winreg/-/winreg-1.2.4.tgz#ba065629b7a925130e15779108cf540990e98d1b"
|
||||
integrity sha512-IHpzORub7kYlb8A43Iig3reOvlcBJGX9gZ0WycHhghHtA65X0LYnMRuJs+aH1abVnMJztQkvQNlltnbPi5aGIA==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
word-wrap@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
@ -3158,15 +3150,6 @@ winreg@1.2.4:
|
|||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||
integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
|
||||
dependencies:
|
||||
ansi-styles "^6.1.0"
|
||||
string-width "^5.0.1"
|
||||
strip-ansi "^7.0.1"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
|
@ -3187,6 +3170,11 @@ yallist@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||
|
||||
yallist@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-5.0.0.tgz#00e2de443639ed0d78fd87de0d27469fbcffb533"
|
||||
integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==
|
||||
|
||||
yargs-parser@^21.1.1:
|
||||
version "21.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||
|
|
|
@ -18,7 +18,7 @@ A guide written by Green, an ente.io lover
|
|||
Migrating from Authy can be tiring, as you cannot export your 2FA codes through
|
||||
the app, meaning that you would have to reconfigure 2FA for all of your accounts
|
||||
for your new 2FA authenticator. However, easier ways exist to export your codes
|
||||
out of Authy. This guide will cover two of the most used methods for mograting
|
||||
out of Authy. This guide will cover two of the most used methods for migrating
|
||||
from Authy to Ente Authenticator.
|
||||
|
||||
> [!CAUTION]
|
||||
|
|
|
@ -25,10 +25,13 @@ configure the endpoint the app should be connecting to.
|
|||
> You can download the CLI from
|
||||
> [here](https://github.com/ente-io/ente/releases?q=tag%3Acli-v0)
|
||||
|
||||
Define a config.yaml and put it either in the same directory as CLI or path
|
||||
defined in env variable `ENTE_CLI_CONFIG_PATH`
|
||||
Define a config.yaml and put it either in the same directory as where you run
|
||||
the CLI from ("current working directory"), or in the path defined in env
|
||||
variable `ENTE_CLI_CONFIG_PATH`:
|
||||
|
||||
```yaml
|
||||
endpoint:
|
||||
api: "http://localhost:8080"
|
||||
```
|
||||
|
||||
(Another [example](https://github.com/ente-io/ente/blob/main/cli/config.yaml.example))
|
||||
|
|
|
@ -108,8 +108,6 @@ PODS:
|
|||
- FlutterMacOS
|
||||
- integration_test (0.0.1):
|
||||
- Flutter
|
||||
- isar_flutter_libs (1.0.0):
|
||||
- Flutter
|
||||
- libwebp (1.3.2):
|
||||
- libwebp/demux (= 1.3.2)
|
||||
- libwebp/mux (= 1.3.2)
|
||||
|
@ -246,7 +244,6 @@ DEPENDENCIES:
|
|||
- image_editor_common (from `.symlinks/plugins/image_editor_common/ios`)
|
||||
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
|
||||
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
||||
- local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
|
||||
- media_extension (from `.symlinks/plugins/media_extension/ios`)
|
||||
|
@ -341,8 +338,6 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
|
||||
integration_test:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
isar_flutter_libs:
|
||||
:path: ".symlinks/plugins/isar_flutter_libs/ios"
|
||||
local_auth_darwin:
|
||||
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
||||
local_auth_ios:
|
||||
|
@ -427,7 +422,6 @@ SPEC CHECKSUMS:
|
|||
image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43
|
||||
in_app_purchase_storekit: 0e4b3c2e43ba1e1281f4f46dd71b0593ce529892
|
||||
integration_test: 13825b8a9334a850581300559b8839134b124670
|
||||
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
|
||||
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
|
||||
local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98
|
||||
local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9
|
||||
|
|
|
@ -308,7 +308,6 @@
|
|||
"${BUILT_PRODUCTS_DIR}/image_editor_common/image_editor_common.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/in_app_purchase_storekit/in_app_purchase_storekit.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/isar_flutter_libs/isar_flutter_libs.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/local_auth_darwin/local_auth_darwin.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/local_auth_ios/local_auth_ios.framework",
|
||||
|
@ -390,7 +389,6 @@
|
|||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_editor_common.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/in_app_purchase_storekit.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/isar_flutter_libs.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_darwin.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_ios.framework",
|
||||
|
|
|
@ -105,5 +105,14 @@
|
|||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_googlecast._tcp</string>
|
||||
<string>F5BCEC64._googlecast._tcp</string>
|
||||
</array>
|
||||
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>${PRODUCT_NAME} uses the local network to discover Cast-enabled devices on your WiFi
|
||||
network.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -1,79 +1,167 @@
|
|||
import "dart:io";
|
||||
import "dart:typed_data";
|
||||
|
||||
import "package:isar/isar.dart";
|
||||
import "package:path/path.dart";
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/embedding_updated_event.dart";
|
||||
import "package:photos/models/embedding.dart";
|
||||
import "package:sqlite_async/sqlite_async.dart";
|
||||
|
||||
class EmbeddingsDB {
|
||||
late final Isar _isar;
|
||||
|
||||
EmbeddingsDB._privateConstructor();
|
||||
|
||||
static final EmbeddingsDB instance = EmbeddingsDB._privateConstructor();
|
||||
|
||||
static const databaseName = "ente.embeddings.db";
|
||||
static const tableName = "embeddings";
|
||||
static const columnFileID = "file_id";
|
||||
static const columnModel = "model";
|
||||
static const columnEmbedding = "embedding";
|
||||
static const columnUpdationTime = "updation_time";
|
||||
|
||||
static Future<SqliteDatabase>? _dbFuture;
|
||||
|
||||
Future<SqliteDatabase> get _database async {
|
||||
_dbFuture ??= _initDatabase();
|
||||
return _dbFuture!;
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
_isar = await Isar.open(
|
||||
[EmbeddingSchema],
|
||||
directory: dir.path,
|
||||
);
|
||||
await _clearDeprecatedStore(dir);
|
||||
await _clearDeprecatedStores(dir);
|
||||
}
|
||||
|
||||
Future<SqliteDatabase> _initDatabase() async {
|
||||
final Directory documentsDirectory =
|
||||
await getApplicationDocumentsDirectory();
|
||||
final String path = join(documentsDirectory.path, databaseName);
|
||||
final migrations = SqliteMigrations()
|
||||
..add(
|
||||
SqliteMigration(
|
||||
1,
|
||||
(tx) async {
|
||||
await tx.execute(
|
||||
'CREATE TABLE $tableName ($columnFileID INTEGER NOT NULL, $columnModel INTEGER NOT NULL, $columnEmbedding BLOB NOT NULL, $columnUpdationTime INTEGER, UNIQUE ($columnFileID, $columnModel))',
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
final database = SqliteDatabase(path: path);
|
||||
await migrations.migrate(database);
|
||||
return database;
|
||||
}
|
||||
|
||||
Future<void> clearTable() async {
|
||||
await _isar.writeTxn(() => _isar.clear());
|
||||
final db = await _database;
|
||||
await db.execute('DELETE * FROM $tableName');
|
||||
}
|
||||
|
||||
Future<List<Embedding>> getAll(Model model) async {
|
||||
return _isar.embeddings.filter().modelEqualTo(model).findAll();
|
||||
final db = await _database;
|
||||
final results = await db.getAll('SELECT * FROM $tableName');
|
||||
return _convertToEmbeddings(results);
|
||||
}
|
||||
|
||||
Future<void> put(Embedding embedding) {
|
||||
return _isar.writeTxn(() async {
|
||||
await _isar.embeddings.putByIndex(Embedding.index, embedding);
|
||||
Bus.instance.fire(EmbeddingUpdatedEvent());
|
||||
});
|
||||
Future<void> put(Embedding embedding) async {
|
||||
final db = await _database;
|
||||
await db.execute(
|
||||
'INSERT OR REPLACE INTO $tableName ($columnFileID, $columnModel, $columnEmbedding, $columnUpdationTime) VALUES (?, ?, ?, ?)',
|
||||
_getRowFromEmbedding(embedding),
|
||||
);
|
||||
Bus.instance.fire(EmbeddingUpdatedEvent());
|
||||
}
|
||||
|
||||
Future<void> putMany(List<Embedding> embeddings) {
|
||||
return _isar.writeTxn(() async {
|
||||
await _isar.embeddings.putAllByIndex(Embedding.index, embeddings);
|
||||
Bus.instance.fire(EmbeddingUpdatedEvent());
|
||||
});
|
||||
Future<void> putMany(List<Embedding> embeddings) async {
|
||||
final db = await _database;
|
||||
final inputs = embeddings.map((e) => _getRowFromEmbedding(e)).toList();
|
||||
await db.executeBatch(
|
||||
'INSERT OR REPLACE INTO $tableName ($columnFileID, $columnModel, $columnEmbedding, $columnUpdationTime) values(?, ?, ?, ?)',
|
||||
inputs,
|
||||
);
|
||||
Bus.instance.fire(EmbeddingUpdatedEvent());
|
||||
}
|
||||
|
||||
Future<List<Embedding>> getUnsyncedEmbeddings() async {
|
||||
return await _isar.embeddings.filter().updationTimeEqualTo(null).findAll();
|
||||
final db = await _database;
|
||||
final results = await db.getAll(
|
||||
'SELECT * FROM $tableName WHERE $columnUpdationTime IS NULL',
|
||||
);
|
||||
return _convertToEmbeddings(results);
|
||||
}
|
||||
|
||||
Future<void> deleteEmbeddings(List<int> fileIDs) async {
|
||||
await _isar.writeTxn(() async {
|
||||
final embeddings = <Embedding>[];
|
||||
for (final fileID in fileIDs) {
|
||||
embeddings.addAll(
|
||||
await _isar.embeddings.filter().fileIDEqualTo(fileID).findAll(),
|
||||
);
|
||||
}
|
||||
await _isar.embeddings.deleteAll(embeddings.map((e) => e.id).toList());
|
||||
Bus.instance.fire(EmbeddingUpdatedEvent());
|
||||
});
|
||||
final db = await _database;
|
||||
await db.execute(
|
||||
'DELETE FROM $tableName WHERE $columnFileID IN (${fileIDs.join(", ")})',
|
||||
);
|
||||
Bus.instance.fire(EmbeddingUpdatedEvent());
|
||||
}
|
||||
|
||||
Future<void> deleteAllForModel(Model model) async {
|
||||
await _isar.writeTxn(() async {
|
||||
final embeddings =
|
||||
await _isar.embeddings.filter().modelEqualTo(model).findAll();
|
||||
await _isar.embeddings.deleteAll(embeddings.map((e) => e.id).toList());
|
||||
Bus.instance.fire(EmbeddingUpdatedEvent());
|
||||
});
|
||||
final db = await _database;
|
||||
await db.execute(
|
||||
'DELETE FROM $tableName WHERE $columnModel = ?',
|
||||
[modelToInt(model)!],
|
||||
);
|
||||
Bus.instance.fire(EmbeddingUpdatedEvent());
|
||||
}
|
||||
|
||||
Future<void> _clearDeprecatedStore(Directory dir) async {
|
||||
final deprecatedStore = Directory(dir.path + "/object-box-store");
|
||||
if (await deprecatedStore.exists()) {
|
||||
await deprecatedStore.delete(recursive: true);
|
||||
List<Embedding> _convertToEmbeddings(List<Map<String, dynamic>> results) {
|
||||
final List<Embedding> embeddings = [];
|
||||
for (final result in results) {
|
||||
embeddings.add(_getEmbeddingFromRow(result));
|
||||
}
|
||||
return embeddings;
|
||||
}
|
||||
|
||||
Embedding _getEmbeddingFromRow(Map<String, dynamic> row) {
|
||||
final fileID = row[columnFileID];
|
||||
final model = intToModel(row[columnModel])!;
|
||||
final bytes = row[columnEmbedding] as Uint8List;
|
||||
final list = Float32List.view(bytes.buffer);
|
||||
return Embedding(fileID: fileID, model: model, embedding: list);
|
||||
}
|
||||
|
||||
List<Object?> _getRowFromEmbedding(Embedding embedding) {
|
||||
return [
|
||||
embedding.fileID,
|
||||
modelToInt(embedding.model)!,
|
||||
Float32List.fromList(embedding.embedding).buffer.asUint8List(),
|
||||
embedding.updationTime,
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _clearDeprecatedStores(Directory dir) async {
|
||||
final deprecatedObjectBox = Directory(dir.path + "/object-box-store");
|
||||
if (await deprecatedObjectBox.exists()) {
|
||||
await deprecatedObjectBox.delete(recursive: true);
|
||||
}
|
||||
final deprecatedIsar = File(dir.path + "/default.isar");
|
||||
if (await deprecatedIsar.exists()) {
|
||||
await deprecatedIsar.delete();
|
||||
}
|
||||
}
|
||||
|
||||
int? modelToInt(Model model) {
|
||||
switch (model) {
|
||||
case Model.onnxClip:
|
||||
return 1;
|
||||
case Model.ggmlClip:
|
||||
return 2;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Model? intToModel(int model) {
|
||||
switch (model) {
|
||||
case 1:
|
||||
return Model.onnxClip;
|
||||
case 2:
|
||||
return Model.ggmlClip;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -455,6 +455,7 @@ class FilesDB {
|
|||
}
|
||||
|
||||
Future<int> insert(EnteFile file) async {
|
||||
_logger.info("Inserting $file");
|
||||
final db = await instance.database;
|
||||
return db.insert(
|
||||
filesTable,
|
||||
|
|
|
@ -12,10 +12,14 @@ class CastGateway {
|
|||
);
|
||||
return response.data["publicKey"];
|
||||
} catch (e) {
|
||||
if (e is DioError &&
|
||||
e.response != null &&
|
||||
e.response!.statusCode == 404) {
|
||||
return null;
|
||||
if (e is DioError && e.response != null) {
|
||||
if (e.response!.statusCode == 404) {
|
||||
return null;
|
||||
} else if (e.response!.statusCode == 403) {
|
||||
throw CastIPMismatchException();
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
|
@ -48,3 +52,7 @@ class CastGateway {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CastIPMismatchException implements Exception {
|
||||
CastIPMismatchException();
|
||||
}
|
||||
|
|
26
mobile/lib/generated/intl/messages_en.dart
generated
26
mobile/lib/generated/intl/messages_en.dart
generated
|
@ -357,6 +357,13 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"Authentication failed, please try again"),
|
||||
"authenticationSuccessful":
|
||||
MessageLookupByLibrary.simpleMessage("Authentication successful!"),
|
||||
"autoCastDialogBody": MessageLookupByLibrary.simpleMessage(
|
||||
"You\'ll see available Cast devices here."),
|
||||
"autoCastiOSPermission": MessageLookupByLibrary.simpleMessage(
|
||||
"Make sure Local Network permissions are turned on for the Ente Photos app, in Settings."),
|
||||
"autoPair": MessageLookupByLibrary.simpleMessage("Auto pair"),
|
||||
"autoPairGoogle": MessageLookupByLibrary.simpleMessage(
|
||||
"Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos."),
|
||||
"available": MessageLookupByLibrary.simpleMessage("Available"),
|
||||
"backedUpFolders":
|
||||
MessageLookupByLibrary.simpleMessage("Backed up folders"),
|
||||
|
@ -387,6 +394,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"cannotAddMorePhotosAfterBecomingViewer": m9,
|
||||
"cannotDeleteSharedFiles":
|
||||
MessageLookupByLibrary.simpleMessage("Cannot delete shared files"),
|
||||
"castIPMismatchBody": MessageLookupByLibrary.simpleMessage(
|
||||
"Please make sure you are on the same network as the TV."),
|
||||
"castIPMismatchTitle":
|
||||
MessageLookupByLibrary.simpleMessage("Failed to cast album"),
|
||||
"castInstruction": MessageLookupByLibrary.simpleMessage(
|
||||
"Visit cast.ente.io on the device you want to pair.\n\nEnter the code below to play the album on your TV."),
|
||||
"centerPoint": MessageLookupByLibrary.simpleMessage("Center point"),
|
||||
|
@ -460,6 +471,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
MessageLookupByLibrary.simpleMessage("Confirm recovery key"),
|
||||
"confirmYourRecoveryKey":
|
||||
MessageLookupByLibrary.simpleMessage("Confirm your recovery key"),
|
||||
"connectToDevice":
|
||||
MessageLookupByLibrary.simpleMessage("Connect to device"),
|
||||
"contactFamilyAdmin": m12,
|
||||
"contactSupport":
|
||||
MessageLookupByLibrary.simpleMessage("Contact support"),
|
||||
|
@ -721,6 +734,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"filesBackedUpFromDevice": m22,
|
||||
"filesBackedUpInAlbum": m23,
|
||||
"filesDeleted": MessageLookupByLibrary.simpleMessage("Files deleted"),
|
||||
"filesSavedToGallery":
|
||||
MessageLookupByLibrary.simpleMessage("Files saved to gallery"),
|
||||
"flip": MessageLookupByLibrary.simpleMessage("Flip"),
|
||||
"forYourMemories":
|
||||
MessageLookupByLibrary.simpleMessage("for your memories"),
|
||||
|
@ -902,6 +917,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"manageParticipants": MessageLookupByLibrary.simpleMessage("Manage"),
|
||||
"manageSubscription":
|
||||
MessageLookupByLibrary.simpleMessage("Manage subscription"),
|
||||
"manualPairDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Pair with PIN works for any large screen device you want to play your album on."),
|
||||
"map": MessageLookupByLibrary.simpleMessage("Map"),
|
||||
"maps": MessageLookupByLibrary.simpleMessage("Maps"),
|
||||
"mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"),
|
||||
|
@ -936,6 +953,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"no": MessageLookupByLibrary.simpleMessage("No"),
|
||||
"noAlbumsSharedByYouYet":
|
||||
MessageLookupByLibrary.simpleMessage("No albums shared by you yet"),
|
||||
"noDeviceFound":
|
||||
MessageLookupByLibrary.simpleMessage("No device found"),
|
||||
"noDeviceLimit": MessageLookupByLibrary.simpleMessage("None"),
|
||||
"noDeviceThatCanBeDeleted": MessageLookupByLibrary.simpleMessage(
|
||||
"You\'ve no files on this device that can be deleted"),
|
||||
|
@ -982,6 +1001,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"orPickAnExistingOne":
|
||||
MessageLookupByLibrary.simpleMessage("Or pick an existing one"),
|
||||
"pair": MessageLookupByLibrary.simpleMessage("Pair"),
|
||||
"pairWithPin": MessageLookupByLibrary.simpleMessage("Pair with PIN"),
|
||||
"pairingComplete":
|
||||
MessageLookupByLibrary.simpleMessage("Pairing complete"),
|
||||
"passkey": MessageLookupByLibrary.simpleMessage("Passkey"),
|
||||
"passkeyAuthTitle":
|
||||
MessageLookupByLibrary.simpleMessage("Passkey verification"),
|
||||
|
@ -1328,6 +1350,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Success"),
|
||||
"startBackup": MessageLookupByLibrary.simpleMessage("Start backup"),
|
||||
"status": MessageLookupByLibrary.simpleMessage("Status"),
|
||||
"stopCastingBody": MessageLookupByLibrary.simpleMessage(
|
||||
"Do you want to stop casting?"),
|
||||
"stopCastingTitle":
|
||||
MessageLookupByLibrary.simpleMessage("Stop casting"),
|
||||
"storage": MessageLookupByLibrary.simpleMessage("Storage"),
|
||||
"storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Family"),
|
||||
"storageBreakupYou": MessageLookupByLibrary.simpleMessage("You"),
|
||||
|
|
140
mobile/lib/generated/l10n.dart
generated
140
mobile/lib/generated/l10n.dart
generated
|
@ -5945,6 +5945,16 @@ class S {
|
|||
);
|
||||
}
|
||||
|
||||
/// `Files saved to gallery`
|
||||
String get filesSavedToGallery {
|
||||
return Intl.message(
|
||||
'Files saved to gallery',
|
||||
name: 'filesSavedToGallery',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Failed to save file to gallery`
|
||||
String get fileFailedToSaveToGallery {
|
||||
return Intl.message(
|
||||
|
@ -8378,6 +8388,26 @@ class S {
|
|||
);
|
||||
}
|
||||
|
||||
/// `Auto pair`
|
||||
String get autoPair {
|
||||
return Intl.message(
|
||||
'Auto pair',
|
||||
name: 'autoPair',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Pair with PIN`
|
||||
String get pairWithPin {
|
||||
return Intl.message(
|
||||
'Pair with PIN',
|
||||
name: 'pairWithPin',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Device not found`
|
||||
String get deviceNotFound {
|
||||
return Intl.message(
|
||||
|
@ -8563,6 +8593,116 @@ class S {
|
|||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.`
|
||||
String get autoPairGoogle {
|
||||
return Intl.message(
|
||||
'Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.',
|
||||
name: 'autoPairGoogle',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Pair with PIN works for any large screen device you want to play your album on.`
|
||||
String get manualPairDesc {
|
||||
return Intl.message(
|
||||
'Pair with PIN works for any large screen device you want to play your album on.',
|
||||
name: 'manualPairDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Connect to device`
|
||||
String get connectToDevice {
|
||||
return Intl.message(
|
||||
'Connect to device',
|
||||
name: 'connectToDevice',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `You'll see available Cast devices here.`
|
||||
String get autoCastDialogBody {
|
||||
return Intl.message(
|
||||
'You\'ll see available Cast devices here.',
|
||||
name: 'autoCastDialogBody',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Make sure Local Network permissions are turned on for the Ente Photos app, in Settings.`
|
||||
String get autoCastiOSPermission {
|
||||
return Intl.message(
|
||||
'Make sure Local Network permissions are turned on for the Ente Photos app, in Settings.',
|
||||
name: 'autoCastiOSPermission',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `No device found`
|
||||
String get noDeviceFound {
|
||||
return Intl.message(
|
||||
'No device found',
|
||||
name: 'noDeviceFound',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Stop casting`
|
||||
String get stopCastingTitle {
|
||||
return Intl.message(
|
||||
'Stop casting',
|
||||
name: 'stopCastingTitle',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Do you want to stop casting?`
|
||||
String get stopCastingBody {
|
||||
return Intl.message(
|
||||
'Do you want to stop casting?',
|
||||
name: 'stopCastingBody',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Failed to cast album`
|
||||
String get castIPMismatchTitle {
|
||||
return Intl.message(
|
||||
'Failed to cast album',
|
||||
name: 'castIPMismatchTitle',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Please make sure you are on the same network as the TV.`
|
||||
String get castIPMismatchBody {
|
||||
return Intl.message(
|
||||
'Please make sure you are on the same network as the TV.',
|
||||
name: 'castIPMismatchBody',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Pairing complete`
|
||||
String get pairingComplete {
|
||||
return Intl.message(
|
||||
'Pairing complete',
|
||||
name: 'pairingComplete',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
||||
|
|
|
@ -835,6 +835,7 @@
|
|||
"close": "Close",
|
||||
"setAs": "Set as",
|
||||
"fileSavedToGallery": "File saved to gallery",
|
||||
"filesSavedToGallery": "Files saved to gallery",
|
||||
"fileFailedToSaveToGallery": "Failed to save file to gallery",
|
||||
"download": "Download",
|
||||
"pressAndHoldToPlayVideo": "Press and hold to play video",
|
||||
|
@ -1195,6 +1196,8 @@
|
|||
"verifyPasskey": "Verify passkey",
|
||||
"playOnTv": "Play album on TV",
|
||||
"pair": "Pair",
|
||||
"autoPair": "Auto pair",
|
||||
"pairWithPin": "Pair with PIN",
|
||||
"deviceNotFound": "Device not found",
|
||||
"castInstruction": "Visit cast.ente.io on the device you want to pair.\n\nEnter the code below to play the album on your TV.",
|
||||
"deviceCodeHint": "Enter the code",
|
||||
|
@ -1212,5 +1215,16 @@
|
|||
"endpointUpdatedMessage": "Endpoint updated successfully",
|
||||
"customEndpoint": "Connected to {endpoint}",
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
"search": "Search",
|
||||
"autoPairGoogle": "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.",
|
||||
"manualPairDesc": "Pair with PIN works for any large screen device you want to play your album on.",
|
||||
"connectToDevice": "Connect to device",
|
||||
"autoCastDialogBody": "You'll see available Cast devices here.",
|
||||
"autoCastiOSPermission": "Make sure Local Network permissions are turned on for the Ente Photos app, in Settings.",
|
||||
"noDeviceFound": "No device found",
|
||||
"stopCastingTitle": "Stop casting",
|
||||
"stopCastingBody": "Do you want to stop casting?",
|
||||
"castIPMismatchTitle": "Failed to cast album",
|
||||
"castIPMismatchBody": "Please make sure you are on the same network as the TV.",
|
||||
"pairingComplete": "Pairing complete"
|
||||
}
|
|
@ -1,17 +1,7 @@
|
|||
import "dart:convert";
|
||||
|
||||
import "package:isar/isar.dart";
|
||||
|
||||
part 'embedding.g.dart';
|
||||
|
||||
@collection
|
||||
class Embedding {
|
||||
static const index = 'unique_file_model_embedding';
|
||||
|
||||
Id id = Isar.autoIncrement;
|
||||
final int fileID;
|
||||
@enumerated
|
||||
@Index(name: index, composite: [CompositeIndex('fileID')], unique: true, replace: true)
|
||||
final Model model;
|
||||
final List<double> embedding;
|
||||
int? updationTime;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -308,7 +308,7 @@ class EnteFile {
|
|||
@override
|
||||
String toString() {
|
||||
return '''File(generatedID: $generatedID, localID: $localID, title: $title,
|
||||
uploadedFileId: $uploadedFileID, modificationTime: $modificationTime,
|
||||
type: $fileType, uploadedFileId: $uploadedFileID, modificationTime: $modificationTime,
|
||||
ownerID: $ownerID, collectionID: $collectionID, updationTime: $updationTime)''';
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import "package:dio/dio.dart";
|
||||
import "package:ente_cast/ente_cast.dart";
|
||||
import "package:ente_cast_normal/ente_cast_normal.dart";
|
||||
import "package:ente_feature_flag/ente_feature_flag.dart";
|
||||
import "package:shared_preferences/shared_preferences.dart";
|
||||
|
||||
|
@ -26,3 +28,9 @@ FlagService get flagService {
|
|||
);
|
||||
return _flagService!;
|
||||
}
|
||||
|
||||
CastService? _castService;
|
||||
CastService get castService {
|
||||
_castService ??= CastServiceImpl();
|
||||
return _castService!;
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ class EmbeddingStore {
|
|||
|
||||
static final EmbeddingStore instance = EmbeddingStore._privateConstructor();
|
||||
|
||||
static const kEmbeddingsSyncTimeKey = "sync_time_embeddings_v2";
|
||||
static const kEmbeddingsSyncTimeKey = "sync_time_embeddings_v3";
|
||||
|
||||
final _logger = Logger("EmbeddingStore");
|
||||
final _dio = NetworkClient.instance.enteDio;
|
||||
|
|
|
@ -72,8 +72,8 @@ class SemanticSearchService {
|
|||
_mlFramework = _currentModel == Model.onnxClip
|
||||
? ONNX(shouldDownloadOverMobileData)
|
||||
: GGML(shouldDownloadOverMobileData);
|
||||
await EmbeddingsDB.instance.init();
|
||||
await EmbeddingStore.instance.init();
|
||||
await EmbeddingsDB.instance.init();
|
||||
await _loadEmbeddings();
|
||||
Bus.instance.on<EmbeddingUpdatedEvent>().listen((event) {
|
||||
_embeddingLoaderDebouncer.run(() async {
|
||||
|
|
128
mobile/lib/ui/cast/auto.dart
Normal file
128
mobile/lib/ui/cast/auto.dart
Normal file
|
@ -0,0 +1,128 @@
|
|||
import "dart:io";
|
||||
|
||||
import "package:ente_cast/ente_cast.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/service_locator.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/common/loading_widget.dart";
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
|
||||
class AutoCastDialog extends StatefulWidget {
|
||||
// async method that takes string as input
|
||||
// and returns void
|
||||
final void Function(String) onConnect;
|
||||
AutoCastDialog(
|
||||
this.onConnect, {
|
||||
Key? key,
|
||||
}) : super(key: key) {}
|
||||
|
||||
@override
|
||||
State<AutoCastDialog> createState() => _AutoCastDialogState();
|
||||
}
|
||||
|
||||
class _AutoCastDialogState extends State<AutoCastDialog> {
|
||||
final bool doesUserExist = true;
|
||||
final Set<Object> _isDeviceTapInProgress = {};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textStyle = getEnteTextTheme(context);
|
||||
final AlertDialog alert = AlertDialog(
|
||||
title: Text(
|
||||
S.of(context).connectToDevice,
|
||||
style: textStyle.largeBold,
|
||||
),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).autoCastDialogBody,
|
||||
style: textStyle.bodyMuted,
|
||||
),
|
||||
if (Platform.isIOS)
|
||||
Text(
|
||||
S.of(context).autoCastiOSPermission,
|
||||
style: textStyle.bodyMuted,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FutureBuilder<List<(String, Object)>>(
|
||||
future: castService.searchDevices(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'Error: ${snapshot.error.toString()}',
|
||||
),
|
||||
);
|
||||
} else if (!snapshot.hasData) {
|
||||
return const EnteLoadingWidget();
|
||||
}
|
||||
|
||||
if (snapshot.data!.isEmpty) {
|
||||
return Center(child: Text(S.of(context).noDeviceFound));
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: snapshot.data!.map((result) {
|
||||
final device = result.$2;
|
||||
final name = result.$1;
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
if (_isDeviceTapInProgress.contains(device)) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isDeviceTapInProgress.add(device);
|
||||
});
|
||||
try {
|
||||
await _connectToYourApp(context, device);
|
||||
} catch (e) {
|
||||
showGenericErrorDialog(context: context, error: e)
|
||||
.ignore();
|
||||
} finally {
|
||||
setState(() {
|
||||
_isDeviceTapInProgress.remove(device);
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(name)),
|
||||
if (_isDeviceTapInProgress.contains(device))
|
||||
const EnteLoadingWidget(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return alert;
|
||||
}
|
||||
|
||||
Future<void> _connectToYourApp(
|
||||
BuildContext context,
|
||||
Object castDevice,
|
||||
) async {
|
||||
await castService.connectDevice(
|
||||
context,
|
||||
castDevice,
|
||||
onMessage: (message) {
|
||||
if (message.containsKey(CastMessageType.pairCode)) {
|
||||
final code = message[CastMessageType.pairCode]!['code'];
|
||||
widget.onConnect(code);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
76
mobile/lib/ui/cast/choose.dart
Normal file
76
mobile/lib/ui/cast/choose.dart
Normal file
|
@ -0,0 +1,76 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/l10n/l10n.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/components/buttons/button_widget.dart";
|
||||
import "package:photos/ui/components/models/button_type.dart";
|
||||
|
||||
class CastChooseDialog extends StatefulWidget {
|
||||
const CastChooseDialog({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CastChooseDialog> createState() => _CastChooseDialogState();
|
||||
}
|
||||
|
||||
class _CastChooseDialogState extends State<CastChooseDialog> {
|
||||
final bool doesUserExist = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textStyle = getEnteTextTheme(context);
|
||||
final AlertDialog alert = AlertDialog(
|
||||
title: Text(
|
||||
context.l10n.playOnTv,
|
||||
style: textStyle.largeBold,
|
||||
),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
S.of(context).autoPairGoogle,
|
||||
style: textStyle.bodyMuted,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ButtonWidget(
|
||||
labelText: S.of(context).autoPair,
|
||||
icon: Icons.cast_outlined,
|
||||
buttonType: ButtonType.neutral,
|
||||
buttonSize: ButtonSize.large,
|
||||
shouldStickToDarkTheme: true,
|
||||
buttonAction: ButtonAction.first,
|
||||
shouldSurfaceExecutionStates: false,
|
||||
isInAlert: true,
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop(ButtonAction.first);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 36),
|
||||
Text(
|
||||
S.of(context).manualPairDesc,
|
||||
style: textStyle.bodyMuted,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ButtonWidget(
|
||||
labelText: S.of(context).pairWithPin,
|
||||
buttonType: ButtonType.neutral,
|
||||
// icon for pairing with TV manually
|
||||
icon: Icons.tv_outlined,
|
||||
buttonSize: ButtonSize.large,
|
||||
isInAlert: true,
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop(ButtonAction.second);
|
||||
},
|
||||
shouldStickToDarkTheme: true,
|
||||
buttonAction: ButtonAction.second,
|
||||
shouldSurfaceExecutionStates: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return alert;
|
||||
}
|
||||
}
|
38
mobile/lib/ui/common/popup_item.dart
Normal file
38
mobile/lib/ui/common/popup_item.dart
Normal file
|
@ -0,0 +1,38 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class EntePopupMenuItem<T> extends PopupMenuItem<T> {
|
||||
final String label;
|
||||
final IconData? icon;
|
||||
final Widget? iconWidget;
|
||||
|
||||
EntePopupMenuItem(
|
||||
this.label, {
|
||||
required T value,
|
||||
this.icon,
|
||||
this.iconWidget,
|
||||
Key? key,
|
||||
}) : assert(
|
||||
icon != null || iconWidget != null,
|
||||
'Either icon or iconWidget must be provided.',
|
||||
),
|
||||
assert(
|
||||
!(icon != null && iconWidget != null),
|
||||
'Only one of icon or iconWidget can be provided.',
|
||||
),
|
||||
super(
|
||||
value: value,
|
||||
key: key,
|
||||
child: Row(
|
||||
children: [
|
||||
if (iconWidget != null)
|
||||
iconWidget
|
||||
else if (icon != null)
|
||||
Icon(icon),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text(label),
|
||||
],
|
||||
), // Initially empty, will be populated in build
|
||||
);
|
||||
}
|
|
@ -3,6 +3,7 @@ import "dart:async";
|
|||
import 'package:fast_base58/fast_base58.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import "package:logging/logging.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
|
@ -30,6 +31,8 @@ import 'package:photos/ui/sharing/manage_links_widget.dart';
|
|||
import "package:photos/ui/tools/collage/collage_creator_page.dart";
|
||||
import "package:photos/ui/viewer/location/update_location_data_widget.dart";
|
||||
import 'package:photos/utils/delete_file_util.dart';
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
import "package:photos/utils/file_download_util.dart";
|
||||
import 'package:photos/utils/magic_util.dart';
|
||||
import 'package:photos/utils/navigation_util.dart';
|
||||
import "package:photos/utils/share_util.dart";
|
||||
|
@ -56,6 +59,7 @@ class FileSelectionActionsWidget extends StatefulWidget {
|
|||
|
||||
class _FileSelectionActionsWidgetState
|
||||
extends State<FileSelectionActionsWidget> {
|
||||
static final _logger = Logger("FileSelectionActionsWidget");
|
||||
late int currentUserID;
|
||||
late FilesSplit split;
|
||||
late CollectionActions collectionActions;
|
||||
|
@ -115,6 +119,8 @@ class _FileSelectionActionsWidgetState
|
|||
!widget.selectedFiles.files.any(
|
||||
(element) => element.fileType == FileType.video,
|
||||
);
|
||||
final showDownloadOption =
|
||||
widget.selectedFiles.files.any((element) => element.localID == null);
|
||||
|
||||
//To animate adding and removing of [SelectedActionButton], add all items
|
||||
//and set [shouldShow] to false for items that should not be shown and true
|
||||
|
@ -367,6 +373,16 @@ class _FileSelectionActionsWidgetState
|
|||
);
|
||||
}
|
||||
|
||||
if (showDownloadOption) {
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
labelText: S.of(context).download,
|
||||
icon: Icons.cloud_download_outlined,
|
||||
onTap: () => _download(widget.selectedFiles.files.toList()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
labelText: S.of(context).share,
|
||||
|
@ -379,41 +395,36 @@ class _FileSelectionActionsWidgetState
|
|||
),
|
||||
);
|
||||
|
||||
if (items.isNotEmpty) {
|
||||
final scrollController = ScrollController();
|
||||
// h4ck: https://github.com/flutter/flutter/issues/57920#issuecomment-893970066
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).removePadding(removeBottom: true),
|
||||
child: SafeArea(
|
||||
child: Scrollbar(
|
||||
radius: const Radius.circular(1),
|
||||
thickness: 2,
|
||||
controller: scrollController,
|
||||
thumbVisibility: true,
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(
|
||||
decelerationRate: ScrollDecelerationRate.fast,
|
||||
),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(width: 4),
|
||||
...items,
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
),
|
||||
final scrollController = ScrollController();
|
||||
// h4ck: https://github.com/flutter/flutter/issues/57920#issuecomment-893970066
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).removePadding(removeBottom: true),
|
||||
child: SafeArea(
|
||||
child: Scrollbar(
|
||||
radius: const Radius.circular(1),
|
||||
thickness: 2,
|
||||
controller: scrollController,
|
||||
thumbVisibility: true,
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(
|
||||
decelerationRate: ScrollDecelerationRate.fast,
|
||||
),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(width: 4),
|
||||
...items,
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// TODO: Return "Select All" here
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _moveFiles() async {
|
||||
|
@ -647,4 +658,29 @@ class _FileSelectionActionsWidgetState
|
|||
widget.selectedFiles.clearAll();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _download(List<EnteFile> files) async {
|
||||
final dialog = createProgressDialog(
|
||||
context,
|
||||
S.of(context).downloading,
|
||||
isDismissible: true,
|
||||
);
|
||||
await dialog.show();
|
||||
try {
|
||||
final futures = <Future>[];
|
||||
for (final file in files) {
|
||||
if (file.localID == null) {
|
||||
futures.add(downloadToGallery(file));
|
||||
}
|
||||
}
|
||||
await Future.wait(futures);
|
||||
await dialog.hide();
|
||||
widget.selectedFiles.clearAll();
|
||||
showToast(context, S.of(context).filesSavedToGallery);
|
||||
} catch (e) {
|
||||
_logger.warning("Failed to save files", e);
|
||||
await dialog.hide();
|
||||
await showGenericErrorDialog(context: context, error: e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,30 +4,23 @@ import 'package:flutter/cupertino.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:media_extension/media_extension.dart';
|
||||
import 'package:path/path.dart' as file_path;
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/events/local_photos_updated_event.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/l10n/l10n.dart";
|
||||
import "package:photos/models/file/extensions/file_props.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import 'package:photos/models/file/file_type.dart';
|
||||
import 'package:photos/models/file/trash_file.dart';
|
||||
import 'package:photos/models/ignored_file.dart';
|
||||
import "package:photos/models/metadata/common_keys.dart";
|
||||
import 'package:photos/models/selected_files.dart';
|
||||
import "package:photos/service_locator.dart";
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import 'package:photos/services/hidden_service.dart';
|
||||
import 'package:photos/services/ignored_files_service.dart';
|
||||
import 'package:photos/services/local_sync_service.dart';
|
||||
import 'package:photos/ui/collections/collection_action_sheet.dart';
|
||||
import 'package:photos/ui/viewer/file/custom_app_bar.dart';
|
||||
import "package:photos/ui/viewer/file_details/favorite_widget.dart";
|
||||
import "package:photos/ui/viewer/file_details/upload_icon_widget.dart";
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
import "package:photos/utils/file_download_util.dart";
|
||||
import 'package:photos/utils/file_util.dart';
|
||||
import "package:photos/utils/magic_util.dart";
|
||||
import 'package:photos/utils/toast_util.dart';
|
||||
|
@ -165,7 +158,7 @@ class FileAppBarState extends State<FileAppBar> {
|
|||
Icon(
|
||||
Platform.isAndroid
|
||||
? Icons.download
|
||||
: CupertinoIcons.cloud_download,
|
||||
: Icons.cloud_download_outlined,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
const Padding(
|
||||
|
@ -330,98 +323,16 @@ class FileAppBarState extends State<FileAppBar> {
|
|||
);
|
||||
await dialog.show();
|
||||
try {
|
||||
final FileType type = file.fileType;
|
||||
final bool downloadLivePhotoOnDroid =
|
||||
type == FileType.livePhoto && Platform.isAndroid;
|
||||
AssetEntity? savedAsset;
|
||||
final File? fileToSave = await getFile(file);
|
||||
//Disabling notifications for assets changing to insert the file into
|
||||
//files db before triggering a sync.
|
||||
await PhotoManager.stopChangeNotify();
|
||||
if (type == FileType.image) {
|
||||
savedAsset = await PhotoManager.editor
|
||||
.saveImageWithPath(fileToSave!.path, title: file.title!);
|
||||
} else if (type == FileType.video) {
|
||||
savedAsset = await PhotoManager.editor
|
||||
.saveVideo(fileToSave!, title: file.title!);
|
||||
} else if (type == FileType.livePhoto) {
|
||||
final File? liveVideoFile =
|
||||
await getFileFromServer(file, liveVideo: true);
|
||||
if (liveVideoFile == null) {
|
||||
throw AssertionError("Live video can not be null");
|
||||
}
|
||||
if (downloadLivePhotoOnDroid) {
|
||||
await _saveLivePhotoOnDroid(fileToSave!, liveVideoFile, file);
|
||||
} else {
|
||||
savedAsset = await PhotoManager.editor.darwin.saveLivePhoto(
|
||||
imageFile: fileToSave!,
|
||||
videoFile: liveVideoFile,
|
||||
title: file.title!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (savedAsset != null) {
|
||||
file.localID = savedAsset.id;
|
||||
await FilesDB.instance.insert(file);
|
||||
Bus.instance.fire(
|
||||
LocalPhotosUpdatedEvent(
|
||||
[file],
|
||||
source: "download",
|
||||
),
|
||||
);
|
||||
} else if (!downloadLivePhotoOnDroid && savedAsset == null) {
|
||||
_logger.severe('Failed to save assert of type $type');
|
||||
}
|
||||
await downloadToGallery(file);
|
||||
showToast(context, S.of(context).fileSavedToGallery);
|
||||
await dialog.hide();
|
||||
} catch (e) {
|
||||
_logger.warning("Failed to save file", e);
|
||||
await dialog.hide();
|
||||
await showGenericErrorDialog(context: context, error: e);
|
||||
} finally {
|
||||
await PhotoManager.startChangeNotify();
|
||||
LocalSyncService.instance.checkAndSync().ignore();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveLivePhotoOnDroid(
|
||||
File image,
|
||||
File video,
|
||||
EnteFile enteFile,
|
||||
) async {
|
||||
debugPrint("Downloading LivePhoto on Droid");
|
||||
AssetEntity? savedAsset = await (PhotoManager.editor
|
||||
.saveImageWithPath(image.path, title: enteFile.title!));
|
||||
if (savedAsset == null) {
|
||||
throw Exception("Failed to save image of live photo");
|
||||
}
|
||||
IgnoredFile ignoreVideoFile = IgnoredFile(
|
||||
savedAsset.id,
|
||||
savedAsset.title ?? '',
|
||||
savedAsset.relativePath ?? 'remoteDownload',
|
||||
"remoteDownload",
|
||||
);
|
||||
await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]);
|
||||
final videoTitle = file_path.basenameWithoutExtension(enteFile.title!) +
|
||||
file_path.extension(video.path);
|
||||
savedAsset = (await (PhotoManager.editor.saveVideo(
|
||||
video,
|
||||
title: videoTitle,
|
||||
)));
|
||||
if (savedAsset == null) {
|
||||
throw Exception("Failed to save video of live photo");
|
||||
}
|
||||
|
||||
ignoreVideoFile = IgnoredFile(
|
||||
savedAsset.id,
|
||||
savedAsset.title ?? videoTitle,
|
||||
savedAsset.relativePath ?? 'remoteDownload',
|
||||
"remoteDownload",
|
||||
);
|
||||
await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]);
|
||||
}
|
||||
|
||||
Future<void> _setAs(EnteFile file) async {
|
||||
final dialog = createProgressDialog(context, S.of(context).pleaseWait);
|
||||
await dialog.show();
|
||||
|
|
|
@ -24,6 +24,9 @@ import 'package:photos/services/collections_service.dart';
|
|||
import 'package:photos/services/sync_service.dart';
|
||||
import 'package:photos/services/update_service.dart';
|
||||
import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
|
||||
import "package:photos/ui/cast/auto.dart";
|
||||
import "package:photos/ui/cast/choose.dart";
|
||||
import "package:photos/ui/common/popup_item.dart";
|
||||
import 'package:photos/ui/components/action_sheet_widget.dart';
|
||||
import 'package:photos/ui/components/buttons/button_widget.dart';
|
||||
import 'package:photos/ui/components/models/button_type.dart';
|
||||
|
@ -319,263 +322,136 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
),
|
||||
);
|
||||
}
|
||||
final List<PopupMenuItem<AlbumPopupAction>> items = [];
|
||||
if (galleryType.canRename()) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
|
||||
if (widget.collection != null && castService.isSupported) {
|
||||
actions.add(
|
||||
Tooltip(
|
||||
message: "Cast album",
|
||||
child: IconButton(
|
||||
icon: castService.getActiveSessions().isNotEmpty
|
||||
? const Icon(Icons.cast_connected_rounded)
|
||||
: const Icon(Icons.cast_outlined),
|
||||
onPressed: () async {
|
||||
await _castChoiceDialog();
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final List<EntePopupMenuItem<AlbumPopupAction>> items = [];
|
||||
items.addAll([
|
||||
if (galleryType.canRename())
|
||||
EntePopupMenuItem(
|
||||
isQuickLink
|
||||
? S.of(context).convertToAlbum
|
||||
: S.of(context).renameAlbum,
|
||||
value: AlbumPopupAction.rename,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(isQuickLink ? Icons.photo_album_outlined : Icons.edit),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text(
|
||||
isQuickLink
|
||||
? S.of(context).convertToAlbum
|
||||
: S.of(context).renameAlbum,
|
||||
),
|
||||
],
|
||||
),
|
||||
icon: isQuickLink ? Icons.photo_album_outlined : Icons.edit,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (galleryType.canSetCover()) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
if (galleryType.canSetCover())
|
||||
EntePopupMenuItem(
|
||||
S.of(context).setCover,
|
||||
value: AlbumPopupAction.setCover,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.image_outlined),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text(S.of(context).setCover),
|
||||
],
|
||||
),
|
||||
icon: Icons.image_outlined,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (galleryType.showMap()) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
if (galleryType.showMap())
|
||||
EntePopupMenuItem(
|
||||
S.of(context).map,
|
||||
value: AlbumPopupAction.map,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.map_outlined),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text(S.of(context).map),
|
||||
],
|
||||
),
|
||||
icon: Icons.map_outlined,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (galleryType.canSort()) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
if (galleryType.canSort())
|
||||
EntePopupMenuItem(
|
||||
S.of(context).sortAlbumsBy,
|
||||
value: AlbumPopupAction.sort,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.sort_outlined),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text(
|
||||
S.of(context).sortAlbumsBy,
|
||||
),
|
||||
],
|
||||
),
|
||||
icon: Icons.sort_outlined,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (galleryType == GalleryType.uncategorized) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
if (galleryType == GalleryType.uncategorized)
|
||||
EntePopupMenuItem(
|
||||
S.of(context).cleanUncategorized,
|
||||
value: AlbumPopupAction.cleanUncategorized,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.crop_original_outlined),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text(S.of(context).cleanUncategorized),
|
||||
],
|
||||
),
|
||||
icon: Icons.crop_original_outlined,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (galleryType.canPin()) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
if (galleryType.canPin())
|
||||
EntePopupMenuItem(
|
||||
widget.collection!.isPinned
|
||||
? S.of(context).unpinAlbum
|
||||
: S.of(context).pinAlbum,
|
||||
value: AlbumPopupAction.pinAlbum,
|
||||
child: Row(
|
||||
children: [
|
||||
widget.collection!.isPinned
|
||||
? const Icon(CupertinoIcons.pin_slash)
|
||||
: Transform.rotate(
|
||||
angle: 45 * math.pi / 180, // rotate by 45 degrees
|
||||
child: const Icon(CupertinoIcons.pin),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text(
|
||||
widget.collection!.isPinned
|
||||
? S.of(context).unpinAlbum
|
||||
: S.of(context).pinAlbum,
|
||||
),
|
||||
],
|
||||
),
|
||||
iconWidget: widget.collection!.isPinned
|
||||
? const Icon(CupertinoIcons.pin_slash)
|
||||
: Transform.rotate(
|
||||
angle: 45 * math.pi / 180, // rotate by 45 degrees
|
||||
child: const Icon(CupertinoIcons.pin),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
]);
|
||||
final bool isArchived = widget.collection?.isArchived() ?? false;
|
||||
final bool isHidden = widget.collection?.isHidden() ?? false;
|
||||
// Do not show archive option for favorite collection. If collection is
|
||||
// already archived, allow user to unarchive that collection.
|
||||
if (isArchived || (galleryType.canArchive() && !isHidden)) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
value: AlbumPopupAction.ownedArchive,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(isArchived ? Icons.unarchive : Icons.archive_outlined),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text(
|
||||
isArchived
|
||||
? S.of(context).unarchiveAlbum
|
||||
: S.of(context).archiveAlbum,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!isArchived && galleryType.canHide()) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
value: AlbumPopupAction.ownedHide,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isHidden
|
||||
? Icons.visibility_outlined
|
||||
: Icons.visibility_off_outlined,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text(
|
||||
isHidden ? S.of(context).unhide : S.of(context).hide,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (widget.collection != null && isInternalUser) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
value: AlbumPopupAction.playOnTv,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.tv_outlined),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text(context.l10n.playOnTv),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (galleryType.canDelete()) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
value: isQuickLink
|
||||
? AlbumPopupAction.removeLink
|
||||
: AlbumPopupAction.delete,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isQuickLink
|
||||
? Icons.remove_circle_outline
|
||||
: Icons.delete_outline,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text(
|
||||
isQuickLink
|
||||
? S.of(context).removeLink
|
||||
: S.of(context).deleteAlbum,
|
||||
),
|
||||
],
|
||||
items.addAll(
|
||||
[
|
||||
// Do not show archive option for favorite collection. If collection is
|
||||
// already archived, allow user to unarchive that collection.
|
||||
if (isArchived || (galleryType.canArchive() && !isHidden))
|
||||
EntePopupMenuItem(
|
||||
value: AlbumPopupAction.ownedArchive,
|
||||
isArchived
|
||||
? S.of(context).unarchiveAlbum
|
||||
: S.of(context).archiveAlbum,
|
||||
icon: isArchived ? Icons.unarchive : Icons.archive_outlined,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (galleryType == GalleryType.sharedCollection) {
|
||||
final bool hasShareeArchived = widget.collection!.hasShareeArchived();
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
value: AlbumPopupAction.sharedArchive,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
hasShareeArchived ? Icons.unarchive : Icons.archive_outlined,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text(
|
||||
hasShareeArchived
|
||||
? S.of(context).unarchiveAlbum
|
||||
: S.of(context).archiveAlbum,
|
||||
),
|
||||
],
|
||||
if (!isArchived && galleryType.canHide())
|
||||
EntePopupMenuItem(
|
||||
value: AlbumPopupAction.ownedHide,
|
||||
isHidden ? S.of(context).unhide : S.of(context).hide,
|
||||
icon: isHidden
|
||||
? Icons.visibility_outlined
|
||||
: Icons.visibility_off_outlined,
|
||||
),
|
||||
),
|
||||
);
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
value: AlbumPopupAction.leave,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.logout),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text(S.of(context).leaveAlbum),
|
||||
],
|
||||
if (widget.collection != null && isInternalUser)
|
||||
EntePopupMenuItem(
|
||||
value: AlbumPopupAction.playOnTv,
|
||||
context.l10n.playOnTv,
|
||||
icon: Icons.tv_outlined,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (galleryType == GalleryType.localFolder) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
value: AlbumPopupAction.freeUpSpace,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.delete_sweep_outlined),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text(S.of(context).freeUpDeviceSpace),
|
||||
],
|
||||
if (galleryType.canDelete())
|
||||
EntePopupMenuItem(
|
||||
isQuickLink ? S.of(context).removeLink : S.of(context).deleteAlbum,
|
||||
value: isQuickLink
|
||||
? AlbumPopupAction.removeLink
|
||||
: AlbumPopupAction.delete,
|
||||
icon: isQuickLink
|
||||
? Icons.remove_circle_outline
|
||||
: Icons.delete_outline,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (galleryType == GalleryType.sharedCollection)
|
||||
EntePopupMenuItem(
|
||||
widget.collection!.hasShareeArchived()
|
||||
? S.of(context).unarchiveAlbum
|
||||
: S.of(context).archiveAlbum,
|
||||
value: AlbumPopupAction.sharedArchive,
|
||||
icon: widget.collection!.hasShareeArchived()
|
||||
? Icons.unarchive
|
||||
: Icons.archive_outlined,
|
||||
),
|
||||
if (galleryType == GalleryType.sharedCollection)
|
||||
EntePopupMenuItem(
|
||||
S.of(context).leaveAlbum,
|
||||
value: AlbumPopupAction.leave,
|
||||
icon: Icons.logout,
|
||||
),
|
||||
if (galleryType == GalleryType.localFolder)
|
||||
EntePopupMenuItem(
|
||||
S.of(context).freeUpDeviceSpace,
|
||||
value: AlbumPopupAction.freeUpSpace,
|
||||
icon: Icons.delete_sweep_outlined,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (items.isNotEmpty) {
|
||||
actions.add(
|
||||
PopupMenuButton(
|
||||
|
@ -603,7 +479,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
} else if (value == AlbumPopupAction.leave) {
|
||||
await _leaveAlbum(context);
|
||||
} else if (value == AlbumPopupAction.playOnTv) {
|
||||
await castAlbum();
|
||||
await _castChoiceDialog();
|
||||
} else if (value == AlbumPopupAction.freeUpSpace) {
|
||||
await _deleteBackedUpFiles(context);
|
||||
} else if (value == AlbumPopupAction.setCover) {
|
||||
|
@ -838,10 +714,56 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> castAlbum() async {
|
||||
Future<void> _castChoiceDialog() async {
|
||||
final gw = CastGateway(NetworkClient.instance.enteDio);
|
||||
if (castService.getActiveSessions().isNotEmpty) {
|
||||
await showChoiceDialog(
|
||||
context,
|
||||
title: S.of(context).stopCastingTitle,
|
||||
firstButtonLabel: S.of(context).yes,
|
||||
secondButtonLabel: S.of(context).no,
|
||||
body: S.of(context).stopCastingBody,
|
||||
firstButtonOnTap: () async {
|
||||
gw.revokeAllTokens().ignore();
|
||||
await castService.closeActiveCasts();
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// stop any existing cast session
|
||||
gw.revokeAllTokens().ignore();
|
||||
final result = await showDialog<ButtonAction?>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext context) {
|
||||
return const CastChooseDialog();
|
||||
},
|
||||
);
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
// wait to allow the dialog to close
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
if (result == ButtonAction.first) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext context) {
|
||||
return AutoCastDialog(
|
||||
(device) async {
|
||||
await _castPair(gw, device);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
if (result == ButtonAction.second) {
|
||||
await _pairWithPin(gw, '');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pairWithPin(CastGateway gw, String code) async {
|
||||
await showTextInputDialog(
|
||||
context,
|
||||
title: context.l10n.playOnTv,
|
||||
|
@ -849,28 +771,49 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
submitButtonLabel: S.of(context).pair,
|
||||
textInputType: TextInputType.streetAddress,
|
||||
hintText: context.l10n.deviceCodeHint,
|
||||
showOnlyLoadingState: true,
|
||||
alwaysShowSuccessState: false,
|
||||
initialValue: code,
|
||||
onSubmit: (String text) async {
|
||||
try {
|
||||
final code = text.trim();
|
||||
final String? publicKey = await gw.getPublicKey(code);
|
||||
if (publicKey == null) {
|
||||
showToast(context, S.of(context).deviceNotFound);
|
||||
return;
|
||||
}
|
||||
final String castToken = const Uuid().v4().toString();
|
||||
final castPayload = CollectionsService.instance
|
||||
.getCastData(castToken, widget.collection!, publicKey);
|
||||
await gw.publishCastPayload(
|
||||
code,
|
||||
castPayload,
|
||||
widget.collection!.id,
|
||||
castToken,
|
||||
);
|
||||
} catch (e, s) {
|
||||
_logger.severe("Failed to cast album", e, s);
|
||||
await showGenericErrorDialog(context: context, error: e);
|
||||
final bool paired = await _castPair(gw, text);
|
||||
if (!paired) {
|
||||
Future.delayed(Duration.zero, () => _pairWithPin(gw, code));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _castPair(CastGateway gw, String code) async {
|
||||
try {
|
||||
final String? publicKey = await gw.getPublicKey(code);
|
||||
if (publicKey == null) {
|
||||
showToast(context, S.of(context).deviceNotFound);
|
||||
|
||||
return false;
|
||||
}
|
||||
final String castToken = const Uuid().v4().toString();
|
||||
final castPayload = CollectionsService.instance
|
||||
.getCastData(castToken, widget.collection!, publicKey);
|
||||
await gw.publishCastPayload(
|
||||
code,
|
||||
castPayload,
|
||||
widget.collection!.id,
|
||||
castToken,
|
||||
);
|
||||
showToast(context, S.of(context).pairingComplete);
|
||||
return true;
|
||||
} catch (e, s) {
|
||||
_logger.severe("Failed to cast album", e, s);
|
||||
if (e is CastIPMismatchException) {
|
||||
await showErrorDialog(
|
||||
context,
|
||||
S.of(context).castIPMismatchTitle,
|
||||
S.of(context).castIPMismatchBody,
|
||||
);
|
||||
} else {
|
||||
await showGenericErrorDialog(context: context, error: e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,14 +4,23 @@ import "package:computer/computer.dart";
|
|||
import 'package:dio/dio.dart';
|
||||
import "package:flutter/foundation.dart";
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as file_path;
|
||||
import "package:photo_manager/photo_manager.dart";
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import 'package:photos/core/network/network.dart';
|
||||
import "package:photos/db/files_db.dart";
|
||||
import "package:photos/events/local_photos_updated_event.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import "package:photos/models/file/file_type.dart";
|
||||
import "package:photos/models/ignored_file.dart";
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import "package:photos/services/ignored_files_service.dart";
|
||||
import "package:photos/services/local_sync_service.dart";
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import "package:photos/utils/data_util.dart";
|
||||
import "package:photos/utils/fake_progress.dart";
|
||||
import "package:photos/utils/file_util.dart";
|
||||
|
||||
final _logger = Logger("file_download_util");
|
||||
|
||||
|
@ -115,6 +124,97 @@ Future<Uint8List> getFileKeyUsingBgWorker(EnteFile file) async {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> downloadToGallery(EnteFile file) async {
|
||||
try {
|
||||
final FileType type = file.fileType;
|
||||
final bool downloadLivePhotoOnDroid =
|
||||
type == FileType.livePhoto && Platform.isAndroid;
|
||||
AssetEntity? savedAsset;
|
||||
final File? fileToSave = await getFile(file);
|
||||
//Disabling notifications for assets changing to insert the file into
|
||||
//files db before triggering a sync.
|
||||
await PhotoManager.stopChangeNotify();
|
||||
if (type == FileType.image) {
|
||||
savedAsset = await PhotoManager.editor
|
||||
.saveImageWithPath(fileToSave!.path, title: file.title!);
|
||||
} else if (type == FileType.video) {
|
||||
savedAsset =
|
||||
await PhotoManager.editor.saveVideo(fileToSave!, title: file.title!);
|
||||
} else if (type == FileType.livePhoto) {
|
||||
final File? liveVideoFile =
|
||||
await getFileFromServer(file, liveVideo: true);
|
||||
if (liveVideoFile == null) {
|
||||
throw AssertionError("Live video can not be null");
|
||||
}
|
||||
if (downloadLivePhotoOnDroid) {
|
||||
await _saveLivePhotoOnDroid(fileToSave!, liveVideoFile, file);
|
||||
} else {
|
||||
savedAsset = await PhotoManager.editor.darwin.saveLivePhoto(
|
||||
imageFile: fileToSave!,
|
||||
videoFile: liveVideoFile,
|
||||
title: file.title!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (savedAsset != null) {
|
||||
file.localID = savedAsset.id;
|
||||
await FilesDB.instance.insert(file);
|
||||
Bus.instance.fire(
|
||||
LocalPhotosUpdatedEvent(
|
||||
[file],
|
||||
source: "download",
|
||||
),
|
||||
);
|
||||
} else if (!downloadLivePhotoOnDroid && savedAsset == null) {
|
||||
_logger.severe('Failed to save assert of type $type');
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.severe("Failed to save file", e);
|
||||
rethrow;
|
||||
} finally {
|
||||
await PhotoManager.startChangeNotify();
|
||||
LocalSyncService.instance.checkAndSync().ignore();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveLivePhotoOnDroid(
|
||||
File image,
|
||||
File video,
|
||||
EnteFile enteFile,
|
||||
) async {
|
||||
debugPrint("Downloading LivePhoto on Droid");
|
||||
AssetEntity? savedAsset = await (PhotoManager.editor
|
||||
.saveImageWithPath(image.path, title: enteFile.title!));
|
||||
if (savedAsset == null) {
|
||||
throw Exception("Failed to save image of live photo");
|
||||
}
|
||||
IgnoredFile ignoreVideoFile = IgnoredFile(
|
||||
savedAsset.id,
|
||||
savedAsset.title ?? '',
|
||||
savedAsset.relativePath ?? 'remoteDownload',
|
||||
"remoteDownload",
|
||||
);
|
||||
await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]);
|
||||
final videoTitle = file_path.basenameWithoutExtension(enteFile.title!) +
|
||||
file_path.extension(video.path);
|
||||
savedAsset = (await (PhotoManager.editor.saveVideo(
|
||||
video,
|
||||
title: videoTitle,
|
||||
)));
|
||||
if (savedAsset == null) {
|
||||
throw Exception("Failed to save video of live photo");
|
||||
}
|
||||
|
||||
ignoreVideoFile = IgnoredFile(
|
||||
savedAsset.id,
|
||||
savedAsset.title ?? videoTitle,
|
||||
savedAsset.relativePath ?? 'remoteDownload',
|
||||
"remoteDownload",
|
||||
);
|
||||
await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]);
|
||||
}
|
||||
|
||||
Uint8List _decryptFileKey(Map<String, dynamic> args) {
|
||||
final encryptedKey = CryptoUtil.base642bin(args["encryptedKey"]);
|
||||
final nonce = CryptoUtil.base642bin(args["keyDecryptionNonce"]);
|
||||
|
|
|
@ -357,10 +357,16 @@ class FileUploader {
|
|||
final List<ConnectivityResult> connections =
|
||||
await (Connectivity().checkConnectivity());
|
||||
bool canUploadUnderCurrentNetworkConditions = true;
|
||||
if (connections.any((element) => element == ConnectivityResult.mobile)) {
|
||||
canUploadUnderCurrentNetworkConditions =
|
||||
Configuration.instance.shouldBackupOverMobileData();
|
||||
if (!Configuration.instance.shouldBackupOverMobileData()) {
|
||||
if (connections.any((element) => element == ConnectivityResult.mobile)) {
|
||||
canUploadUnderCurrentNetworkConditions = false;
|
||||
} else {
|
||||
_logger.info(
|
||||
"mobileBackupDisabled, backing up with connections: ${connections.map((e) => e.name).toString()}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!canUploadUnderCurrentNetworkConditions) {
|
||||
throw WiFiUnavailableError();
|
||||
}
|
||||
|
|
10
mobile/plugins/ente_cast/.metadata
Normal file
10
mobile/plugins/ente_cast/.metadata
Normal file
|
@ -0,0 +1,10 @@
|
|||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: 0b8abb4724aa590dd0f429683339b1e045a1594d
|
||||
channel: stable
|
||||
|
||||
project_type: plugin
|
1
mobile/plugins/ente_cast/analysis_options.yaml
Normal file
1
mobile/plugins/ente_cast/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
include: ../../analysis_options.yaml
|
2
mobile/plugins/ente_cast/lib/ente_cast.dart
Normal file
2
mobile/plugins/ente_cast/lib/ente_cast.dart
Normal file
|
@ -0,0 +1,2 @@
|
|||
export 'src/model.dart';
|
||||
export 'src/service.dart';
|
5
mobile/plugins/ente_cast/lib/src/model.dart
Normal file
5
mobile/plugins/ente_cast/lib/src/model.dart
Normal file
|
@ -0,0 +1,5 @@
|
|||
// create enum for type of message for cast
|
||||
enum CastMessageType {
|
||||
pairCode,
|
||||
alreadyCasting,
|
||||
}
|
18
mobile/plugins/ente_cast/lib/src/service.dart
Normal file
18
mobile/plugins/ente_cast/lib/src/service.dart
Normal file
|
@ -0,0 +1,18 @@
|
|||
import "package:ente_cast/src/model.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
|
||||
abstract class CastService {
|
||||
bool get isSupported;
|
||||
Future<List<(String, Object)>> searchDevices();
|
||||
Future<void> connectDevice(
|
||||
BuildContext context,
|
||||
Object device, {
|
||||
int? collectionID,
|
||||
// callback that take a map of string, dynamic
|
||||
void Function(Map<CastMessageType, Map<String, dynamic>>)? onMessage,
|
||||
});
|
||||
// returns a map of sessionID to deviceNames
|
||||
Map<String, String> getActiveSessions();
|
||||
|
||||
Future<void> closeActiveCasts();
|
||||
}
|
19
mobile/plugins/ente_cast/pubspec.yaml
Normal file
19
mobile/plugins/ente_cast/pubspec.yaml
Normal file
|
@ -0,0 +1,19 @@
|
|||
name: ente_cast
|
||||
version: 0.0.1
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
collection:
|
||||
dio: ^4.0.6
|
||||
flutter:
|
||||
sdk: flutter
|
||||
shared_preferences: ^2.0.5
|
||||
stack_trace:
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints:
|
||||
|
||||
flutter:
|
10
mobile/plugins/ente_cast_none/.metadata
Normal file
10
mobile/plugins/ente_cast_none/.metadata
Normal file
|
@ -0,0 +1,10 @@
|
|||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: 0b8abb4724aa590dd0f429683339b1e045a1594d
|
||||
channel: stable
|
||||
|
||||
project_type: plugin
|
1
mobile/plugins/ente_cast_none/analysis_options.yaml
Normal file
1
mobile/plugins/ente_cast_none/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
include: ../../analysis_options.yaml
|
1
mobile/plugins/ente_cast_none/lib/ente_cast_none.dart
Normal file
1
mobile/plugins/ente_cast_none/lib/ente_cast_none.dart
Normal file
|
@ -0,0 +1 @@
|
|||
export 'src/service.dart';
|
35
mobile/plugins/ente_cast_none/lib/src/service.dart
Normal file
35
mobile/plugins/ente_cast_none/lib/src/service.dart
Normal file
|
@ -0,0 +1,35 @@
|
|||
import "package:ente_cast/ente_cast.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
|
||||
class CastServiceImpl extends CastService {
|
||||
@override
|
||||
Future<void> connectDevice(
|
||||
BuildContext context,
|
||||
Object device, {
|
||||
int? collectionID,
|
||||
void Function(Map<CastMessageType, Map<String, dynamic>>)? onMessage,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isSupported => false;
|
||||
|
||||
@override
|
||||
Future<List<(String, Object)>> searchDevices() {
|
||||
// TODO: implement searchDevices
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> closeActiveCasts() {
|
||||
// TODO: implement closeActiveCasts
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> getActiveSessions() {
|
||||
// TODO: implement getActiveSessions
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
18
mobile/plugins/ente_cast_none/pubspec.yaml
Normal file
18
mobile/plugins/ente_cast_none/pubspec.yaml
Normal file
|
@ -0,0 +1,18 @@
|
|||
name: ente_cast_none
|
||||
version: 0.0.1
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
ente_cast:
|
||||
path: ../ente_cast
|
||||
flutter:
|
||||
sdk: flutter
|
||||
stack_trace:
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints:
|
||||
|
||||
flutter:
|
10
mobile/plugins/ente_cast_normal/.metadata
Normal file
10
mobile/plugins/ente_cast_normal/.metadata
Normal file
|
@ -0,0 +1,10 @@
|
|||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: 0b8abb4724aa590dd0f429683339b1e045a1594d
|
||||
channel: stable
|
||||
|
||||
project_type: plugin
|
1
mobile/plugins/ente_cast_normal/analysis_options.yaml
Normal file
1
mobile/plugins/ente_cast_normal/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
include: ../../analysis_options.yaml
|
|
@ -0,0 +1 @@
|
|||
export 'src/service.dart';
|
100
mobile/plugins/ente_cast_normal/lib/src/service.dart
Normal file
100
mobile/plugins/ente_cast_normal/lib/src/service.dart
Normal file
|
@ -0,0 +1,100 @@
|
|||
import "dart:developer" as dev;
|
||||
|
||||
import "package:cast/cast.dart";
|
||||
import "package:ente_cast/ente_cast.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class CastServiceImpl extends CastService {
|
||||
final String _appId = 'F5BCEC64';
|
||||
final String _pairRequestNamespace = 'urn:x-cast:pair-request';
|
||||
final Map<int, String> collectionIDToSessions = {};
|
||||
|
||||
@override
|
||||
Future<void> connectDevice(
|
||||
BuildContext context,
|
||||
Object device, {
|
||||
int? collectionID,
|
||||
void Function(Map<CastMessageType, Map<String, dynamic>>)? onMessage,
|
||||
}) async {
|
||||
final CastDevice castDevice = device as CastDevice;
|
||||
final session = await CastSessionManager().startSession(castDevice);
|
||||
session.messageStream.listen((message) {
|
||||
if (message['type'] == "RECEIVER_STATUS") {
|
||||
dev.log(
|
||||
"got RECEIVER_STATUS, Send request to pair",
|
||||
name: "CastServiceImpl",
|
||||
);
|
||||
session.sendMessage(_pairRequestNamespace, {});
|
||||
} else {
|
||||
if (onMessage != null && message.containsKey("code")) {
|
||||
onMessage(
|
||||
{
|
||||
CastMessageType.pairCode: message,
|
||||
},
|
||||
);
|
||||
}
|
||||
print('receive message: $message');
|
||||
}
|
||||
});
|
||||
|
||||
session.stateStream.listen((state) {
|
||||
if (state == CastSessionState.connected) {
|
||||
debugPrint("Send request to pair");
|
||||
session.sendMessage(_pairRequestNamespace, {});
|
||||
} else if (state == CastSessionState.closed) {
|
||||
dev.log('Session closed', name: 'CastServiceImpl');
|
||||
}
|
||||
});
|
||||
|
||||
debugPrint("Send request to launch");
|
||||
session.sendMessage(CastSession.kNamespaceReceiver, {
|
||||
'type': 'LAUNCH',
|
||||
'appId': _appId, // set the appId of your app here
|
||||
});
|
||||
// session.sendMessage('urn:x-cast:pair-request', {});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<(String, Object)>> searchDevices() {
|
||||
return CastDiscoveryService().search().then((devices) {
|
||||
return devices.map((device) => (device.name, device)).toList();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isSupported => true;
|
||||
|
||||
@override
|
||||
Future<void> closeActiveCasts() {
|
||||
final sessions = CastSessionManager().sessions;
|
||||
for (final session in sessions) {
|
||||
debugPrint("send close message for ${session.sessionId}");
|
||||
Future(() {
|
||||
session.sendMessage(CastSession.kNamespaceConnection, {
|
||||
'type': 'CLOSE',
|
||||
});
|
||||
}).timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () {
|
||||
debugPrint('sendMessage timed out after 5 seconds');
|
||||
},
|
||||
);
|
||||
debugPrint("close session ${session.sessionId}");
|
||||
session.close();
|
||||
}
|
||||
CastSessionManager().sessions.clear();
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> getActiveSessions() {
|
||||
final sessions = CastSessionManager().sessions;
|
||||
final Map<String, String> result = {};
|
||||
for (final session in sessions) {
|
||||
if (session.state == CastSessionState.connected) {
|
||||
result[session.sessionId] = session.state.toString();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
333
mobile/plugins/ente_cast_normal/pubspec.lock
Normal file
333
mobile/plugins/ente_cast_normal/pubspec.lock
Normal file
|
@ -0,0 +1,333 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
cast:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: multicast_version
|
||||
resolved-ref: "1f39cd4d6efa9363e77b2439f0317bae0c92dda1"
|
||||
url: "https://github.com/guyluz11/flutter_cast.git"
|
||||
source: git
|
||||
version: "2.0.9"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
dio:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio
|
||||
sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.6"
|
||||
ente_cast:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../ente_cast"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.0"
|
||||
multicast_dns:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: multicast_dns
|
||||
sha256: "316cc47a958d4bd3c67bd238fe8b44fdfb6133bad89cb191c0c3bd3edb14e296"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.2+6"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
protobuf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: protobuf
|
||||
sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
shared_preferences:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.5"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
stack_trace:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
sdks:
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.19.0"
|
22
mobile/plugins/ente_cast_normal/pubspec.yaml
Normal file
22
mobile/plugins/ente_cast_normal/pubspec.yaml
Normal file
|
@ -0,0 +1,22 @@
|
|||
name: ente_cast_normal
|
||||
version: 0.0.1
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
cast:
|
||||
git:
|
||||
url: https://github.com/guyluz11/flutter_cast.git
|
||||
ref: multicast_version
|
||||
ente_cast:
|
||||
path: ../ente_cast
|
||||
flutter:
|
||||
sdk: flutter
|
||||
stack_trace:
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints:
|
||||
|
||||
flutter:
|
|
@ -209,6 +209,15 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
cast:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "."
|
||||
ref: multicast_version
|
||||
resolved-ref: "1f39cd4d6efa9363e77b2439f0317bae0c92dda1"
|
||||
url: "https://github.com/guyluz11/flutter_cast.git"
|
||||
source: git
|
||||
version: "2.0.9"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -342,10 +351,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
|
||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
version: "1.0.8"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -354,14 +363,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
dartx:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dartx
|
||||
sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -434,6 +435,20 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.17"
|
||||
ente_cast:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "plugins/ente_cast"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
ente_cast_normal:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "plugins/ente_cast_normal"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
ente_feature_flag:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1093,30 +1108,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
isar:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: isar
|
||||
sha256: "99165dadb2cf2329d3140198363a7e7bff9bbd441871898a87e26914d25cf1ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0+1"
|
||||
isar_flutter_libs:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: isar_flutter_libs
|
||||
sha256: bc6768cc4b9c61aabff77152e7f33b4b17d2fc93134f7af1c3dd51500fe8d5e8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0+1"
|
||||
isar_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: isar_generator
|
||||
sha256: "76c121e1295a30423604f2f819bc255bc79f852f3bc8743a24017df6068ad133"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0+1"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1423,6 +1414,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
multicast_dns:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: multicast_dns
|
||||
sha256: "316cc47a958d4bd3c67bd238fe8b44fdfb6133bad89cb191c0c3bd3edb14e296"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.2+6"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1736,6 +1735,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
protobuf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: protobuf
|
||||
sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -2181,14 +2188,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.9"
|
||||
time:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: time
|
||||
sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -2558,14 +2557,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
xxh3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xxh3
|
||||
sha256: a92b30944a9aeb4e3d4f3c3d4ddb3c7816ca73475cd603682c4f8149690f56d7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -12,7 +12,7 @@ description: ente photos application
|
|||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
|
||||
version: 0.8.84+604
|
||||
version: 0.8.88+608
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
@ -39,7 +39,7 @@ dependencies:
|
|||
connectivity_plus: ^6.0.2
|
||||
cross_file: ^0.3.3
|
||||
crypto: ^3.0.2
|
||||
cupertino_icons: ^1.0.0
|
||||
cupertino_icons: ^1.0.8
|
||||
defer_pointer: ^0.0.2
|
||||
device_info_plus: ^9.0.3
|
||||
dio: ^4.0.6
|
||||
|
@ -47,6 +47,10 @@ dependencies:
|
|||
dotted_border: ^2.1.0
|
||||
dropdown_button2: ^2.0.0
|
||||
email_validator: ^2.0.1
|
||||
ente_cast:
|
||||
path: plugins/ente_cast
|
||||
ente_cast_normal:
|
||||
path: plugins/ente_cast_normal
|
||||
ente_feature_flag:
|
||||
path: plugins/ente_feature_flag
|
||||
equatable: ^2.0.5
|
||||
|
@ -95,8 +99,6 @@ dependencies:
|
|||
image_editor: ^1.3.0
|
||||
in_app_purchase: ^3.0.7
|
||||
intl: ^0.18.0
|
||||
isar: ^3.1.0+1
|
||||
isar_flutter_libs: ^3.1.0+1
|
||||
json_annotation: ^4.8.0
|
||||
latlong2: ^0.9.0
|
||||
like_button: ^2.0.5
|
||||
|
@ -192,7 +194,6 @@ dev_dependencies:
|
|||
freezed: ^2.5.2
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
isar_generator: ^3.1.0+1
|
||||
json_serializable: ^6.6.1
|
||||
test: ^1.22.0
|
||||
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
# TODO: add `rustup@1.25.2` to `srclibs`
|
||||
# TODO: verify if `gcc-multilib` or `libc-dev` is needed
|
||||
$$rustup$$/rustup-init.sh -y
|
||||
source $HOME/.cargo/env
|
||||
cd thirdparty/isar/
|
||||
bash tool/build_android.sh x86
|
||||
bash tool/build_android.sh x64
|
||||
bash tool/build_android.sh armv7
|
||||
bash tool/build_android.sh arm64
|
||||
mv libisar_android_arm64.so libisar.so
|
||||
mv libisar.so $PUB_CACHE/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/arm64-v8a/
|
||||
mv libisar_android_armv7.so libisar.so
|
||||
mv libisar.so $PUB_CACHE/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/armeabi-v7a/
|
||||
mv libisar_android_x64.so libisar.so
|
||||
mv libisar.so $PUB_CACHE/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/x86_64/
|
||||
mv libisar_android_x86.so libisar.so
|
||||
mv libisar.so $PUB_CACHE/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/x86/
|
|
@ -45,7 +45,7 @@ require you to clone the repository or build any images.
|
|||
+ image: ghcr.io/ente-io/server
|
||||
```
|
||||
|
||||
4. Create an (empty) configuration file. Yyou can later put your custom
|
||||
4. Create an (empty) configuration file. You can later put your custom
|
||||
configuration in this if needed.
|
||||
|
||||
```sh
|
||||
|
|
|
@ -9,8 +9,7 @@ type CastRequest struct {
|
|||
}
|
||||
|
||||
type RegisterDeviceRequest struct {
|
||||
DeviceCode *string `json:"deviceCode"`
|
||||
PublicKey string `json:"publicKey" binding:"required"`
|
||||
PublicKey string `json:"publicKey" binding:"required"`
|
||||
}
|
||||
|
||||
type AuthContext struct {
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
entity "github.com/ente-io/museum/ente/cast"
|
||||
"github.com/ente-io/museum/pkg/controller/cast"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/ente-io/museum/ente"
|
||||
entity "github.com/ente-io/museum/ente/cast"
|
||||
"github.com/ente-io/museum/pkg/controller"
|
||||
"github.com/ente-io/museum/pkg/controller/cast"
|
||||
"github.com/ente-io/museum/pkg/utils/handler"
|
||||
"github.com/ente-io/stacktrace"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CastHandler exposes request handlers for publicly accessible collections
|
||||
|
@ -126,7 +126,7 @@ func (h *CastHandler) GetDiff(c *gin.Context) {
|
|||
}
|
||||
|
||||
func getDeviceCode(c *gin.Context) string {
|
||||
return c.Param("deviceCode")
|
||||
return strings.ToUpper(c.Param("deviceCode"))
|
||||
}
|
||||
|
||||
func (h *CastHandler) getFileForType(c *gin.Context, objectType ente.ObjectType) {
|
||||
|
|
|
@ -2,7 +2,6 @@ package cast
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/ente-io/museum/ente"
|
||||
"github.com/ente-io/museum/ente/cast"
|
||||
"github.com/ente-io/museum/pkg/controller/access"
|
||||
castRepo "github.com/ente-io/museum/pkg/repo/cast"
|
||||
|
@ -28,7 +27,7 @@ func NewController(castRepo *castRepo.Repository,
|
|||
}
|
||||
|
||||
func (c *Controller) RegisterDevice(ctx *gin.Context, request *cast.RegisterDeviceRequest) (string, error) {
|
||||
return c.CastRepo.AddCode(ctx, request.DeviceCode, request.PublicKey, network.GetClientIP(ctx))
|
||||
return c.CastRepo.AddCode(ctx, request.PublicKey, network.GetClientIP(ctx))
|
||||
}
|
||||
|
||||
func (c *Controller) GetPublicKey(ctx *gin.Context, deviceCode string) (string, error) {
|
||||
|
@ -42,7 +41,6 @@ func (c *Controller) GetPublicKey(ctx *gin.Context, deviceCode string) (string,
|
|||
"ip": ip,
|
||||
"clientIP": network.GetClientIP(ctx),
|
||||
}).Warn("GetPublicKey: IP mismatch")
|
||||
return "", &ente.ErrCastIPMismatch
|
||||
}
|
||||
return pubKey, nil
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package storagebonus
|
|||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ente-io/museum/pkg/utils/random"
|
||||
|
||||
"github.com/ente-io/museum/ente"
|
||||
entity "github.com/ente-io/museum/ente/storagebonus"
|
||||
|
@ -119,7 +119,7 @@ func (c *Controller) GetOrCreateReferralCode(ctx *gin.Context, userID int64) (*s
|
|||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, stacktrace.Propagate(err, "failed to get storagebonus code")
|
||||
}
|
||||
code, err := generateAlphaNumString(codeLength)
|
||||
code, err := random.GenerateAlphaNumString(codeLength)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
}
|
||||
|
@ -131,30 +131,3 @@ func (c *Controller) GetOrCreateReferralCode(ctx *gin.Context, userID int64) (*s
|
|||
}
|
||||
return referralCode, nil
|
||||
}
|
||||
|
||||
// generateAlphaNumString returns AlphaNumeric code of given length
|
||||
// which exclude number 0 and letter O. The code always starts with an
|
||||
// alphabet
|
||||
func generateAlphaNumString(length int) (string, error) {
|
||||
// Define the alphabet and numbers to be used in the string.
|
||||
alphabet := "ABCDEFGHIJKLMNPQRSTUVWXYZ"
|
||||
// Define the alphabet and numbers to be used in the string.
|
||||
alphaNum := fmt.Sprintf("%s123456789", alphabet)
|
||||
// Allocate a byte slice with the desired length.
|
||||
result := make([]byte, length)
|
||||
// Generate the first letter as an alphabet.
|
||||
r0, err := auth.GenerateRandomInt(int64(len(alphabet)))
|
||||
if err != nil {
|
||||
return "", stacktrace.Propagate(err, "")
|
||||
}
|
||||
result[0] = alphabet[r0]
|
||||
// Generate the remaining characters as alphanumeric.
|
||||
for i := 1; i < length; i++ {
|
||||
ri, err := auth.GenerateRandomInt(int64(len(alphaNum)))
|
||||
if err != nil {
|
||||
return "", stacktrace.Propagate(err, "")
|
||||
}
|
||||
result[i] = alphaNum[ri]
|
||||
}
|
||||
return string(result), nil
|
||||
}
|
||||
|
|
|
@ -8,23 +8,16 @@ import (
|
|||
"github.com/ente-io/stacktrace"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
func (r *Repository) AddCode(ctx context.Context, code *string, pubKey string, ip string) (string, error) {
|
||||
var codeValue string
|
||||
var err error
|
||||
if code == nil || *code == "" {
|
||||
codeValue, err = random.GenerateSixDigitOtp()
|
||||
if err != nil {
|
||||
return "", stacktrace.Propagate(err, "")
|
||||
}
|
||||
} else {
|
||||
codeValue = strings.TrimSpace(*code)
|
||||
func (r *Repository) AddCode(ctx context.Context, pubKey string, ip string) (string, error) {
|
||||
codeValue, err := random.GenerateAlphaNumString(6)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = r.DB.ExecContext(ctx, "INSERT INTO casting (code, public_key, id, ip) VALUES ($1, $2, $3, $4)", codeValue, pubKey, uuid.New(), ip)
|
||||
if err != nil {
|
||||
|
|
|
@ -13,3 +13,30 @@ func GenerateSixDigitOtp() (string, error) {
|
|||
}
|
||||
return fmt.Sprintf("%06d", n), nil
|
||||
}
|
||||
|
||||
// GenerateAlphaNumString returns AlphaNumeric code of given length
|
||||
// which exclude number 0 and letter O. The code always starts with an
|
||||
// alphabet
|
||||
func GenerateAlphaNumString(length int) (string, error) {
|
||||
// Define the alphabet and numbers to be used in the string.
|
||||
alphabet := "ABCDEFGHIJKLMNPQRSTUVWXYZ"
|
||||
// Define the alphabet and numbers to be used in the string.
|
||||
alphaNum := fmt.Sprintf("%s123456789", alphabet)
|
||||
// Allocate a byte slice with the desired length.
|
||||
result := make([]byte, length)
|
||||
// Generate the first letter as an alphabet.
|
||||
r0, err := auth.GenerateRandomInt(int64(len(alphabet)))
|
||||
if err != nil {
|
||||
return "", stacktrace.Propagate(err, "")
|
||||
}
|
||||
result[0] = alphabet[r0]
|
||||
// Generate the remaining characters as alphanumeric.
|
||||
for i := 1; i < length; i++ {
|
||||
ri, err := auth.GenerateRandomInt(int64(len(alphaNum)))
|
||||
if err != nil {
|
||||
return "", stacktrace.Propagate(err, "")
|
||||
}
|
||||
result[i] = alphaNum[ri]
|
||||
}
|
||||
return string(result), nil
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue