mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 11:58:41 +00:00
Compare commits
26 Commits
v0.6.0-can
...
v0.6.0-can
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
627d8ef787 | ||
|
|
5563823a7a | ||
|
|
d6804bb0fd | ||
|
|
1350633690 | ||
|
|
50196d8fde | ||
|
|
2e0ccb53ec | ||
|
|
1498ee405b | ||
|
|
cb863c4afa | ||
|
|
2629d39501 | ||
|
|
38305cd984 | ||
|
|
93116c24f2 | ||
|
|
017b9c8615 | ||
|
|
9ce3a96862 | ||
|
|
a0ff520ba4 | ||
|
|
a8b8986d89 | ||
|
|
8ffc096fee | ||
|
|
7e457f7b4c | ||
|
|
aedf2d339e | ||
|
|
ffd5ae52b3 | ||
|
|
3093194da8 | ||
|
|
68b4f792f0 | ||
|
|
e2c6e4f9fc | ||
|
|
9ff7dbffb7 | ||
|
|
0c561da061 | ||
|
|
06951319a6 | ||
|
|
0bfcab4067 |
6
.github/actions/build-rust/action.yml
vendored
6
.github/actions/build-rust/action.yml
vendored
@@ -29,13 +29,15 @@ runs:
|
||||
if: ${{ inputs.target != 'x86_64-unknown-linux-gnu' && inputs.target != 'aarch64-unknown-linux-gnu' }}
|
||||
shell: bash
|
||||
run: yarn workspace @affine/native build --target ${{ inputs.target }}
|
||||
env:
|
||||
CARGO_BUILD_INCREMENTAL: 'false'
|
||||
|
||||
- name: Build
|
||||
if: ${{ inputs.target == 'x86_64-unknown-linux-gnu' }}
|
||||
uses: addnab/docker-run-action@v3
|
||||
with:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
|
||||
options: --user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build
|
||||
options: --user 0:0 -e CARGO_BUILD_INCREMENTAL=false -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build
|
||||
run: yarn workspace @affine/native build --target ${{ inputs.target }}
|
||||
|
||||
- name: Build
|
||||
@@ -43,5 +45,5 @@ runs:
|
||||
uses: addnab/docker-run-action@v3
|
||||
with:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
|
||||
options: --user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build
|
||||
options: --user 0:0 -e CARGO_BUILD_INCREMENTAL=false -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build
|
||||
run: yarn workspace @affine/native build --target ${{ inputs.target }}
|
||||
|
||||
48
.github/workflows/build.yml
vendored
48
.github/workflows/build.yml
vendored
@@ -21,7 +21,6 @@ on:
|
||||
- '!.github/workflows/build.yml'
|
||||
|
||||
env:
|
||||
CARGO_BUILD_INCREMENTAL: 'false'
|
||||
DEBUG: napi:*
|
||||
APP_NAME: affine
|
||||
MACOSX_DEPLOYMENT_TARGET: '10.13'
|
||||
@@ -55,23 +54,6 @@ jobs:
|
||||
path: ./packages/component/storybook-static
|
||||
if-no-files-found: error
|
||||
|
||||
build-electron:
|
||||
name: Build @affine/electron
|
||||
runs-on: ubuntu-latest
|
||||
environment: development
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Build Electron
|
||||
working-directory: apps/electron
|
||||
run: yarn build-layers
|
||||
- name: Upload Ubuntu desktop artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: affine-ubuntu
|
||||
path: ./apps/electron/dist
|
||||
|
||||
build:
|
||||
name: Build @affine/web
|
||||
runs-on: ubuntu-latest
|
||||
@@ -322,7 +304,7 @@ jobs:
|
||||
target: x86_64-pc-windows-msvc,
|
||||
test: true,
|
||||
}
|
||||
needs: [build, build-electron]
|
||||
needs: [build]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
@@ -333,11 +315,17 @@ jobs:
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
target: ${{ matrix.spec.target }}
|
||||
- name: Download Ubuntu desktop artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: affine-ubuntu
|
||||
path: ./apps/electron/dist
|
||||
- name: Run unit tests
|
||||
if: ${{ matrix.spec.test }}
|
||||
shell: bash
|
||||
run: |
|
||||
rm -rf apps/electron/node_modules/better-sqlite3/build
|
||||
yarn --cwd apps/electron/node_modules/better-sqlite3 run install
|
||||
yarn test:unit
|
||||
env:
|
||||
NATIVE_TEST: 'true'
|
||||
- name: Build layers
|
||||
run: yarn workspace @affine/electron build-layers
|
||||
|
||||
- name: Download static resource artifact
|
||||
uses: actions/download-artifact@v3
|
||||
@@ -346,20 +334,20 @@ jobs:
|
||||
path: ./apps/electron/resources/web-static
|
||||
|
||||
- name: Rebuild Electron dependences
|
||||
run: yarn rebuild:for-electron
|
||||
working-directory: apps/electron
|
||||
shell: bash
|
||||
run: |
|
||||
rm -rf apps/electron/node_modules/better-sqlite3/build
|
||||
yarn workspace @affine/electron rebuild:for-electron --arch=${{ matrix.spec.arch }}
|
||||
|
||||
- name: Run desktop tests
|
||||
if: ${{ matrix.spec.test && matrix.spec.os == 'ubuntu-latest' }}
|
||||
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test
|
||||
working-directory: apps/electron
|
||||
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine/electron test
|
||||
env:
|
||||
COVERAGE: true
|
||||
|
||||
- name: Run desktop tests
|
||||
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
|
||||
run: yarn test
|
||||
working-directory: apps/electron
|
||||
run: yarn workspace @affine/electron test
|
||||
env:
|
||||
COVERAGE: true
|
||||
|
||||
|
||||
111
.github/workflows/nightly-build.yml
vendored
111
.github/workflows/nightly-build.yml
vendored
@@ -22,18 +22,30 @@ concurrency:
|
||||
|
||||
env:
|
||||
BUILD_TYPE: internal
|
||||
RELEASE_VERSION: ${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
jobs:
|
||||
set-build-version:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
outputs:
|
||||
version: 0.0.0-${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: toeverything/set-build-version@latest
|
||||
- id: version
|
||||
run: echo ::set-output name=version::${{ env.BUILD_VERSION }}
|
||||
|
||||
before-make:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
needs:
|
||||
- set-build-version
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Replace Version
|
||||
run: ./scripts/set-version.sh ${{ env.RELEASE_VERSION }}
|
||||
run: ./scripts/set-version.sh ${{ needs.set-build-version.outputs.version }}
|
||||
- name: generate-assets
|
||||
working-directory: apps/electron
|
||||
run: yarn generate-assets
|
||||
@@ -53,6 +65,7 @@ jobs:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
API_SERVER_PROFILE: prod
|
||||
ENABLE_TEST_PROPERTIES: false
|
||||
ENABLE_IMAGE_PREVIEW_MODAL: false
|
||||
|
||||
- name: Upload Artifact (web-static)
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -60,30 +73,40 @@ jobs:
|
||||
name: before-make-web-static
|
||||
path: apps/electron/resources/web-static
|
||||
|
||||
- name: Upload Artifact (electron dist)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: before-make-electron-dist
|
||||
path: apps/electron/dist
|
||||
|
||||
- name: Upload YML Build Script
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: release-yml-build-script
|
||||
path: apps/electron/scripts/generate-yml.js
|
||||
|
||||
make-distribution:
|
||||
environment: production
|
||||
strategy:
|
||||
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
|
||||
matrix:
|
||||
spec:
|
||||
- { os: macos-latest, platform: macos, arch: x64 }
|
||||
- { os: macos-latest, platform: macos, arch: arm64 }
|
||||
- { os: ubuntu-latest, platform: linux, arch: x64 }
|
||||
- { os: windows-latest, platform: windows, arch: x64 }
|
||||
- {
|
||||
os: macos-latest,
|
||||
platform: darwin,
|
||||
arch: x64,
|
||||
target: x86_64-apple-darwin,
|
||||
}
|
||||
- {
|
||||
os: macos-latest,
|
||||
platform: darwin,
|
||||
arch: arm64,
|
||||
target: aarch64-apple-darwin,
|
||||
}
|
||||
- {
|
||||
os: ubuntu-latest,
|
||||
platform: linux,
|
||||
arch: x64,
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
}
|
||||
- {
|
||||
os: windows-latest,
|
||||
platform: win32,
|
||||
arch: x64,
|
||||
target: x86_64-pc-windows-msvc,
|
||||
}
|
||||
runs-on: ${{ matrix.spec.os }}
|
||||
needs: before-make
|
||||
needs:
|
||||
- before-make
|
||||
- set-build-version
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
@@ -93,36 +116,43 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Build AFFiNE native
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
target: ${{ matrix.spec.target }}
|
||||
- name: Replace Version
|
||||
run: ./scripts/set-version.sh ${{ env.RELEASE_VERSION }}
|
||||
run: ./scripts/set-version.sh ${{ needs.set-build-version.outputs.version }}
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: before-make-web-static
|
||||
path: apps/electron/resources/web-static
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: before-make-electron-dist
|
||||
path: apps/electron/dist
|
||||
- name: Rebuild Electron dependences
|
||||
shell: bash
|
||||
run: |
|
||||
rm -rf apps/electron/node_modules/better-sqlite3/build
|
||||
yarn workspace @affine/electron rebuild:for-electron --arch=${{ matrix.spec.arch }}
|
||||
|
||||
- name: Build layers
|
||||
run: yarn workspace @affine/electron build-layers
|
||||
|
||||
- name: Signing By Apple Developer ID
|
||||
if: ${{ matrix.spec.platform == 'macos' }}
|
||||
if: ${{ matrix.spec.platform == 'darwin' }}
|
||||
uses: apple-actions/import-codesign-certs@v2
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
|
||||
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
|
||||
|
||||
- name: make
|
||||
run: yarn make-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
||||
working-directory: apps/electron
|
||||
run: yarn workspace @affine/electron make --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
|
||||
|
||||
- name: Save artifacts (mac)
|
||||
if: ${{ matrix.spec.platform == 'macos' }}
|
||||
if: ${{ matrix.spec.platform == 'darwin' }}
|
||||
run: |
|
||||
mkdir -p builds
|
||||
mv apps/electron/out/*/make/*.dmg ./builds/affine-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.dmg
|
||||
mv apps/electron/out/*/make/zip/darwin/${{ matrix.spec.arch }}/*.zip ./builds/affine-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.zip
|
||||
- name: Save artifacts (windows)
|
||||
if: ${{ matrix.spec.platform == 'windows' }}
|
||||
if: ${{ matrix.spec.platform == 'win32' }}
|
||||
run: |
|
||||
mkdir -p builds
|
||||
mv apps/electron/out/*/make/zip/win32/x64/AFFiNE*-win32-x64-*.zip ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.zip
|
||||
@@ -143,52 +173,51 @@ jobs:
|
||||
path: builds
|
||||
|
||||
release:
|
||||
needs: make-distribution
|
||||
needs:
|
||||
- make-distribution
|
||||
- set-build-version
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Download Artifacts (macos-x64)
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: affine-macos-x64-builds
|
||||
name: affine-darwin-x64-builds
|
||||
path: ./
|
||||
- name: Download Artifacts (macos-arm64)
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: affine-macos-arm64-builds
|
||||
name: affine-darwin-arm64-builds
|
||||
path: ./
|
||||
- name: Download Artifacts (windows-x64)
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: affine-windows-x64-builds
|
||||
name: affine-win32-x64-builds
|
||||
path: ./
|
||||
- name: Download Artifacts (linux-x64)
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: affine-linux-x64-builds
|
||||
path: ./
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: release-yml-build-script
|
||||
path: ./
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Generate Release yml
|
||||
run: |
|
||||
cp ./apps/electron/scripts/generate-yml.js .
|
||||
node generate-yml.js
|
||||
env:
|
||||
RELEASE_VERSION: ${{ env.RELEASE_VERSION }}
|
||||
RELEASE_VERSION: ${{ needs.set-build-version.outputs.version }}
|
||||
- name: Create Release Draft
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
with:
|
||||
repository: 'toeverything/AFFiNE-Releases'
|
||||
name: ${{ env.RELEASE_VERSION }}
|
||||
tag_name: ${{ env.RELEASE_VERSION }}
|
||||
name: ${{ needs.set-build-version.outputs.version }}
|
||||
tag_name: ${{ needs.set-build-version.outputs.version }}
|
||||
prerelease: true
|
||||
files: |
|
||||
./VERSION
|
||||
|
||||
62
.github/workflows/release-desktop-app.yml
vendored
62
.github/workflows/release-desktop-app.yml
vendored
@@ -36,7 +36,6 @@ concurrency:
|
||||
|
||||
env:
|
||||
BUILD_TYPE: ${{ github.event.inputs.build-type }}
|
||||
CARGO_BUILD_INCREMENTAL: 'false'
|
||||
DEBUG: napi:*
|
||||
APP_NAME: affine
|
||||
MACOSX_DEPLOYMENT_TARGET: '10.13'
|
||||
@@ -50,8 +49,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: generate-assets
|
||||
working-directory: apps/electron
|
||||
run: yarn generate-assets
|
||||
run: yarn workspace @affine/electron generate-assets
|
||||
env:
|
||||
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
|
||||
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
|
||||
@@ -68,6 +66,7 @@ jobs:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
API_SERVER_PROFILE: prod
|
||||
ENABLE_TEST_PROPERTIES: false
|
||||
ENABLE_IMAGE_PREVIEW_MODAL: false
|
||||
|
||||
- name: Upload Artifact (web-static)
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -75,18 +74,6 @@ jobs:
|
||||
name: before-make-web-static
|
||||
path: apps/electron/resources/web-static
|
||||
|
||||
- name: Upload Artifact (electron dist)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: before-make-electron-dist
|
||||
path: apps/electron/dist
|
||||
|
||||
- name: Upload YML Build Script
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: release-yml-build-script
|
||||
path: apps/electron/scripts/generate-yml.js
|
||||
|
||||
make-distribution:
|
||||
environment: ${{ github.ref_name == 'master' && 'production' || 'development' }}
|
||||
strategy:
|
||||
@@ -95,13 +82,13 @@ jobs:
|
||||
spec:
|
||||
- {
|
||||
os: macos-latest,
|
||||
platform: macos,
|
||||
platform: darwin,
|
||||
arch: x64,
|
||||
target: x86_64-apple-darwin,
|
||||
}
|
||||
- {
|
||||
os: macos-latest,
|
||||
platform: macos,
|
||||
platform: darwin,
|
||||
arch: arm64,
|
||||
target: aarch64-apple-darwin,
|
||||
}
|
||||
@@ -113,7 +100,7 @@ jobs:
|
||||
}
|
||||
- {
|
||||
os: windows-latest,
|
||||
platform: windows,
|
||||
platform: win32,
|
||||
arch: x64,
|
||||
target: x86_64-pc-windows-msvc,
|
||||
}
|
||||
@@ -136,30 +123,34 @@ jobs:
|
||||
with:
|
||||
name: before-make-web-static
|
||||
path: apps/electron/resources/web-static
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: before-make-electron-dist
|
||||
path: apps/electron/dist
|
||||
|
||||
- name: Rebuild Electron dependences
|
||||
shell: bash
|
||||
run: |
|
||||
rm -rf apps/electron/node_modules/better-sqlite3/build
|
||||
yarn workspace @affine/electron rebuild:for-electron --arch=${{ matrix.spec.arch }}
|
||||
|
||||
- name: Build layers
|
||||
run: yarn workspace @affine/electron build-layers
|
||||
|
||||
- name: Signing By Apple Developer ID
|
||||
if: ${{ matrix.spec.platform == 'macos' }}
|
||||
if: ${{ matrix.spec.platform == 'darwin' }}
|
||||
uses: apple-actions/import-codesign-certs@v2
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
|
||||
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
|
||||
|
||||
- name: make
|
||||
run: yarn make-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
||||
working-directory: apps/electron
|
||||
run: yarn workspace @affine/electron make --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
|
||||
|
||||
- name: Save artifacts (mac)
|
||||
if: ${{ matrix.spec.platform == 'macos' }}
|
||||
if: ${{ matrix.spec.platform == 'darwin' }}
|
||||
run: |
|
||||
mkdir -p builds
|
||||
mv apps/electron/out/*/make/*.dmg ./builds/affine-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.dmg
|
||||
mv apps/electron/out/*/make/zip/darwin/${{ matrix.spec.arch }}/*.zip ./builds/affine-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.zip
|
||||
- name: Save artifacts (windows)
|
||||
if: ${{ matrix.spec.platform == 'windows' }}
|
||||
if: ${{ matrix.spec.platform == 'win32' }}
|
||||
run: |
|
||||
mkdir -p builds
|
||||
mv apps/electron/out/*/make/zip/win32/x64/AFFiNE*-win32-x64-*.zip ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.zip
|
||||
@@ -184,37 +175,36 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Download Artifacts (macos-x64)
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: affine-macos-x64-builds
|
||||
name: affine-darwin-x64-builds
|
||||
path: ./
|
||||
- name: Download Artifacts (macos-arm64)
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: affine-macos-arm64-builds
|
||||
name: affine-darwin-arm64-builds
|
||||
path: ./
|
||||
- name: Download Artifacts (windows-x64)
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: affine-windows-x64-builds
|
||||
name: affine-win32-x64-builds
|
||||
path: ./
|
||||
- name: Download Artifacts (linux-x64)
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: affine-linux-x64-builds
|
||||
path: ./
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: release-yml-build-script
|
||||
path: ./
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Generate Release yml
|
||||
run: |
|
||||
RELEASE_VERSION=${{ github.event.inputs.version }} node generate-yml.js
|
||||
cp ./apps/electron/scripts/generate-yml.js .
|
||||
node generate-yml.js
|
||||
env:
|
||||
RELEASE_VERSION: ${{ github.event.inputs.version }}
|
||||
- name: Create Release Draft
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -37,5 +37,6 @@
|
||||
"apps/electron/layers/**/*.spec.ts",
|
||||
"tests/unit/**/*.spec.ts",
|
||||
"tests/unit/**/*.spec.tsx"
|
||||
]
|
||||
],
|
||||
"deepscan.enable": true
|
||||
}
|
||||
|
||||
44
Cargo.lock
generated
44
Cargo.lock
generated
@@ -11,6 +11,7 @@ dependencies = [
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"notify",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -51,6 +52,12 @@ version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@@ -498,12 +505,31 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
@@ -533,11 +559,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.8"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { app, Menu } from 'electron';
|
||||
|
||||
import { isMacOS } from '../../utils';
|
||||
import { subjects } from './events';
|
||||
import { checkForUpdatesAndNotify } from './handlers/updater';
|
||||
|
||||
// Unique id for menuitems
|
||||
const MENUITEM_NEW_PAGE = 'affine:new-page';
|
||||
@@ -114,6 +115,12 @@ export function createApplicationMenu() {
|
||||
await shell.openExternal('https://affine.pro/');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Check for Updates',
|
||||
click: async () => {
|
||||
await checkForUpdatesAndNotify(true);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,19 +1,34 @@
|
||||
import { Subject } from 'rxjs';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
|
||||
import type { MainEventListener } from './type';
|
||||
|
||||
interface UpdateMeta {
|
||||
version: string;
|
||||
allowAutoUpdate: boolean;
|
||||
}
|
||||
|
||||
export const updaterSubjects = {
|
||||
// means it is ready for restart and install the new version
|
||||
clientUpdateReady: new Subject<UpdateMeta>(),
|
||||
updateAvailable: new Subject<UpdateMeta>(),
|
||||
updateReady: new Subject<UpdateMeta>(),
|
||||
downloadProgress: new BehaviorSubject<number>(0),
|
||||
};
|
||||
|
||||
export const updaterEvents = {
|
||||
onClientUpdateReady: (fn: (versionMeta: UpdateMeta) => void) => {
|
||||
const sub = updaterSubjects.clientUpdateReady.subscribe(fn);
|
||||
onUpdateAvailable: (fn: (versionMeta: UpdateMeta) => void) => {
|
||||
const sub = updaterSubjects.updateAvailable.subscribe(fn);
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
},
|
||||
onUpdateReady: (fn: (versionMeta: UpdateMeta) => void) => {
|
||||
const sub = updaterSubjects.updateReady.subscribe(fn);
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
},
|
||||
onDownloadProgress: (fn: (progress: number) => void) => {
|
||||
const sub = updaterSubjects.downloadProgress.subscribe(fn);
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@ import assert from 'node:assert';
|
||||
import path from 'node:path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import type { Subscription } from 'rxjs';
|
||||
import { v4 } from 'uuid';
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
@@ -99,6 +101,11 @@ const electronModule = {
|
||||
handlers.push(callback);
|
||||
registeredHandlers.set(name, handlers);
|
||||
},
|
||||
addEventListener: (...args: any[]) => {
|
||||
// @ts-ignore
|
||||
electronModule.app.on(...args);
|
||||
},
|
||||
removeEventListener: () => {},
|
||||
},
|
||||
BrowserWindow: {
|
||||
getAllWindows: () => {
|
||||
@@ -116,6 +123,8 @@ vi.doMock('electron', () => {
|
||||
return electronModule;
|
||||
});
|
||||
|
||||
let connectableSubscription: Subscription;
|
||||
|
||||
beforeEach(async () => {
|
||||
const { registerHandlers } = await import('../register');
|
||||
registerHandlers();
|
||||
@@ -123,20 +132,24 @@ beforeEach(async () => {
|
||||
// should also register events
|
||||
const { registerEvents } = await import('../../events');
|
||||
registerEvents();
|
||||
await fs.mkdirp(SESSION_DATA_PATH);
|
||||
const { database$ } = await import('../db/ensure-db');
|
||||
|
||||
connectableSubscription = database$.connect();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const { cleanupSQLiteDBs } = await import('../db/ensure-db');
|
||||
await cleanupSQLiteDBs();
|
||||
await fs.remove(SESSION_DATA_PATH);
|
||||
|
||||
// reset registered handlers
|
||||
registeredHandlers.get('before-quit')?.forEach(fn => fn());
|
||||
|
||||
connectableSubscription.unsubscribe();
|
||||
|
||||
await fs.remove(SESSION_DATA_PATH);
|
||||
});
|
||||
|
||||
describe('ensureSQLiteDB', () => {
|
||||
test('should create db file on connection if it does not exist', async () => {
|
||||
const id = 'test-workspace-id';
|
||||
const id = v4();
|
||||
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||
const workspaceDB = await ensureSQLiteDB(id);
|
||||
const file = workspaceDB.path;
|
||||
@@ -146,70 +159,76 @@ describe('ensureSQLiteDB', () => {
|
||||
|
||||
test('when db file is removed', async () => {
|
||||
// stub webContents.send
|
||||
const sendStub = vi.fn();
|
||||
browserWindow.webContents.send = sendStub;
|
||||
const id = 'test-workspace-id';
|
||||
const sendSpy = vi.spyOn(browserWindow.webContents, 'send');
|
||||
const id = v4();
|
||||
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||
let workspaceDB = await ensureSQLiteDB(id);
|
||||
const file = workspaceDB.path;
|
||||
const fileExists = await fs.pathExists(file);
|
||||
expect(fileExists).toBe(true);
|
||||
|
||||
// Can't remove file on Windows, because the sqlite is still holding the file handle
|
||||
if (process.platform === 'win32') {
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.remove(file);
|
||||
|
||||
// wait for 1000ms for file watcher to detect file removal
|
||||
// wait for 2000ms for file watcher to detect file removal
|
||||
await delay(2000);
|
||||
|
||||
expect(sendStub).toBeCalledWith('db:onDBFileMissing', id);
|
||||
expect(sendSpy).toBeCalledWith('db:onDBFileMissing', id);
|
||||
|
||||
// ensureSQLiteDB should recreate the db file
|
||||
workspaceDB = await ensureSQLiteDB(id);
|
||||
const fileExists2 = await fs.pathExists(file);
|
||||
expect(fileExists2).toBe(true);
|
||||
sendSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('when db file is updated', async () => {
|
||||
// stub webContents.send
|
||||
const sendStub = vi.fn();
|
||||
browserWindow.webContents.send = sendStub;
|
||||
const id = 'test-workspace-id';
|
||||
const id = v4();
|
||||
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||
const { dbSubjects } = await import('../../events/db');
|
||||
const workspaceDB = await ensureSQLiteDB(id);
|
||||
const file = workspaceDB.path;
|
||||
const fileExists = await fs.pathExists(file);
|
||||
expect(fileExists).toBe(true);
|
||||
|
||||
// wait to make sure
|
||||
await delay(500);
|
||||
|
||||
const dbUpdateSpy = vi.spyOn(dbSubjects.dbFileUpdate, 'next');
|
||||
await delay(100);
|
||||
// writes some data to the db file
|
||||
await fs.appendFile(file, 'random-data', { encoding: 'binary' });
|
||||
// write again
|
||||
await fs.appendFile(file, 'random-data', { encoding: 'binary' });
|
||||
|
||||
// wait for 200ms for file watcher to detect file change
|
||||
// wait for 2000ms for file watcher to detect file change
|
||||
await delay(2000);
|
||||
|
||||
expect(sendStub).toBeCalledWith('db:onDBFileUpdate', id);
|
||||
expect(dbUpdateSpy).toBeCalledWith(id);
|
||||
dbUpdateSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('workspace handlers', () => {
|
||||
test('list all workspace ids', async () => {
|
||||
const ids = ['test-workspace-id', 'test-workspace-id-2'];
|
||||
const ids = [v4(), v4()];
|
||||
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||
await Promise.all(ids.map(id => ensureSQLiteDB(id)));
|
||||
const list = await dispatch('workspace', 'list');
|
||||
expect(list.map(([id]) => id)).toEqual(ids);
|
||||
expect(list.map(([id]) => id).sort()).toEqual(ids.sort());
|
||||
});
|
||||
|
||||
test('delete workspace', async () => {
|
||||
const ids = ['test-workspace-id', 'test-workspace-id-2'];
|
||||
// @TODO dispatch is hanging on Windows
|
||||
if (process.platform === 'win32') {
|
||||
return;
|
||||
}
|
||||
const ids = [v4(), v4()];
|
||||
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||
await Promise.all(ids.map(id => ensureSQLiteDB(id)));
|
||||
await dispatch('workspace', 'delete', 'test-workspace-id-2');
|
||||
await dispatch('workspace', 'delete', ids[1]);
|
||||
const list = await dispatch('workspace', 'list');
|
||||
expect(list.map(([id]) => id)).toEqual(['test-workspace-id']);
|
||||
expect(list.map(([id]) => id)).toEqual([ids[0]]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -244,7 +263,7 @@ describe('UI handlers', () => {
|
||||
|
||||
describe('db handlers', () => {
|
||||
test('apply doc and get doc updates', async () => {
|
||||
const workspaceId = 'test-workspace-id';
|
||||
const workspaceId = v4();
|
||||
const bin = await dispatch('db', 'getDocAsUpdates', workspaceId);
|
||||
// ? is this a good test?
|
||||
expect(bin.every((byte: number) => byte === 0)).toBe(true);
|
||||
@@ -264,13 +283,13 @@ describe('db handlers', () => {
|
||||
});
|
||||
|
||||
test('get non existent blob', async () => {
|
||||
const workspaceId = 'test-workspace-id';
|
||||
const workspaceId = v4();
|
||||
const bin = await dispatch('db', 'getBlob', workspaceId, 'non-existent-id');
|
||||
expect(bin).toBeNull();
|
||||
});
|
||||
|
||||
test('list blobs (empty)', async () => {
|
||||
const workspaceId = 'test-workspace-id';
|
||||
const workspaceId = v4();
|
||||
const list = await dispatch('db', 'getPersistedBlobs', workspaceId);
|
||||
expect(list).toEqual([]);
|
||||
});
|
||||
@@ -318,7 +337,7 @@ describe('dialog handlers', () => {
|
||||
const mockShowItemInFolder = vi.fn();
|
||||
electronModule.shell.showItemInFolder = mockShowItemInFolder;
|
||||
|
||||
const id = 'test-workspace-id';
|
||||
const id = v4();
|
||||
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||
const db = await ensureSQLiteDB(id);
|
||||
|
||||
@@ -334,13 +353,15 @@ describe('dialog handlers', () => {
|
||||
electronModule.dialog.showSaveDialog = mockShowSaveDialog;
|
||||
electronModule.shell.showItemInFolder = mockShowItemInFolder;
|
||||
|
||||
const id = 'test-workspace-id';
|
||||
const id = v4();
|
||||
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||
await ensureSQLiteDB(id);
|
||||
|
||||
await dispatch('dialog', 'saveDBFileAs', id);
|
||||
expect(mockShowSaveDialog).toBeCalled();
|
||||
expect(mockShowItemInFolder).not.toBeCalled();
|
||||
electronModule.dialog = {};
|
||||
electronModule.shell = {};
|
||||
});
|
||||
|
||||
test('saveDBFileAs', async () => {
|
||||
@@ -352,7 +373,7 @@ describe('dialog handlers', () => {
|
||||
electronModule.dialog.showSaveDialog = mockShowSaveDialog;
|
||||
electronModule.shell.showItemInFolder = mockShowItemInFolder;
|
||||
|
||||
const id = 'test-workspace-id';
|
||||
const id = v4();
|
||||
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||
await ensureSQLiteDB(id);
|
||||
|
||||
@@ -403,11 +424,13 @@ describe('dialog handlers', () => {
|
||||
const res = await dispatch('dialog', 'loadDBFile');
|
||||
expect(mockShowOpenDialog).toBeCalled();
|
||||
expect(res.error).toBe('DB_FILE_INVALID');
|
||||
|
||||
electronModule.dialog = {};
|
||||
});
|
||||
|
||||
test('loadDBFile', async () => {
|
||||
// we use ensureSQLiteDB to create a valid db file
|
||||
const id = 'test-workspace-id';
|
||||
const id = v4();
|
||||
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||
const db = await ensureSQLiteDB(id);
|
||||
|
||||
@@ -417,6 +440,11 @@ describe('dialog handlers', () => {
|
||||
await fs.ensureDir(basePath);
|
||||
await fs.copyFile(db.path, originDBFilePath);
|
||||
|
||||
// on Windows, we skip this test because we can't delete the db file
|
||||
if (process.platform === 'win32') {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove db
|
||||
await fs.remove(db.path);
|
||||
|
||||
@@ -440,19 +468,19 @@ describe('dialog handlers', () => {
|
||||
});
|
||||
|
||||
test('moveDBFile', async () => {
|
||||
const newPath = path.join(SESSION_DATA_PATH, 'affine-test', 'xxx');
|
||||
const newPath = path.join(SESSION_DATA_PATH, 'xxx');
|
||||
const mockShowSaveDialog = vi.fn(() => {
|
||||
return { filePath: newPath };
|
||||
}) as any;
|
||||
electronModule.dialog.showSaveDialog = mockShowSaveDialog;
|
||||
|
||||
const id = 'test-workspace-id';
|
||||
const id = v4();
|
||||
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||
await ensureSQLiteDB(id);
|
||||
|
||||
const res = await dispatch('dialog', 'moveDBFile', id);
|
||||
expect(mockShowSaveDialog).toBeCalled();
|
||||
expect(res.filePath).toBe(newPath);
|
||||
electronModule.dialog = {};
|
||||
});
|
||||
|
||||
test('moveDBFile (skipped)', async () => {
|
||||
@@ -461,12 +489,13 @@ describe('dialog handlers', () => {
|
||||
}) as any;
|
||||
electronModule.dialog.showSaveDialog = mockShowSaveDialog;
|
||||
|
||||
const id = 'test-workspace-id';
|
||||
const id = v4();
|
||||
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||
await ensureSQLiteDB(id);
|
||||
|
||||
const res = await dispatch('dialog', 'moveDBFile', id);
|
||||
expect(mockShowSaveDialog).toBeCalled();
|
||||
expect(res.filePath).toBe(undefined);
|
||||
electronModule.dialog = {};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,94 +1,160 @@
|
||||
import { watch } from 'chokidar';
|
||||
import type { NotifyEvent } from '@affine/native/event';
|
||||
import { createFSWatcher } from '@affine/native/fs-watcher';
|
||||
import { app } from 'electron';
|
||||
import {
|
||||
connectable,
|
||||
defer,
|
||||
from,
|
||||
fromEvent,
|
||||
identity,
|
||||
lastValueFrom,
|
||||
Observable,
|
||||
ReplaySubject,
|
||||
Subject,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
debounceTime,
|
||||
exhaustMap,
|
||||
filter,
|
||||
groupBy,
|
||||
ignoreElements,
|
||||
mergeMap,
|
||||
shareReplay,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
takeUntil,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { appContext } from '../../context';
|
||||
import { subjects } from '../../events';
|
||||
import { logger } from '../../logger';
|
||||
import { debounce, ts } from '../../utils';
|
||||
import { ts } from '../../utils';
|
||||
import type { WorkspaceSQLiteDB } from './sqlite';
|
||||
import { openWorkspaceDatabase } from './sqlite';
|
||||
|
||||
const dbMapping = new Map<string, Promise<WorkspaceSQLiteDB>>();
|
||||
const dbWatchers = new Map<string, () => void>();
|
||||
const databaseInput$ = new Subject<string>();
|
||||
export const databaseConnector$ = new ReplaySubject<WorkspaceSQLiteDB>();
|
||||
|
||||
const groupedDatabaseInput$ = databaseInput$.pipe(groupBy(identity));
|
||||
|
||||
export const database$ = connectable(
|
||||
groupedDatabaseInput$.pipe(
|
||||
mergeMap(workspaceDatabase$ =>
|
||||
workspaceDatabase$.pipe(
|
||||
// only open the first db with the same workspaceId, and emit it to the downstream
|
||||
exhaustMap(workspaceId => {
|
||||
logger.info('[ensureSQLiteDB] open db connection', workspaceId);
|
||||
return from(openWorkspaceDatabase(appContext, workspaceId)).pipe(
|
||||
switchMap(db => {
|
||||
return startWatchingDBFile(db).pipe(
|
||||
// ignore all events and only emit the db to the downstream
|
||||
ignoreElements(),
|
||||
startWith(db)
|
||||
);
|
||||
})
|
||||
);
|
||||
}),
|
||||
shareReplay(1)
|
||||
)
|
||||
),
|
||||
tap({
|
||||
complete: () => {
|
||||
logger.info('[FSWatcher] close all watchers');
|
||||
createFSWatcher().close();
|
||||
},
|
||||
})
|
||||
),
|
||||
{
|
||||
connector: () => databaseConnector$,
|
||||
resetOnDisconnect: true,
|
||||
}
|
||||
);
|
||||
|
||||
export const databaseConnectableSubscription = database$.connect();
|
||||
|
||||
// 1. File delete
|
||||
// 2. File move
|
||||
// - on Linux, it's `type: { modify: { kind: 'rename', mode: 'from' } }`
|
||||
// - on Windows, it's `type: { remove: { kind: 'any' } }`
|
||||
// - on macOS, it's `type: { modify: { kind: 'rename', mode: 'any' } }`
|
||||
export function isRemoveOrMoveEvent(event: NotifyEvent) {
|
||||
return (
|
||||
typeof event.type === 'object' &&
|
||||
('remove' in event.type ||
|
||||
('modify' in event.type &&
|
||||
event.type.modify.kind === 'rename' &&
|
||||
(event.type.modify.mode === 'from' ||
|
||||
event.type.modify.mode === 'any')))
|
||||
);
|
||||
}
|
||||
|
||||
// if we removed the file, we will stop watching it
|
||||
function startWatchingDBFile(db: WorkspaceSQLiteDB) {
|
||||
if (dbWatchers.has(db.workspaceId)) {
|
||||
return dbWatchers.get(db.workspaceId);
|
||||
}
|
||||
logger.info('watch db file', db.path);
|
||||
const watcher = watch(db.path);
|
||||
|
||||
const debounceOnChange = debounce(() => {
|
||||
logger.info(
|
||||
'db file changed on disk',
|
||||
db.workspaceId,
|
||||
ts() - db.lastUpdateTime,
|
||||
'ms'
|
||||
const FSWatcher = createFSWatcher();
|
||||
return new Observable<NotifyEvent>(subscriber => {
|
||||
logger.info('[FSWatcher] start watching db file', db.workspaceId);
|
||||
const subscription = FSWatcher.watch(db.path, {
|
||||
recursive: false,
|
||||
}).subscribe(
|
||||
event => {
|
||||
logger.info('[FSWatcher]', event);
|
||||
subscriber.next(event);
|
||||
// remove file or move file, complete the observable and close db
|
||||
if (isRemoveOrMoveEvent(event)) {
|
||||
subscriber.complete();
|
||||
}
|
||||
},
|
||||
err => {
|
||||
subscriber.error(err);
|
||||
}
|
||||
);
|
||||
// reconnect db
|
||||
db.reconnectDB();
|
||||
subjects.db.dbFileUpdate.next(db.workspaceId);
|
||||
}, 1000);
|
||||
|
||||
watcher.on('change', () => {
|
||||
const currentTime = ts();
|
||||
if (currentTime - db.lastUpdateTime > 100) {
|
||||
debounceOnChange();
|
||||
}
|
||||
});
|
||||
|
||||
dbWatchers.set(db.workspaceId, () => {
|
||||
watcher.close();
|
||||
});
|
||||
|
||||
// todo: there is still a possibility that the file is deleted
|
||||
// but we didn't get the event soon enough and another event tries to
|
||||
// access the db
|
||||
watcher.on('unlink', () => {
|
||||
logger.info('db file missing', db.workspaceId);
|
||||
subjects.db.dbFileMissing.next(db.workspaceId);
|
||||
// cleanup
|
||||
watcher.close().then(() => {
|
||||
return () => {
|
||||
// destroy on unsubscribe
|
||||
logger.info('[FSWatcher] cleanup db file watcher', db.workspaceId);
|
||||
db.destroy();
|
||||
dbWatchers.delete(db.workspaceId);
|
||||
dbMapping.delete(db.workspaceId);
|
||||
});
|
||||
});
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}).pipe(
|
||||
debounceTime(1000),
|
||||
filter(event => !isRemoveOrMoveEvent(event)),
|
||||
tap({
|
||||
next: () => {
|
||||
logger.info(
|
||||
'[FSWatcher] db file changed on disk',
|
||||
db.workspaceId,
|
||||
ts() - db.lastUpdateTime,
|
||||
'ms'
|
||||
);
|
||||
db.reconnectDB();
|
||||
subjects.db.dbFileUpdate.next(db.workspaceId);
|
||||
},
|
||||
complete: () => {
|
||||
// todo: there is still a possibility that the file is deleted
|
||||
// but we didn't get the event soon enough and another event tries to
|
||||
// access the db
|
||||
logger.info('[FSWatcher] db file missing', db.workspaceId);
|
||||
subjects.db.dbFileMissing.next(db.workspaceId);
|
||||
db.destroy();
|
||||
},
|
||||
}),
|
||||
takeUntil(defer(() => fromEvent(app, 'before-quit')))
|
||||
);
|
||||
}
|
||||
|
||||
export async function ensureSQLiteDB(id: string) {
|
||||
let workspaceDB = dbMapping.get(id);
|
||||
if (!workspaceDB) {
|
||||
logger.info('[ensureSQLiteDB] open db connection', id);
|
||||
workspaceDB = openWorkspaceDatabase(appContext, id);
|
||||
dbMapping.set(id, workspaceDB);
|
||||
startWatchingDBFile(await workspaceDB);
|
||||
}
|
||||
return await workspaceDB;
|
||||
export function ensureSQLiteDB(id: string) {
|
||||
const deferValue = lastValueFrom(
|
||||
database$.pipe(
|
||||
filter(db => db.workspaceId === id && db.db.open),
|
||||
take(1),
|
||||
tap({
|
||||
error: err => {
|
||||
logger.error('[ensureSQLiteDB] error', err);
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
databaseInput$.next(id);
|
||||
return deferValue;
|
||||
}
|
||||
|
||||
export async function disconnectSQLiteDB(id: string) {
|
||||
const dbp = dbMapping.get(id);
|
||||
if (dbp) {
|
||||
const db = await dbp;
|
||||
logger.info('close db connection', id);
|
||||
db.destroy();
|
||||
dbWatchers.get(id)?.();
|
||||
dbWatchers.delete(id);
|
||||
dbMapping.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanupSQLiteDBs() {
|
||||
for (const [id] of dbMapping) {
|
||||
logger.info('close db connection', id);
|
||||
await disconnectSQLiteDB(id);
|
||||
}
|
||||
dbMapping.clear();
|
||||
dbWatchers.clear();
|
||||
}
|
||||
|
||||
app?.on('before-quit', async () => {
|
||||
await cleanupSQLiteDBs();
|
||||
});
|
||||
|
||||
@@ -42,6 +42,7 @@ export class WorkspaceSQLiteDB {
|
||||
ydoc = new Y.Doc();
|
||||
firstConnect = false;
|
||||
lastUpdateTime = ts();
|
||||
destroyed = false;
|
||||
|
||||
constructor(public path: string, public workspaceId: string) {
|
||||
this.db = this.reconnectDB();
|
||||
@@ -58,7 +59,7 @@ export class WorkspaceSQLiteDB {
|
||||
};
|
||||
|
||||
reconnectDB = () => {
|
||||
logger.log('open db', this.workspaceId);
|
||||
logger.log('[WorkspaceSQLiteDB] open db', this.workspaceId);
|
||||
if (this.db) {
|
||||
this.db.close();
|
||||
}
|
||||
@@ -224,8 +225,9 @@ export async function openWorkspaceDatabase(
|
||||
}
|
||||
|
||||
export function isValidDBFile(path: string) {
|
||||
let db: Database | null = null;
|
||||
try {
|
||||
const db = sqlite(path);
|
||||
db = sqlite(path);
|
||||
// check if db has two tables, one for updates and onefor blobs
|
||||
const statement = db.prepare(
|
||||
`SELECT name FROM sqlite_schema WHERE type='table'`
|
||||
@@ -239,6 +241,7 @@ export function isValidDBFile(path: string) {
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('isValidDBFile', error);
|
||||
db?.close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import { nanoid } from 'nanoid';
|
||||
|
||||
import { appContext } from '../../context';
|
||||
import { logger } from '../../logger';
|
||||
import { ensureSQLiteDB } from '../db/ensure-db';
|
||||
import { ensureSQLiteDB, isRemoveOrMoveEvent } from '../db/ensure-db';
|
||||
import type { WorkspaceSQLiteDB } from '../db/sqlite';
|
||||
import { getWorkspaceDBPath, isValidDBFile } from '../db/sqlite';
|
||||
import { listWorkspaces } from '../workspace/workspace';
|
||||
|
||||
@@ -232,17 +233,29 @@ export async function moveDBFile(
|
||||
workspaceId: string,
|
||||
dbFileLocation?: string
|
||||
): Promise<MoveDBFileResult> {
|
||||
let db: WorkspaceSQLiteDB | null = null;
|
||||
try {
|
||||
const db = await ensureSQLiteDB(workspaceId);
|
||||
|
||||
const { moveFile, FsWatcher } = await import('@affine/native');
|
||||
db = await ensureSQLiteDB(workspaceId);
|
||||
// get the real file path of db
|
||||
const realpath = await fs.realpath(db.path);
|
||||
const isLink = realpath !== db.path;
|
||||
|
||||
const watcher = FsWatcher.watch(realpath, { recursive: false });
|
||||
const waitForRemove = new Promise<void>(resolve => {
|
||||
const subscription = watcher.subscribe(event => {
|
||||
if (isRemoveOrMoveEvent(event)) {
|
||||
subscription.unsubscribe();
|
||||
// resolve after FSWatcher in `database$` is fired
|
||||
setImmediate(() => {
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
const newFilePath =
|
||||
dbFileLocation ||
|
||||
dbFileLocation ??
|
||||
(
|
||||
getFakedResult() ||
|
||||
getFakedResult() ??
|
||||
(await dialog.showSaveDialog({
|
||||
properties: ['showOverwriteConfirmation'],
|
||||
title: 'Move Workspace Storage',
|
||||
@@ -263,32 +276,39 @@ export async function moveDBFile(
|
||||
};
|
||||
}
|
||||
|
||||
db.db.close();
|
||||
|
||||
if (await fs.pathExists(newFilePath)) {
|
||||
return {
|
||||
error: 'FILE_ALREADY_EXISTS',
|
||||
};
|
||||
}
|
||||
|
||||
db.db.close();
|
||||
|
||||
if (isLink) {
|
||||
// remove the old link to unblock new link
|
||||
await fs.unlink(db.path);
|
||||
}
|
||||
|
||||
await fs.move(realpath, newFilePath, {
|
||||
overwrite: true,
|
||||
});
|
||||
logger.info(`[moveDBFile] move ${realpath} -> ${newFilePath}`);
|
||||
|
||||
await moveFile(realpath, newFilePath);
|
||||
|
||||
await fs.ensureSymlink(newFilePath, db.path, 'file');
|
||||
logger.info(`openMoveDBFileDialog symlink: ${realpath} -> ${newFilePath}`);
|
||||
db.reconnectDB();
|
||||
logger.info(`[moveDBFile] symlink: ${realpath} -> ${newFilePath}`);
|
||||
// wait for the file move event emits to the FileWatcher in database$ in ensure-db.ts
|
||||
// so that the db will be destroyed and we can call the `ensureSQLiteDB` in the next step
|
||||
// or the FileWatcher will continue listen on the `realpath` and emit file change events
|
||||
// then the database will reload while receiving these events; and the moved database file will be recreated while reloading database
|
||||
await waitForRemove;
|
||||
logger.info(`removed`);
|
||||
await ensureSQLiteDB(workspaceId);
|
||||
|
||||
return {
|
||||
filePath: newFilePath,
|
||||
};
|
||||
} catch (err) {
|
||||
logger.error('moveDBFile', err);
|
||||
db?.destroy();
|
||||
logger.error('[moveDBFile]', err);
|
||||
return {
|
||||
error: 'UNKNOWN_ERROR',
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ export const dialogHandlers = {
|
||||
saveDBFileAs: async (_, workspaceId: string) => {
|
||||
return saveDBFileAs(workspaceId);
|
||||
},
|
||||
moveDBFile: async (_, workspaceId: string, dbFileLocation?: string) => {
|
||||
moveDBFile: (_, workspaceId: string, dbFileLocation?: string) => {
|
||||
return moveDBFile(workspaceId, dbFileLocation);
|
||||
},
|
||||
selectDBFileLocation: async () => {
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { app } from 'electron';
|
||||
|
||||
import type { NamespaceHandlers } from '../type';
|
||||
import { checkForUpdatesAndNotify, quitAndInstall } from './updater';
|
||||
|
||||
export const updaterHandlers = {
|
||||
updateClient: async () => {
|
||||
const { updateClient } = await import('./updater');
|
||||
return updateClient();
|
||||
currentVersion: async () => {
|
||||
return app.getVersion();
|
||||
},
|
||||
quitAndInstall: async () => {
|
||||
return quitAndInstall();
|
||||
},
|
||||
checkForUpdatesAndNotify: async () => {
|
||||
return checkForUpdatesAndNotify(true);
|
||||
},
|
||||
} satisfies NamespaceHandlers;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { app } from 'electron';
|
||||
import type { AppUpdater } from 'electron-updater';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -21,10 +22,22 @@ const isDev = mode === 'development';
|
||||
|
||||
let _autoUpdater: AppUpdater | null = null;
|
||||
|
||||
export const updateClient = async () => {
|
||||
export const quitAndInstall = async () => {
|
||||
_autoUpdater?.quitAndInstall();
|
||||
};
|
||||
|
||||
let lastCheckTime = 0;
|
||||
export const checkForUpdatesAndNotify = async (force = true) => {
|
||||
if (!_autoUpdater) {
|
||||
return; // ?
|
||||
}
|
||||
// check every 30 minutes (1800 seconds) at most
|
||||
if (force || lastCheckTime + 1000 * 1800 < Date.now()) {
|
||||
lastCheckTime = Date.now();
|
||||
return _autoUpdater.checkForUpdatesAndNotify();
|
||||
}
|
||||
};
|
||||
|
||||
export const registerUpdater = async () => {
|
||||
// require it will cause some side effects and will break generate-main-exposed-meta,
|
||||
// so we wrap it in a function
|
||||
@@ -37,6 +50,9 @@ export const registerUpdater = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: support auto update on windows and linux
|
||||
const allowAutoUpdate = isMacOS();
|
||||
|
||||
_autoUpdater.autoDownload = false;
|
||||
_autoUpdater.allowPrerelease = buildType !== 'stable';
|
||||
_autoUpdater.autoInstallOnAppQuit = false;
|
||||
@@ -49,24 +65,36 @@ export const registerUpdater = async () => {
|
||||
releaseType: buildType === 'stable' ? 'release' : 'prerelease',
|
||||
});
|
||||
|
||||
if (isMacOS()) {
|
||||
_autoUpdater.on('update-available', () => {
|
||||
// register events for checkForUpdatesAndNotify
|
||||
_autoUpdater.on('update-available', info => {
|
||||
if (allowAutoUpdate) {
|
||||
_autoUpdater!.downloadUpdate();
|
||||
logger.info('Update available, downloading...');
|
||||
logger.info('Update available, downloading...', info);
|
||||
}
|
||||
updaterSubjects.updateAvailable.next({
|
||||
version: info.version,
|
||||
allowAutoUpdate,
|
||||
});
|
||||
_autoUpdater.on('download-progress', e => {
|
||||
logger.info(`Download progress: ${e.percent}`);
|
||||
});
|
||||
_autoUpdater.on('download-progress', e => {
|
||||
logger.info(`Download progress: ${e.percent}`);
|
||||
updaterSubjects.downloadProgress.next(e.percent);
|
||||
});
|
||||
_autoUpdater.on('update-downloaded', e => {
|
||||
updaterSubjects.updateReady.next({
|
||||
version: e.version,
|
||||
allowAutoUpdate,
|
||||
});
|
||||
_autoUpdater.on('update-downloaded', e => {
|
||||
updaterSubjects.clientUpdateReady.next({
|
||||
version: e.version,
|
||||
});
|
||||
logger.info('Update downloaded, ready to install');
|
||||
});
|
||||
_autoUpdater.on('error', e => {
|
||||
logger.error('Error while updating client', e);
|
||||
});
|
||||
_autoUpdater.forceDevUpdateConfig = isDev;
|
||||
await _autoUpdater.checkForUpdatesAndNotify();
|
||||
}
|
||||
// I guess we can skip it?
|
||||
// updaterSubjects.clientDownloadProgress.next(100);
|
||||
logger.info('Update downloaded, ready to install');
|
||||
});
|
||||
_autoUpdater.on('error', e => {
|
||||
logger.error('Error while updating client', e);
|
||||
});
|
||||
_autoUpdater.forceDevUpdateConfig = isDev;
|
||||
|
||||
app.on('activate', async () => {
|
||||
await checkForUpdatesAndNotify(false);
|
||||
});
|
||||
};
|
||||
|
||||
6
apps/electron/layers/preload/preload.d.ts
vendored
6
apps/electron/layers/preload/preload.d.ts
vendored
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/consistent-type-imports */
|
||||
|
||||
interface Window {
|
||||
apis?: typeof import('./src/affine-apis').apis;
|
||||
events?: typeof import('./src/affine-apis').events;
|
||||
appInfo?: typeof import('./src/affine-apis').appInfo;
|
||||
apis: typeof import('./src/affine-apis').apis;
|
||||
events: typeof import('./src/affine-apis').events;
|
||||
appInfo: typeof import('./src/affine-apis').appInfo;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/electron",
|
||||
"private": true,
|
||||
"version": "0.6.0-canary.2",
|
||||
"version": "0.6.0-canary.3",
|
||||
"author": "affine",
|
||||
"repository": {
|
||||
"url": "https://github.com/toeverything/AFFiNE",
|
||||
@@ -18,10 +18,6 @@
|
||||
"generate-main-exposed-meta": "zx scripts/generate-main-exposed-meta.mjs",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make",
|
||||
"make-macos-arm64": "electron-forge make --platform=darwin --arch=arm64",
|
||||
"make-macos-x64": "electron-forge make --platform=darwin --arch=x64",
|
||||
"make-windows-x64": "electron-forge make --platform=win32 --arch=x64",
|
||||
"make-linux-x64": "electron-forge make --platform=linux --arch=x64",
|
||||
"rebuild:for-unit-test": "yarn rebuild better-sqlite3",
|
||||
"rebuild:for-electron": "yarn electron-rebuild",
|
||||
"test": "playwright test"
|
||||
@@ -32,6 +28,7 @@
|
||||
"main": "./dist/layers/main/index.js",
|
||||
"devDependencies": {
|
||||
"@affine-test/kit": "workspace:*",
|
||||
"@affine/native": "workspace:*",
|
||||
"@electron-forge/cli": "^6.1.1",
|
||||
"@electron-forge/core": "^6.1.1",
|
||||
"@electron-forge/core-utils": "^6.1.1",
|
||||
@@ -44,6 +41,7 @@
|
||||
"@electron/remote": "2.0.9",
|
||||
"@types/better-sqlite3": "^7.6.4",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"cross-env": "7.0.3",
|
||||
"electron": "24.3.0",
|
||||
"electron-log": "^5.0.0-beta.23",
|
||||
@@ -54,6 +52,7 @@
|
||||
"playwright": "^1.33.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"undici": "^5.22.1",
|
||||
"uuid": "^9.0.0",
|
||||
"zx": "^7.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -8,6 +8,11 @@ import { config } from './common.mjs';
|
||||
const NODE_ENV =
|
||||
process.env.NODE_ENV === 'development' ? 'development' : 'production';
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
$.shell = true;
|
||||
$.prefix = '';
|
||||
}
|
||||
|
||||
async function buildLayers() {
|
||||
const common = config();
|
||||
await esbuild.build(common.preload);
|
||||
|
||||
@@ -12,16 +12,6 @@ const DEV_SERVER_URL = process.env.DEV_SERVER_URL;
|
||||
/** @type 'production' | 'development'' */
|
||||
const mode = (process.env.NODE_ENV = process.env.NODE_ENV || 'development');
|
||||
|
||||
const nativeNodeModulesPlugin = {
|
||||
name: 'native-node-modules',
|
||||
setup(build) {
|
||||
// Mark native Node.js modules as external
|
||||
build.onResolve({ filter: /\.node$/, namespace: 'file' }, args => {
|
||||
return { path: args.path, external: true };
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// List of env that will be replaced by esbuild
|
||||
const ENV_MACROS = ['AFFINE_GOOGLE_CLIENT_ID', 'AFFINE_GOOGLE_CLIENT_SECRET'];
|
||||
|
||||
@@ -50,9 +40,13 @@ export const config = () => {
|
||||
target: `node${NODE_MAJOR_VERSION}`,
|
||||
platform: 'node',
|
||||
external: ['electron', 'yjs', 'better-sqlite3', 'electron-updater'],
|
||||
plugins: [nativeNodeModulesPlugin],
|
||||
define: define,
|
||||
format: 'cjs',
|
||||
loader: {
|
||||
'.node': 'copy',
|
||||
},
|
||||
assetNames: '[name]',
|
||||
treeShaking: true,
|
||||
},
|
||||
preload: {
|
||||
entryPoints: [resolve(root, './layers/preload/src/index.ts')],
|
||||
@@ -61,7 +55,6 @@ export const config = () => {
|
||||
target: `node${NODE_MAJOR_VERSION}`,
|
||||
platform: 'node',
|
||||
external: ['electron', '../main/exposed-meta'],
|
||||
plugins: [nativeNodeModulesPlugin],
|
||||
define: define,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -32,9 +32,6 @@ if (process.platform === 'win32') {
|
||||
|
||||
cd(repoRootDir);
|
||||
|
||||
// step 1: build electron resources
|
||||
await $`yarn workspace @affine/electron build-layers`;
|
||||
|
||||
// step 2: build web (nextjs) dist
|
||||
if (!process.env.SKIP_WEB_BUILD) {
|
||||
process.env.ENABLE_LEGACY_PROVIDER = 'false';
|
||||
@@ -59,6 +56,17 @@ if (!process.env.SKIP_WEB_BUILD) {
|
||||
await fs.move(affineWebOutDir, publicAffineOutDir, { overwrite: true });
|
||||
}
|
||||
|
||||
// step 3: update app-updater.yml content with build type in resources folder
|
||||
if (process.env.BUILD_TYPE === 'internal') {
|
||||
const appUpdaterYml = path.join(publicDistDir, 'app-update.yml');
|
||||
const appUpdaterYmlContent = await fs.readFile(appUpdaterYml, 'utf-8');
|
||||
const newAppUpdaterYmlContent = appUpdaterYmlContent.replace(
|
||||
'AFFiNE',
|
||||
'AFFiNE-Releases'
|
||||
);
|
||||
await fs.writeFile(appUpdaterYml, newAppUpdaterYmlContent);
|
||||
}
|
||||
|
||||
/// --------
|
||||
/// --------
|
||||
/// --------
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
#!/usr/bin/env zx
|
||||
/* eslint-disable @typescript-eslint/no-restricted-imports */
|
||||
import 'zx/globals';
|
||||
|
||||
const mainDistDir = path.resolve(__dirname, '../dist/layers/main');
|
||||
|
||||
// be careful and avoid any side effects in
|
||||
|
||||
@@ -9,14 +9,16 @@
|
||||
"types": ["node"],
|
||||
"outDir": "dist",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", "package.json"],
|
||||
"exclude": ["out", "dist", "node_modules"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/native"
|
||||
}
|
||||
],
|
||||
"ts-node": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/server",
|
||||
"private": true,
|
||||
"version": "0.6.0-canary.2",
|
||||
"version": "0.6.0-canary.3",
|
||||
"description": "Affine Node.js server",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/web",
|
||||
"private": true,
|
||||
"version": "0.6.0-canary.2",
|
||||
"version": "0.6.0-canary.3",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
|
||||
@@ -74,7 +74,7 @@ const NameWorkspaceContent = ({
|
||||
data-testid="create-workspace-input"
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={t['Set a Workspace name']()}
|
||||
maxLength={15} // TODO: the max workspace name length?
|
||||
maxLength={64}
|
||||
minLength={0}
|
||||
onChange={value => {
|
||||
setWorkspaceName(value);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Empty, IconButton, Modal, ModalWrapper } from '@affine/component';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon } from '@blocksuite/icons';
|
||||
import type React from 'react';
|
||||
|
||||
@@ -20,6 +22,7 @@ interface TmpDisableAffineCloudModalProps {
|
||||
export const TmpDisableAffineCloudModal: React.FC<
|
||||
TmpDisableAffineCloudModalProps
|
||||
> = ({ open, onClose }) => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<Modal
|
||||
data-testid="disable-affine-cloud-modal"
|
||||
@@ -37,21 +40,25 @@ export const TmpDisableAffineCloudModal: React.FC<
|
||||
</IconButton>
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentTitle>AFFiNE Cloud is upgrading now.</ContentTitle>
|
||||
<ContentTitle>
|
||||
{t['com.affine.cloudTempDisable.title']()}
|
||||
</ContentTitle>
|
||||
<StyleTips>
|
||||
We are upgrading the AFFiNE Cloud service and it is temporarily
|
||||
unavailable on the client side. If you wish to stay updated on the
|
||||
progress and be notified on availability, you can join the
|
||||
<a
|
||||
href="https://community.affine.pro"
|
||||
target="_blank"
|
||||
style={{
|
||||
color: 'var(--affine-link-color)',
|
||||
}}
|
||||
>
|
||||
AFFiNE Community
|
||||
</a>
|
||||
.
|
||||
<Trans i18nKey="com.affine.cloudTempDisable.description">
|
||||
We are upgrading the AFFiNE Cloud service and it is temporarily
|
||||
unavailable on the client side. If you wish to stay updated on the
|
||||
progress and be notified on availability, you can fill out the
|
||||
<a
|
||||
href="https://6dxre9ihosp.typeform.com/to/B8IHwuyy"
|
||||
target="_blank"
|
||||
style={{
|
||||
color: 'var(--affine-link-color)',
|
||||
}}
|
||||
>
|
||||
AFFiNE Cloud Signup
|
||||
</a>
|
||||
.
|
||||
</Trans>
|
||||
</StyleTips>
|
||||
<StyleImage>
|
||||
<Empty
|
||||
@@ -69,7 +76,7 @@ export const TmpDisableAffineCloudModal: React.FC<
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Got it
|
||||
{t['Got it']()}
|
||||
</StyleButton>
|
||||
</StyleButtonContainer>
|
||||
</Content>
|
||||
|
||||
@@ -137,7 +137,7 @@ export const GeneralPanel: React.FC<PanelProps> = ({
|
||||
value={input}
|
||||
data-testid="workspace-name-input"
|
||||
placeholder={t['Workspace Name']()}
|
||||
maxLength={50}
|
||||
maxLength={64}
|
||||
minLength={0}
|
||||
onChange={newName => {
|
||||
setInput(newName);
|
||||
|
||||
@@ -19,7 +19,7 @@ import { pageListEmptyStyle } from './index.css';
|
||||
|
||||
export type BlockSuitePageListProps = {
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
listType: 'all' | 'trash' | 'favorite' | 'shared' | 'public';
|
||||
listType: 'all' | 'trash' | 'shared' | 'public';
|
||||
isPublic?: true;
|
||||
onOpenPage: (pageId: string, newTab?: boolean) => void;
|
||||
};
|
||||
@@ -31,7 +31,6 @@ const filter = {
|
||||
const parentMeta = allMetas.find(m => m.subpageIds?.includes(pageMeta.id));
|
||||
return !parentMeta?.trash && pageMeta.trash;
|
||||
},
|
||||
favorite: (pageMeta: PageMeta) => pageMeta.favorite && !pageMeta.trash,
|
||||
shared: (pageMeta: PageMeta) => pageMeta.isPublic && !pageMeta.trash,
|
||||
};
|
||||
|
||||
@@ -52,9 +51,6 @@ const PageListEmpty = (props: {
|
||||
if (listType === 'all') {
|
||||
return t['emptyAllPages']();
|
||||
}
|
||||
if (listType === 'favorite') {
|
||||
return t['emptyFavorite']();
|
||||
}
|
||||
if (listType === 'trash') {
|
||||
return t['emptyTrash']();
|
||||
}
|
||||
@@ -102,7 +98,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
||||
pageId: pageMeta.id,
|
||||
title: pageMeta.title,
|
||||
createDate: formatDate(pageMeta.createDate),
|
||||
updatedDate: formatDate(pageMeta.updatedDate),
|
||||
updatedDate: formatDate(pageMeta.updatedDate ?? pageMeta.createDate),
|
||||
onClickPage: () => onOpenPage(pageMeta.id),
|
||||
onClickRestore: () => {
|
||||
restoreFromTrash(pageMeta.id);
|
||||
@@ -129,7 +125,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
||||
favorite: !!pageMeta.favorite,
|
||||
isPublicPage: !!pageMeta.isPublic,
|
||||
createDate: formatDate(pageMeta.createDate),
|
||||
updatedDate: formatDate(pageMeta.updatedDate),
|
||||
updatedDate: formatDate(pageMeta.updatedDate ?? pageMeta.createDate),
|
||||
onClickPage: () => onOpenPage(pageMeta.id),
|
||||
onOpenPageInNewTab: () => onOpenPage(pageMeta.id, true),
|
||||
onClickRestore: () => {
|
||||
|
||||
@@ -40,7 +40,6 @@ const CommonMenu = () => {
|
||||
return (
|
||||
<FlexWrapper alignItems="center" justifyContent="center">
|
||||
<Menu
|
||||
width={276}
|
||||
content={content}
|
||||
placement="bottom"
|
||||
disablePortal={true}
|
||||
@@ -137,7 +136,6 @@ const PageMenu = () => {
|
||||
<>
|
||||
<FlexWrapper alignItems="center" justifyContent="center">
|
||||
<Menu
|
||||
width={276}
|
||||
content={EditMenu}
|
||||
placement="bottom-end"
|
||||
disablePortal={true}
|
||||
|
||||
@@ -30,10 +30,12 @@ export const StyledThemeButton = styled('button')<{
|
||||
active: boolean;
|
||||
}>(({ active }) => {
|
||||
return {
|
||||
padding: '0 8px',
|
||||
height: '100%',
|
||||
flex: 1,
|
||||
cursor: 'pointer',
|
||||
color: active ? 'var(--affine-primary-color)' : 'var(--affine-icon-color)',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
export const StyledVerticalDivider = styled('div')(() => {
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import type { PopperProps } from '@affine/component';
|
||||
import { QuickSearchTips } from '@affine/component';
|
||||
import { getEnvironment } from '@affine/env';
|
||||
import { ArrowDownSmallIcon } from '@blocksuite/icons';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type {
|
||||
FC,
|
||||
HTMLAttributes,
|
||||
PropsWithChildren,
|
||||
ReactElement,
|
||||
} from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { openQuickSearchModalAtom } from '../../../atoms';
|
||||
import { guideQuickSearchTipsAtom } from '../../../atoms/guide';
|
||||
import { QuickSearchButton } from '../../pure/quick-search-button';
|
||||
import { EditorModeSwitch } from './editor-mode-switch';
|
||||
import type { BaseHeaderProps } from './header';
|
||||
@@ -23,11 +18,6 @@ import * as styles from './styles.css';
|
||||
|
||||
export type WorkspaceHeaderProps = BaseHeaderProps;
|
||||
|
||||
const isMac = () => {
|
||||
const env = getEnvironment();
|
||||
return env.isBrowser && env.isMacOs;
|
||||
};
|
||||
|
||||
export const WorkspaceHeader: FC<
|
||||
PropsWithChildren<WorkspaceHeaderProps> & HTMLAttributes<HTMLDivElement>
|
||||
> = (props): ReactElement => {
|
||||
@@ -41,56 +31,6 @@ export const WorkspaceHeader: FC<
|
||||
assertExists(pageMeta);
|
||||
const title = pageMeta.title;
|
||||
|
||||
const popperRef: PopperProps['popperRef'] = useRef(null);
|
||||
|
||||
const [showQuickSearchTips, setShowQuickSearchTips] = useAtom(
|
||||
guideQuickSearchTipsAtom
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!headerRef.current) {
|
||||
return;
|
||||
}
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (showQuickSearchTips || !popperRef.current) {
|
||||
return;
|
||||
}
|
||||
popperRef.current.update();
|
||||
});
|
||||
resizeObserver.observe(headerRef.current);
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [showQuickSearchTips]);
|
||||
|
||||
const TipsContent = (
|
||||
<div className={styles.quickSearchTipContent}>
|
||||
<div>
|
||||
Click button
|
||||
{
|
||||
<span
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
>
|
||||
<ArrowDownSmallIcon />
|
||||
</span>
|
||||
}
|
||||
or use
|
||||
{isMac() ? ' ⌘ + K' : ' Ctrl + K'} to activate Quick Search. Then you
|
||||
can search keywords or quickly open recently viewed pages.
|
||||
</div>
|
||||
<div
|
||||
className={styles.quickSearchTipButton}
|
||||
data-testid="quick-search-got-it"
|
||||
onClick={() => setShowQuickSearchTips(false)}
|
||||
>
|
||||
Got it
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Header ref={headerRef} {...props}>
|
||||
{children}
|
||||
@@ -107,22 +47,14 @@ export const WorkspaceHeader: FC<
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.title}>{title || 'Untitled'}</div>
|
||||
<QuickSearchTips
|
||||
data-testid="quick-search-tips"
|
||||
content={TipsContent}
|
||||
placement="bottom"
|
||||
popperRef={popperRef}
|
||||
open={showQuickSearchTips}
|
||||
offset={[0, -5]}
|
||||
>
|
||||
<div className={styles.searchArrowWrapper}>
|
||||
<QuickSearchButton
|
||||
onClick={() => {
|
||||
setOpenQuickSearch(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</QuickSearchTips>
|
||||
|
||||
<div className={styles.searchArrowWrapper}>
|
||||
<QuickSearchButton
|
||||
onClick={() => {
|
||||
setOpenQuickSearch(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { styled } from '@affine/component';
|
||||
import { AffineLoading } from '@affine/component/affine-loading';
|
||||
import { memo, Suspense } from 'react';
|
||||
|
||||
@@ -17,34 +16,9 @@ export const Loading = memo(function Loading() {
|
||||
);
|
||||
});
|
||||
|
||||
// Used for the full page loading
|
||||
const StyledLoadingContainer = styled('div')(() => {
|
||||
return {
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
color: '#6880FF',
|
||||
flexDirection: 'column',
|
||||
h1: {
|
||||
fontSize: '2em',
|
||||
marginTop: '15px',
|
||||
fontWeight: '600',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated use skeleton instead
|
||||
*/
|
||||
export const PageLoading = () => {
|
||||
// We disable the loading on desktop, because want it looks faster.
|
||||
// This is a design requirement.
|
||||
if (environment.isDesktop) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<StyledLoadingContainer>
|
||||
<Loading />
|
||||
</StyledLoadingContainer>
|
||||
);
|
||||
return null;
|
||||
};
|
||||
|
||||
export default PageLoading;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
DeleteTemporarilyIcon,
|
||||
FavoriteIcon,
|
||||
FolderIcon,
|
||||
SettingsIcon,
|
||||
} from '@blocksuite/icons';
|
||||
@@ -24,11 +23,6 @@ export const useSwitchToConfig = (
|
||||
href: pathGenerator.all(workspaceId),
|
||||
icon: FolderIcon,
|
||||
},
|
||||
{
|
||||
title: t['Favorites'](),
|
||||
href: pathGenerator.favorite(workspaceId),
|
||||
icon: FavoriteIcon,
|
||||
},
|
||||
{
|
||||
title: t['Workspace Settings'](),
|
||||
href: pathGenerator.setting(workspaceId),
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
QuickSearchInput,
|
||||
SidebarContainer,
|
||||
SidebarScrollableContainer,
|
||||
updateAvailableAtom,
|
||||
} from '@affine/component/app-sidebar';
|
||||
import { config } from '@affine/env';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
@@ -20,7 +19,7 @@ import {
|
||||
ShareIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { useAtom } from 'jotai';
|
||||
import type { ReactElement } from 'react';
|
||||
import type React from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
@@ -40,7 +39,6 @@ export type RootAppSidebarProps = {
|
||||
currentPath: string;
|
||||
paths: {
|
||||
all: (workspaceId: string) => string;
|
||||
favorite: (workspaceId: string) => string;
|
||||
trash: (workspaceId: string) => string;
|
||||
setting: (workspaceId: string) => string;
|
||||
shared: (workspaceId: string) => string;
|
||||
@@ -115,7 +113,6 @@ export const RootAppSidebar = ({
|
||||
document.removeEventListener('keydown', keydown, { capture: true });
|
||||
}, [sidebarOpen, setSidebarOpen]);
|
||||
|
||||
const clientUpdateAvailable = useAtomValue(updateAvailableAtom);
|
||||
const [history, setHistory] = useHistoryAtom();
|
||||
const router = useMemo(() => {
|
||||
return {
|
||||
@@ -193,7 +190,8 @@ export const RootAppSidebar = ({
|
||||
</RouteMenuLinkItem>
|
||||
</SidebarScrollableContainer>
|
||||
<SidebarContainer>
|
||||
{clientUpdateAvailable && <AppUpdaterButton />}
|
||||
{environment.isDesktop && <AppUpdaterButton />}
|
||||
<div />
|
||||
<AddPageButton onClick={onClickNewPage} />
|
||||
</SidebarContainer>
|
||||
</AppSidebar>
|
||||
|
||||
@@ -15,7 +15,6 @@ beforeAll(() => {
|
||||
createDynamicRouteParser([
|
||||
'/workspace/[workspaceId]/[pageId]',
|
||||
'/workspace/[workspaceId]/all',
|
||||
'/workspace/[workspaceId]/favorite',
|
||||
'/workspace/[workspaceId]/trash',
|
||||
'/workspace/[workspaceId]/setting',
|
||||
'/workspace/[workspaceId]/shared',
|
||||
@@ -54,19 +53,6 @@ describe('useRouterHelper', () => {
|
||||
// routerHook.result.current.back()
|
||||
// routerHook.rerender()
|
||||
// expect(routerHook.result.current.pathname).toBe('/')
|
||||
|
||||
await hook.jumpToSubPath(
|
||||
'workspace1',
|
||||
WorkspaceSubPath.FAVORITE,
|
||||
RouteLogic.REPLACE
|
||||
);
|
||||
routerHook.rerender();
|
||||
expect(routerHook.result.current.pathname).toBe(
|
||||
'/workspace/[workspaceId]/favorite'
|
||||
);
|
||||
expect(routerHook.result.current.asPath).toBe(
|
||||
'/workspace/workspace1/favorite'
|
||||
);
|
||||
});
|
||||
|
||||
test('should jump to the expected page', async () => {
|
||||
|
||||
@@ -129,6 +129,15 @@ export const AllWorkspaceContext = ({
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var HALTING_PROBLEM_TIMEOUT: number;
|
||||
}
|
||||
|
||||
if (globalThis.HALTING_PROBLEM_TIMEOUT === undefined) {
|
||||
globalThis.HALTING_PROBLEM_TIMEOUT = 1000;
|
||||
}
|
||||
|
||||
export const CurrentWorkspaceContext = ({
|
||||
children,
|
||||
}: PropsWithChildren): ReactElement => {
|
||||
@@ -137,12 +146,15 @@ export const CurrentWorkspaceContext = ({
|
||||
const exist = metadata.find(m => m.id === workspaceId);
|
||||
const router = useRouter();
|
||||
const push = router.push;
|
||||
// fixme(himself65): this is not a good way to handle this,
|
||||
// need a better way to check whether this workspace really exist.
|
||||
useEffect(() => {
|
||||
const id = setTimeout(() => {
|
||||
if (!exist) {
|
||||
void push('/');
|
||||
globalThis.HALTING_PROBLEM_TIMEOUT <<= 1;
|
||||
}
|
||||
}, 1000);
|
||||
}, globalThis.HALTING_PROBLEM_TIMEOUT);
|
||||
return () => {
|
||||
clearTimeout(id);
|
||||
};
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { FavoriteIcon } from '@blocksuite/icons';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { BlockSuitePageList } from '../../../components/blocksuite/block-suite-page-list';
|
||||
import { PageLoading } from '../../../components/pure/loading';
|
||||
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { useRouterHelper } from '../../../hooks/use-router-helper';
|
||||
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
|
||||
import type { NextPageWithLayout } from '../../../shared';
|
||||
|
||||
const FavouritePage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const { jumpToPage } = useRouterHelper(router);
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const t = useAFFiNEI18N();
|
||||
const onClickPage = useCallback(
|
||||
(pageId: string, newTab?: boolean) => {
|
||||
assertExists(currentWorkspace);
|
||||
if (newTab) {
|
||||
window.open(`/workspace/${currentWorkspace?.id}/${pageId}`, '_blank');
|
||||
} else {
|
||||
jumpToPage(currentWorkspace.id, pageId);
|
||||
}
|
||||
},
|
||||
[currentWorkspace, jumpToPage]
|
||||
);
|
||||
if (currentWorkspace === null) {
|
||||
return <PageLoading />;
|
||||
}
|
||||
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
|
||||
assertExists(blockSuiteWorkspace);
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t['Favorites']()} - AFFiNE</title>
|
||||
</Head>
|
||||
<WorkspaceTitle
|
||||
workspace={currentWorkspace}
|
||||
currentPage={null}
|
||||
isPreview={false}
|
||||
isPublic={false}
|
||||
icon={<FavoriteIcon />}
|
||||
>
|
||||
{t['Favorites']()}
|
||||
</WorkspaceTitle>
|
||||
<BlockSuitePageList
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
onOpenPage={onClickPage}
|
||||
listType="favorite"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FavouritePage;
|
||||
|
||||
FavouritePage.getLayout = page => {
|
||||
return <WorkspaceLayout>{page}</WorkspaceLayout>;
|
||||
};
|
||||
@@ -26,7 +26,6 @@ export type NextPageWithLayout<P = Record<string, unknown>, IP = P> = NextPage<
|
||||
|
||||
export const enum WorkspaceSubPath {
|
||||
ALL = 'all',
|
||||
FAVORITE = 'favorite',
|
||||
SETTING = 'setting',
|
||||
TRASH = 'trash',
|
||||
SHARED = 'shared',
|
||||
@@ -34,7 +33,6 @@ export const enum WorkspaceSubPath {
|
||||
|
||||
export const WorkspaceSubPathName = {
|
||||
[WorkspaceSubPath.ALL]: 'All Pages',
|
||||
[WorkspaceSubPath.FAVORITE]: 'Favorites',
|
||||
[WorkspaceSubPath.SETTING]: 'Settings',
|
||||
[WorkspaceSubPath.TRASH]: 'Trash',
|
||||
[WorkspaceSubPath.SHARED]: 'Shared',
|
||||
@@ -44,7 +42,6 @@ export const WorkspaceSubPathName = {
|
||||
|
||||
export const pathGenerator = {
|
||||
all: workspaceId => `/workspace/${workspaceId}/all`,
|
||||
favorite: workspaceId => `/workspace/${workspaceId}/favorite`,
|
||||
trash: workspaceId => `/workspace/${workspaceId}/trash`,
|
||||
setting: workspaceId => `/workspace/${workspaceId}/setting`,
|
||||
shared: workspaceId => `/workspace/${workspaceId}/shared`,
|
||||
@@ -54,7 +51,6 @@ export const pathGenerator = {
|
||||
|
||||
export const publicPathGenerator = {
|
||||
all: workspaceId => `/public-workspace/${workspaceId}/all`,
|
||||
favorite: workspaceId => `/public-workspace/${workspaceId}/favorite`,
|
||||
trash: workspaceId => `/public-workspace/${workspaceId}/trash`,
|
||||
setting: workspaceId => `/public-workspace/${workspaceId}/setting`,
|
||||
shared: workspaceId => `/public-workspace/${workspaceId}/shared`,
|
||||
|
||||
38
docs/jobs.md
38
docs/jobs.md
@@ -95,7 +95,7 @@
|
||||
|
||||
## Contact us
|
||||
|
||||
Interested? Send us your CV to [contact@toeverything.info].
|
||||
Interested? You can full this [form](https://6dxre9ihosp.typeform.com/to/lnHWRsVS) or send us your CV to [contact@toeverything.info].
|
||||
|
||||
Feel free to include any extra information (GitHub link, previous projects, personal blog etc.).
|
||||
|
||||
@@ -296,3 +296,39 @@
|
||||
|
||||
[affine.pro]: http://affine.pro/
|
||||
[contact@toeverything.info]: mailto:contact@toeverything.info
|
||||
|
||||
- <b>Full stack or intern engineer - Mainly work with TypeScript</b> @[affine.pro]
|
||||
|
||||
<details><summary>TypeScript · BlockSuite · Remote</summary>
|
||||
<p>
|
||||
|
||||
## What we do
|
||||
|
||||
We **AFFiNE** hold a vision of shaping a world semantically connected through block components in modern applications.
|
||||
We're open for Fullstack Engineer positions across the BlockSuite sub-team. The **BlockSuite** team works on creating
|
||||
the best **block-editor** and **open-block** protocol for use in AFFiNE. Paving the way for a new generation of SaaS
|
||||
software and developers.
|
||||
|
||||
## Full stack or intern engineer
|
||||
|
||||
### This position is for
|
||||
|
||||
- Actively participate in Affine's open source work, responsible for implementing Affine's core features and continuously improving the user experience.
|
||||
- Optimise and improve the copy and paste function to increase the efficiency of user copy and paste operations.
|
||||
- Responsible for Affine's import and export work. Familiar with the data structure design of software such as Affine, Markdown, and Notion to ensure the accuracy of imported and exported data.
|
||||
|
||||
### What we're looking for
|
||||
|
||||
- Proficient in the JavaScript technology stack.
|
||||
- Good English communication and teamwork skills, able to communicate and collaborate effectively with team members both locally and internationally.
|
||||
- Passionate about open source software, familiar with the open source community and experience in open source projects preferred.
|
||||
- Willingness to take on challenging work, agile thinking, strong learning skills and ability to adapt quickly to new technology and job requirements.
|
||||
|
||||
## Contact us
|
||||
|
||||
Interested? Send us your CV to [contact@toeverything.info].
|
||||
|
||||
Feel free to include any extra information (GitHub link, previous projects, personal blog etc.).
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "AFFiNE",
|
||||
"version": "0.6.0-canary.2",
|
||||
"version": "0.6.0-canary.3",
|
||||
"private": true,
|
||||
"author": "toeverything",
|
||||
"license": "MPL-2.0",
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
"dependencies": {
|
||||
"dotenv": "^16.0.3"
|
||||
},
|
||||
"version": "0.6.0-canary.2"
|
||||
"version": "0.6.0-canary.3"
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import '@affine/component/theme/theme.css';
|
||||
import { useDarkMode } from 'storybook-dark-mode';
|
||||
|
||||
export const parameters = {
|
||||
backgrounds: { disable: true },
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
|
||||
@@ -84,5 +84,5 @@
|
||||
"wait-on": "^7.0.1",
|
||||
"yjs": "^13.6.1"
|
||||
},
|
||||
"version": "0.6.0-canary.2"
|
||||
"version": "0.6.0-canary.3"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { AffineLogoSimSBlue1_1Icon, CloseIcon } from '@blocksuite/icons';
|
||||
|
||||
import {
|
||||
@@ -18,16 +19,18 @@ export const DownloadTips = ({ onClose }: { onClose: () => void }) => {
|
||||
<div className={downloadTipStyle}>
|
||||
<AffineLogoSimSBlue1_1Icon className={downloadTipIconStyle} />
|
||||
<div className={downloadMessageStyle}>
|
||||
Enjoying the demo?
|
||||
<a
|
||||
className={linkStyle}
|
||||
href="https://github.com/toeverything/AFFiNE/releases"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Download the AFFiNE Client
|
||||
</a>
|
||||
for the full experience.
|
||||
<Trans i18nKey="com.affine.banner.content">
|
||||
Enjoying the demo?
|
||||
<a
|
||||
className={linkStyle}
|
||||
href="https://github.com/toeverything/AFFiNE/releases"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Download the AFFiNE Client
|
||||
</a>
|
||||
for the full experience.
|
||||
</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -8,15 +8,11 @@ export const root = style({
|
||||
border: '1px solid var(--affine-black-10)',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
height: '52px',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: '0 24px',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const icon = style({
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { PlusIcon } from '@blocksuite/icons';
|
||||
import clsx from 'clsx';
|
||||
import type React from 'react';
|
||||
|
||||
import { Spotlight } from '../spolight';
|
||||
import * as styles from './index.css';
|
||||
|
||||
interface AddPageButtonProps {
|
||||
@@ -26,6 +28,7 @@ export function AddPageButton({
|
||||
onClick={onClick}
|
||||
>
|
||||
<PlusIcon className={styles.icon} /> {t['New Page']()}
|
||||
<Spotlight />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,11 +13,12 @@ export const root = style({
|
||||
cursor: 'pointer',
|
||||
padding: '0 12px',
|
||||
position: 'relative',
|
||||
transition: 'all 0.3s ease',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
background: 'var(--affine-white-60)',
|
||||
},
|
||||
'&:before': {
|
||||
'&[data-has-update="true"]:before': {
|
||||
content: "''",
|
||||
position: 'absolute',
|
||||
top: '-3px',
|
||||
@@ -30,6 +31,9 @@ export const root = style({
|
||||
opacity: 1,
|
||||
transition: '0.3s ease',
|
||||
},
|
||||
'&[data-disabled="true"]': {
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
vars: {
|
||||
'--svg-dot-animation': `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 122 116'%3E%3Cpath id='b' stroke='%23fff' stroke-linecap='round' stroke-width='0' d='M17.9256 115C17.434 111.774 13.1701 104.086 13.4282 95.6465C13.6862 87.207 18.6628 76.0721 17.9256 64.3628C17.1883 52.6535 8.7772 35.9512 9.00452 25.3907C9.23185 14.8302 16.2114 5.06512 17.9256 1'/%3E%3Cpath id='d' stroke='%23fff' stroke-linecap='round' stroke-width='0' d='M84.1628 115C85.2376 112.055 94.5618 98.8394 93.9975 91.1338C93.4332 83.4281 82.5505 73.2615 84.1628 62.5704C85.775 51.8793 96.4803 35.4248 95.9832 25.7826C95.4861 16.1404 87.9113 4.71163 84.1628 1'/%3E%3Cpath id='f' stroke='%23fff' stroke-linecap='round' stroke-width='0' d='M37.0913 115C37.9604 111.921 44.4347 99.4545 45.3816 92.9773C48.9305 68.7011 35.7877 73.9552 37.0913 62.7781C38.3949 51.6011 47.3889 36.9895 46.9869 26.9091C46.585 16.8286 40.1222 4.88034 37.0913 1'/%3E%3Cpath id='h' stroke='%23fff' stroke-linecap='round' stroke-width='0' d='M112.443 115C111.698 112.235 108.25 106.542 107.715 93.7582C107.241 82.4286 107.229 83.9543 112.443 66.1429C116.085 44.0408 100.661 42.5908 101.006 33.539C101.35 24.4871 109.843 4.48439 112.443 1'/%3E%3Cg%3E%3Ccircle r='1.5' fill='rgba(96, 70, 254, 0.3)'%3E%3CanimateMotion dur='10s' repeatCount='indefinite'%3E%3Cmpath href='%23b' /%3E%3C/animateMotion%3E%3C/circle%3E%3C/g%3E%3Cg%3E%3Ccircle r='1' fill='rgba(96, 70, 254, 0.3)' fill-opacity='1' shape-rendering='crispEdges'%3E%3CanimateMotion dur='8s' repeatCount='indefinite'%3E%3Cmpath href='%23d' /%3E%3C/animateMotion%3E%3C/circle%3E%3C/g%3E%3Cg%3E%3Ccircle r='.5' fill='rgba(96, 70, 254, 0.3)' fill-opacity='1' shape-rendering='crispEdges'%3E%3CanimateMotion dur='4s' repeatCount='indefinite'%3E%3Cmpath href='%23f' /%3E%3C/animateMotion%3E%3C/circle%3E%3C/g%3E%3Cg%3E%3Ccircle r='.8' fill='rgba(96, 70, 254, 0.3)' fill-opacity='1' shape-rendering='crispEdges'%3E%3CanimateMotion dur='6s' repeatCount='indefinite'%3E%3Cmpath href='%23h' /%3E%3C/animateMotion%3E%3C/circle%3E%3C/g%3E%3C/svg%3E")`,
|
||||
@@ -42,39 +46,35 @@ export const icon = style({
|
||||
fontSize: '24px',
|
||||
});
|
||||
|
||||
export const particles = style({
|
||||
background: `var(--svg-dot-animation), var(--svg-dot-animation)`,
|
||||
backgroundRepeat: 'no-repeat, repeat',
|
||||
backgroundPosition: 'center, center top 100%',
|
||||
backgroundSize: '100%, 130%',
|
||||
WebkitMaskImage:
|
||||
'linear-gradient(to top, transparent, black, black, transparent)',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
export const closeIcon = style({
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
});
|
||||
|
||||
export const particlesBefore = style({
|
||||
content: '""',
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: `var(--svg-dot-animation), var(--svg-dot-animation), var(--svg-dot-animation)`,
|
||||
backgroundRepeat: 'no-repeat, repeat, repeat',
|
||||
backgroundPosition: 'center, center top 100%, center center',
|
||||
backgroundSize: '100% 120%, 150%, 120%',
|
||||
filter: 'blur(1px)',
|
||||
willChange: 'filter',
|
||||
top: '4px',
|
||||
right: '4px',
|
||||
height: '14px',
|
||||
width: '14px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: 'var(--affine-shadow-1)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
backgroundColor: 'var(--affine-background-primary-color)',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
transition: '0.1s',
|
||||
borderRadius: '50%',
|
||||
zIndex: 1,
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
transform: 'scale(1.1)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const installLabel = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flex: 1,
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
whiteSpace: 'nowrap',
|
||||
});
|
||||
@@ -82,6 +82,7 @@ export const installLabel = style({
|
||||
export const installLabelNormal = style([
|
||||
installLabel,
|
||||
{
|
||||
justifyContent: 'space-between',
|
||||
selectors: {
|
||||
[`${root}:hover &`]: {
|
||||
display: 'none',
|
||||
@@ -102,31 +103,111 @@ export const installLabelHover = style([
|
||||
},
|
||||
]);
|
||||
|
||||
export const halo = style({
|
||||
overflow: 'hidden',
|
||||
export const updateAvailableWrapper = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: '8px 0',
|
||||
});
|
||||
|
||||
export const versionLabel = style({
|
||||
padding: '0 6px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
fontSize: '10px',
|
||||
lineHeight: '18px',
|
||||
borderRadius: '4px',
|
||||
});
|
||||
|
||||
export const whatsNewLabel = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
whiteSpace: 'nowrap',
|
||||
});
|
||||
|
||||
export const progress = style({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '4px',
|
||||
borderRadius: '12px',
|
||||
background: 'var(--affine-black-10)',
|
||||
});
|
||||
|
||||
export const progressInner = style({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: '100%',
|
||||
borderRadius: '12px',
|
||||
background: 'var(--affine-primary-color)',
|
||||
transition: '0.1s',
|
||||
});
|
||||
|
||||
export const particles = style({
|
||||
background: `var(--svg-dot-animation), var(--svg-dot-animation)`,
|
||||
backgroundRepeat: 'no-repeat, repeat',
|
||||
backgroundPosition: 'center, center top 100%',
|
||||
backgroundSize: '100%, 130%',
|
||||
WebkitMaskImage:
|
||||
'linear-gradient(to top, transparent, black, black, transparent)',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
|
||||
export const particlesBefore = style({
|
||||
content: '""',
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: `var(--svg-dot-animation), var(--svg-dot-animation), var(--svg-dot-animation)`,
|
||||
backgroundRepeat: 'no-repeat, repeat, repeat',
|
||||
backgroundPosition: 'center, center top 100%, center center',
|
||||
backgroundSize: '100% 120%, 150%, 120%',
|
||||
filter: 'blur(1px)',
|
||||
willChange: 'filter',
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
|
||||
export const halo = style({
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
':before': {
|
||||
content: '""',
|
||||
display: 'block',
|
||||
width: '60%',
|
||||
height: '40%',
|
||||
inset: 0,
|
||||
position: 'absolute',
|
||||
top: '80%',
|
||||
left: '50%',
|
||||
background:
|
||||
'linear-gradient(180deg, rgba(50, 26, 206, 0.1) 10%, rgba(50, 26, 206, 0.35) 30%, rgba(84, 56, 255, 1) 50%)',
|
||||
filter: 'blur(10px) saturate(1.2)',
|
||||
transform: 'translateX(-50%) translateY(calc(0 * 1%)) scale(0)',
|
||||
transition: '0.3s ease',
|
||||
willChange: 'filter',
|
||||
willChange: 'filter, transform',
|
||||
transform: 'translateY(100%) scale(0.6)',
|
||||
background:
|
||||
'radial-gradient(ellipse 60% 80% at bottom, rgba(50, 26, 206, 0.35), transparent)',
|
||||
},
|
||||
':after': {
|
||||
content: '""',
|
||||
display: 'block',
|
||||
inset: 0,
|
||||
position: 'absolute',
|
||||
filter: 'blur(10px) saturate(1.2)',
|
||||
transition: '0.1s ease',
|
||||
willChange: 'filter, transform',
|
||||
transform: 'translateY(100%) scale(0.6)',
|
||||
background:
|
||||
'radial-gradient(ellipse 30% 45% at bottom, rgba(50, 26, 206, 0.6), transparent)',
|
||||
},
|
||||
selectors: {
|
||||
'&:hover:before': {
|
||||
transform: 'translateX(-50%) translateY(calc(-70 * 1%)) scale(1)',
|
||||
'&:hover:before, &:hover:after': {
|
||||
transform: 'translateY(0) scale(1)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { getEnvironment } from '@affine/env/config';
|
||||
import { atomWithObservable, atomWithStorage } from 'jotai/utils';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
// todo: move to utils?
|
||||
function rpcToObservable<
|
||||
T,
|
||||
H extends () => Promise<T>,
|
||||
E extends (callback: (t: T) => void) => () => void
|
||||
>(
|
||||
initialValue: T,
|
||||
{
|
||||
event,
|
||||
handler,
|
||||
onSubscribe,
|
||||
}: {
|
||||
event?: E;
|
||||
handler?: H;
|
||||
onSubscribe?: () => void;
|
||||
}
|
||||
) {
|
||||
return new Observable<T>(subscriber => {
|
||||
subscriber.next(initialValue);
|
||||
const environment = getEnvironment();
|
||||
onSubscribe?.();
|
||||
if (typeof window === 'undefined' || !environment.isDesktop || !event) {
|
||||
subscriber.complete();
|
||||
return () => {};
|
||||
}
|
||||
handler?.().then(t => {
|
||||
subscriber.next(t);
|
||||
});
|
||||
return event(t => {
|
||||
subscriber.next(t);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
type InferTFromEvent<E> = E extends (
|
||||
callback: (t: infer T) => void
|
||||
) => () => void
|
||||
? T
|
||||
: never;
|
||||
|
||||
type UpdateMeta = InferTFromEvent<typeof window.events.updater.onUpdateReady>;
|
||||
|
||||
export const updateReadyAtom = atomWithObservable(() => {
|
||||
return rpcToObservable(null as UpdateMeta | null, {
|
||||
event: window.events?.updater.onUpdateReady,
|
||||
});
|
||||
});
|
||||
|
||||
export const updateAvailableAtom = atomWithObservable(() => {
|
||||
return rpcToObservable(null as UpdateMeta | null, {
|
||||
event: window.events?.updater.onUpdateAvailable,
|
||||
onSubscribe: () => {
|
||||
window.apis?.updater.checkForUpdatesAndNotify();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export const downloadProgressAtom = atomWithObservable<number>(() => {
|
||||
return rpcToObservable(0, {
|
||||
event: window.events?.updater.onDownloadProgress,
|
||||
});
|
||||
});
|
||||
|
||||
export const changelogCheckedAtom = atomWithStorage<Record<string, boolean>>(
|
||||
'affine:client-changelog-checked',
|
||||
{}
|
||||
);
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
||||
import { AppUpdaterButton } from '.';
|
||||
|
||||
export default {
|
||||
title: 'Components/AppSidebar/AppUpdaterButton',
|
||||
component: AppUpdaterButton,
|
||||
} satisfies Meta;
|
||||
|
||||
export const Default: StoryFn = () => {
|
||||
return (
|
||||
<main style={{ width: '240px' }}>
|
||||
<AppUpdaterButton />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@@ -1,36 +1,167 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ResetIcon } from '@blocksuite/icons';
|
||||
import { CloseIcon, NewIcon, ResetIcon } from '@blocksuite/icons';
|
||||
import clsx from 'clsx';
|
||||
import { atom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import { startTransition } from 'react';
|
||||
|
||||
import * as styles from './index.css';
|
||||
import {
|
||||
changelogCheckedAtom,
|
||||
downloadProgressAtom,
|
||||
updateAvailableAtom,
|
||||
updateReadyAtom,
|
||||
} from './index.jotai';
|
||||
|
||||
interface AddPageButtonProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const currentVersionAtom = atom(async () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
const currentVersion = await window.apis?.updater.currentVersion();
|
||||
return currentVersion;
|
||||
});
|
||||
|
||||
const currentChangelogUnreadAtom = atom(async get => {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
const mapping = get(changelogCheckedAtom);
|
||||
const currentVersion = await get(currentVersionAtom);
|
||||
if (currentVersion) {
|
||||
return !mapping[currentVersion];
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Although it is called an input, it is actually a button.
|
||||
export function AppUpdaterButton({ className, style }: AddPageButtonProps) {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const currentChangelogUnread = useAtomValue(currentChangelogUnreadAtom);
|
||||
const updateReady = useAtomValue(updateReadyAtom);
|
||||
const updateAvailable = useAtomValue(updateAvailableAtom);
|
||||
const currentVersion = useAtomValue(currentVersionAtom);
|
||||
const downloadProgress = useAtomValue(downloadProgressAtom);
|
||||
const onReadOrDismissChangelog = useSetAtom(changelogCheckedAtom);
|
||||
|
||||
const onReadOrDismissCurrentChangelog = (visit: boolean) => {
|
||||
if (visit) {
|
||||
window.open(
|
||||
`https://github.com/toeverything/AFFiNE/releases/tag/v${currentVersion}`,
|
||||
'_blank'
|
||||
);
|
||||
}
|
||||
|
||||
startTransition(() =>
|
||||
onReadOrDismissChangelog(mapping => {
|
||||
return {
|
||||
...mapping,
|
||||
[currentVersion!]: true,
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
if (!updateAvailable && !currentChangelogUnread) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
data-testid="new-page-button"
|
||||
style={style}
|
||||
className={clsx([styles.root, className])}
|
||||
data-has-update={updateAvailable ? 'true' : 'false'}
|
||||
data-disabled={updateAvailable?.allowAutoUpdate && !updateReady}
|
||||
onClick={() => {
|
||||
window.apis?.updater.updateClient();
|
||||
if (updateReady) {
|
||||
window.apis?.updater.quitAndInstall();
|
||||
} else if (updateAvailable?.allowAutoUpdate) {
|
||||
// wait for download to finish
|
||||
} else if (updateAvailable || currentChangelogUnread) {
|
||||
onReadOrDismissCurrentChangelog(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{updateAvailable &&
|
||||
(updateAvailable.allowAutoUpdate
|
||||
? renderUpdateAvailableAllowAutoUpdate()
|
||||
: renderUpdateAvailableNotAllowAutoUpdate())}
|
||||
|
||||
{!updateAvailable && currentChangelogUnread && renderWhatsNew()}
|
||||
<div className={styles.particles} aria-hidden="true"></div>
|
||||
<span className={styles.halo} aria-hidden="true"></span>
|
||||
<div className={clsx([styles.installLabelNormal])}>
|
||||
<span>{t['Update Available']()}</span>
|
||||
</div>
|
||||
<div className={clsx([styles.installLabelHover])}>
|
||||
<ResetIcon className={styles.icon} />
|
||||
<span>{t['Restart Install Client Update']()}</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
function renderUpdateAvailableAllowAutoUpdate() {
|
||||
return (
|
||||
<div className={clsx([styles.updateAvailableWrapper])}>
|
||||
<div className={clsx([styles.installLabelNormal])}>
|
||||
<span>
|
||||
{!updateReady
|
||||
? t['com.affine.updater.downloading']()
|
||||
: t['com.affine.updater.update-available']()}
|
||||
</span>
|
||||
<span className={styles.versionLabel}>
|
||||
{updateAvailable?.version}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{updateReady ? (
|
||||
<div className={clsx([styles.installLabelHover])}>
|
||||
<ResetIcon className={styles.icon} />
|
||||
<span>{t['com.affine.updater.restart-to-update']()}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.progress}>
|
||||
<div
|
||||
className={styles.progressInner}
|
||||
style={{ width: `${downloadProgress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderUpdateAvailableNotAllowAutoUpdate() {
|
||||
return (
|
||||
<>
|
||||
<div className={clsx([styles.installLabelNormal])}>
|
||||
<span>{t['com.affine.updater.update-available']()}</span>
|
||||
<span className={styles.versionLabel}>
|
||||
{updateAvailable?.version}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={clsx([styles.installLabelHover])}>
|
||||
<span>{t['com.affine.updater.open-download-page']()}</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderWhatsNew() {
|
||||
return (
|
||||
<>
|
||||
<div className={clsx([styles.whatsNewLabel])}>
|
||||
<NewIcon className={styles.icon} />
|
||||
<span>{t[`Discover what's new!`]()}</span>
|
||||
</div>
|
||||
<div
|
||||
className={styles.closeIcon}
|
||||
onClick={e => {
|
||||
onReadOrDismissCurrentChangelog(false);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { atom } from 'jotai';
|
||||
import { atomWithObservable } from 'jotai/utils';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export const APP_SIDEBAR_OPEN = 'app-sidebar-open';
|
||||
export const appSidebarOpenAtom = atomWithStorage(
|
||||
@@ -15,20 +13,3 @@ export const appSidebarWidthAtom = atomWithStorage(
|
||||
'app-sidebar-width',
|
||||
256 /* px */
|
||||
);
|
||||
|
||||
export const updateAvailableAtom = atomWithObservable<boolean>(() => {
|
||||
return new Observable<boolean>(subscriber => {
|
||||
subscriber.next(false);
|
||||
if (typeof window !== 'undefined') {
|
||||
const isMacosDesktop = environment.isDesktop && environment.isMacOs;
|
||||
if (isMacosDesktop) {
|
||||
const dispose = window.events?.updater.onClientUpdateReady(() => {
|
||||
subscriber.next(true);
|
||||
});
|
||||
return () => {
|
||||
dispose?.();
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
appSidebarOpenAtom,
|
||||
appSidebarResizingAtom,
|
||||
appSidebarWidthAtom,
|
||||
updateAvailableAtom,
|
||||
} from './index.jotai';
|
||||
import { ResizeIndicator } from './resize-indicator';
|
||||
import type { SidebarHeaderProps } from './sidebar-header';
|
||||
@@ -122,9 +121,4 @@ export { AppSidebarFallback } from './fallback';
|
||||
export * from './menu-item';
|
||||
export * from './quick-search-input';
|
||||
export * from './sidebar-containers';
|
||||
export {
|
||||
appSidebarFloatingAtom,
|
||||
appSidebarOpenAtom,
|
||||
appSidebarResizingAtom,
|
||||
updateAvailableAtom,
|
||||
};
|
||||
export { appSidebarFloatingAtom, appSidebarOpenAtom, appSidebarResizingAtom };
|
||||
|
||||
@@ -22,6 +22,11 @@ export const root = style({
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
'&[data-active="true"]:hover': {
|
||||
background:
|
||||
// make this a variable?
|
||||
'linear-gradient(0deg, rgba(0, 0, 0, 0.04), rgba(0, 0, 0, 0.04)), rgba(0, 0, 0, 0.04);',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -34,7 +39,7 @@ export const content = style({
|
||||
export const icon = style({
|
||||
marginRight: '14px',
|
||||
color: 'var(--affine-icon-color)',
|
||||
fontSize: '18px',
|
||||
fontSize: '20px',
|
||||
});
|
||||
|
||||
export const spacer = style({
|
||||
|
||||
@@ -13,16 +13,13 @@ export const root = style({
|
||||
cursor: 'pointer',
|
||||
padding: '0 12px',
|
||||
margin: '12px 0',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
},
|
||||
},
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const icon = style({
|
||||
marginRight: '14px',
|
||||
color: 'var(--affine-icon-color)',
|
||||
fontSize: '20px',
|
||||
});
|
||||
|
||||
export const spacer = style({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { SearchIcon } from '@blocksuite/icons';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { Spotlight } from '../spolight';
|
||||
import * as styles from './index.css';
|
||||
|
||||
interface QuickSearchInputProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
@@ -27,6 +28,7 @@ export function QuickSearchInput({ onClick, ...props }: QuickSearchInputProps) {
|
||||
<div className={styles.shortcutHint}>
|
||||
{isMac ? ' ⌘ + K' : ' Ctrl + K'}
|
||||
</div>
|
||||
<Spotlight />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createVar, style } from '@vanilla-extract/css';
|
||||
|
||||
export const spotlightX = createVar();
|
||||
export const spotlightY = createVar();
|
||||
export const spotlightOpacity = createVar();
|
||||
export const spotlightSize = createVar();
|
||||
|
||||
export const spotlight = style({
|
||||
vars: {
|
||||
[spotlightX]: '0px',
|
||||
[spotlightY]: '0px',
|
||||
[spotlightOpacity]: '0',
|
||||
[spotlightSize]: '64px',
|
||||
},
|
||||
position: 'absolute',
|
||||
background: `radial-gradient(${spotlightSize} circle at ${spotlightX} ${spotlightY}, var(--affine-text-primary-color), transparent)`,
|
||||
inset: '0px',
|
||||
pointerEvents: 'none',
|
||||
willChange: 'background',
|
||||
opacity: spotlightOpacity,
|
||||
zIndex: 1,
|
||||
transition: 'all 0.2s',
|
||||
borderRadius: 'inherit',
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import { useTheme } from 'next-themes';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import * as styles from './index.css';
|
||||
|
||||
function useMouseOffset() {
|
||||
const [offset, setOffset] = React.useState<{ x: number; y: number }>();
|
||||
const [outside, setOutside] = React.useState(true);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && ref.current.parentElement) {
|
||||
const el = ref.current.parentElement;
|
||||
const bound = el.getBoundingClientRect();
|
||||
|
||||
// debounce?
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
setOffset({ x: e.clientX - bound.x, y: e.clientY - bound.y });
|
||||
setOutside(false);
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
setOutside(true);
|
||||
};
|
||||
el.addEventListener('mousemove', onMouseMove);
|
||||
el.addEventListener('mouseleave', onMouseLeave);
|
||||
return () => {
|
||||
el.removeEventListener('mousemove', onMouseMove);
|
||||
el.removeEventListener('mouseleave', onMouseLeave);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return [offset, outside, ref] as const;
|
||||
}
|
||||
|
||||
export function Spotlight() {
|
||||
const [offset, outside, ref] = useMouseOffset();
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const offsetVars = assignInlineVars({
|
||||
[styles.spotlightX]: (offset?.x ?? 0) + 'px',
|
||||
[styles.spotlightY]: (offset?.y ?? 0) + 'px',
|
||||
[styles.spotlightOpacity]: outside ? '0' : isDark ? '.1' : '0.07',
|
||||
});
|
||||
|
||||
return <div style={offsetVars} ref={ref} className={styles.spotlight} />;
|
||||
}
|
||||
25
packages/component/src/components/card/block-card/index.tsx
Normal file
25
packages/component/src/components/card/block-card/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const BlockCard = forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
left?: ReactNode;
|
||||
title: string;
|
||||
desc?: string;
|
||||
right?: ReactNode;
|
||||
} & HTMLAttributes<HTMLDivElement>
|
||||
>(({ left, title, desc, right, ...props }, ref) => {
|
||||
return (
|
||||
<div ref={ref} className={styles.blockCard} {...props}>
|
||||
{left && <div className={styles.blockCardAround}>{left}</div>}
|
||||
<div className={styles.blockCardContent}>
|
||||
<div>{title}</div>
|
||||
<div className={styles.blockCardDesc}>{desc}</div>
|
||||
</div>
|
||||
{right && <div className={styles.blockCardAround}>{right}</div>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
BlockCard.displayName = 'BlockCard';
|
||||
@@ -0,0 +1,35 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const blockCard = style({
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
padding: '8px 12px',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
backgroundColor: 'var(--affine-background-primary-color)',
|
||||
borderRadius: '4px',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
boxShadow: 'var(--affine-shadow-1)',
|
||||
},
|
||||
// TODO active styles
|
||||
},
|
||||
});
|
||||
|
||||
export const blockCardAround = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const blockCardContent = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
export const blockCardDesc = style({
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
});
|
||||
@@ -10,7 +10,7 @@ import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-
|
||||
import type { FC } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { WorkspaceAvatar } from '../workspace-avatar';
|
||||
import { WorkspaceAvatar } from '../../workspace-avatar';
|
||||
import {
|
||||
StyledCard,
|
||||
StyledSettingLink,
|
||||
@@ -1,5 +1,5 @@
|
||||
import { displayFlex, styled, textEllipsis } from '../..';
|
||||
import { IconButton } from '../..';
|
||||
import { displayFlex, styled, textEllipsis } from '../../../styles';
|
||||
import { IconButton } from '../../../ui/button/icon-button';
|
||||
|
||||
export const StyleWorkspaceInfo = styled('div')(() => {
|
||||
return {
|
||||
@@ -61,6 +61,9 @@ export const StyledCard = styled('div')<{
|
||||
pointerEvents: 'auto',
|
||||
},
|
||||
},
|
||||
'@media (max-width: 720px)': {
|
||||
width: '100%',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -11,7 +11,12 @@ import {
|
||||
} from '@affine/component';
|
||||
import { OperationCell, TrashOperationCell } from '@affine/component/page-list';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { FavoritedIcon, FavoriteIcon } from '@blocksuite/icons';
|
||||
import {
|
||||
ArrowDownBigIcon,
|
||||
ArrowUpBigIcon,
|
||||
FavoritedIcon,
|
||||
FavoriteIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { useMediaQuery, useTheme } from '@mui/material';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
@@ -21,15 +26,14 @@ import {
|
||||
StyledTitleLink,
|
||||
StyledTitleWrapper,
|
||||
} from './styles';
|
||||
|
||||
export type FavoriteTagProps = {
|
||||
active: boolean;
|
||||
};
|
||||
import { useSorter } from './use-sorter';
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const FavoriteTag = forwardRef<
|
||||
HTMLButtonElement,
|
||||
FavoriteTagProps & Omit<IconButtonProps, 'children'>
|
||||
{
|
||||
active: boolean;
|
||||
} & Omit<IconButtonProps, 'children'>
|
||||
>(({ active, onClick, ...props }, ref) => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
@@ -64,6 +68,9 @@ const FavoriteTag = forwardRef<
|
||||
export type PageListProps = {
|
||||
isPublicWorkspace?: boolean;
|
||||
list: ListData[];
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
listType: 'all' | 'favorite' | 'shared' | 'public';
|
||||
onClickPage: (pageId: string, newTab?: boolean) => void;
|
||||
};
|
||||
@@ -115,35 +122,77 @@ export const PageList: React.FC<PageListProps> = ({
|
||||
listType,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const sorter = useSorter<ListData>({
|
||||
data: list,
|
||||
key: 'createDate',
|
||||
order: 'desc',
|
||||
});
|
||||
|
||||
const isShared = listType === 'shared';
|
||||
|
||||
const theme = useTheme();
|
||||
const isSmallDevices = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
if (isSmallDevices) {
|
||||
return <PageListMobileView list={list} />;
|
||||
return <PageListMobileView list={sorter.data} />;
|
||||
}
|
||||
|
||||
const ListHead = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const titleList = [
|
||||
{
|
||||
key: 'title',
|
||||
text: t['Title'](),
|
||||
proportion: 0.5,
|
||||
},
|
||||
{
|
||||
key: 'createDate',
|
||||
text: t['Created'](),
|
||||
proportion: 0.2,
|
||||
},
|
||||
{
|
||||
key: 'updatedDate',
|
||||
text: isShared
|
||||
? // TODO deprecated
|
||||
'Shared'
|
||||
: t['Updated'](),
|
||||
proportion: 0.2,
|
||||
},
|
||||
{ key: 'unsortable_action', sortable: false },
|
||||
];
|
||||
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell proportion={0.5}>{t['Title']()}</TableCell>
|
||||
<TableCell proportion={0.2}>{t['Created']()}</TableCell>
|
||||
<TableCell proportion={0.2}>
|
||||
{isShared
|
||||
? // TODO add i18n
|
||||
'Shared'
|
||||
: t['Updated']()}
|
||||
</TableCell>
|
||||
<TableCell proportion={0.1}></TableCell>
|
||||
{titleList.map(({ key, text, proportion, sortable = true }) => (
|
||||
<TableCell
|
||||
key={key}
|
||||
proportion={proportion}
|
||||
active={sorter.key === key}
|
||||
onClick={
|
||||
sortable
|
||||
? () => sorter.shiftOrder(key as keyof ListData)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{ display: 'flex', alignItems: 'center', width: '100%' }}
|
||||
>
|
||||
{text}
|
||||
{sorter.key === key &&
|
||||
(sorter.order === 'asc' ? (
|
||||
<ArrowUpBigIcon width={24} height={24} />
|
||||
) : (
|
||||
<ArrowDownBigIcon width={24} height={24} />
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
);
|
||||
};
|
||||
|
||||
const ListItems = list.map(
|
||||
const ListItems = sorter.data.map(
|
||||
(
|
||||
{
|
||||
pageId,
|
||||
@@ -170,13 +219,6 @@ export const PageList: React.FC<PageListProps> = ({
|
||||
icon={icon}
|
||||
text={title || t['Untitled']()}
|
||||
data-testid="title"
|
||||
suffix={
|
||||
<FavoriteTag
|
||||
className={favorite ? '' : 'favorite-button'}
|
||||
onClick={bookmarkPage}
|
||||
active={!!favorite}
|
||||
/>
|
||||
}
|
||||
onClick={onClickPage}
|
||||
/>
|
||||
<TableCell
|
||||
@@ -195,9 +237,14 @@ export const PageList: React.FC<PageListProps> = ({
|
||||
</TableCell>
|
||||
{!isPublicWorkspace && (
|
||||
<TableCell
|
||||
style={{ padding: 0 }}
|
||||
style={{ padding: 0, display: 'flex', alignItems: 'center' }}
|
||||
data-testid={`more-actions-${pageId}`}
|
||||
>
|
||||
<FavoriteTag
|
||||
className={favorite ? '' : 'favorite-button'}
|
||||
onClick={bookmarkPage}
|
||||
active={!!favorite}
|
||||
/>
|
||||
<OperationCell
|
||||
title={title}
|
||||
favorite={favorite}
|
||||
|
||||
82
packages/component/src/components/page-list/use-sorter.ts
Normal file
82
packages/component/src/components/page-list/use-sorter.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
type Sorter<T> = {
|
||||
data: T[];
|
||||
key: keyof T;
|
||||
order: 'asc' | 'desc' | 'none';
|
||||
};
|
||||
|
||||
const defaultSortingFn = <T extends Record<keyof any, unknown>>(
|
||||
ctx: {
|
||||
key: keyof T;
|
||||
order: 'asc' | 'desc' | 'none';
|
||||
},
|
||||
a: T,
|
||||
b: T
|
||||
) => {
|
||||
const valA = a[ctx.key];
|
||||
const valB = b[ctx.key];
|
||||
const revert = ctx.order === 'desc';
|
||||
if (typeof valA !== typeof valB) {
|
||||
return 0;
|
||||
}
|
||||
if (typeof valA === 'string') {
|
||||
return valA.localeCompare(valB as string) * (revert ? 1 : -1);
|
||||
}
|
||||
if (typeof valA === 'number') {
|
||||
return valA - (valB as number) * (revert ? 1 : -1);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const useSorter = <T extends Record<keyof any, unknown>>({
|
||||
data,
|
||||
...defaultSorter
|
||||
}: Sorter<T> & { order: 'asc' | 'desc' }) => {
|
||||
const [sorter, setSorter] = useState<Omit<Sorter<T>, 'data'>>({
|
||||
...defaultSorter,
|
||||
// We should not show sorting icon at first time
|
||||
order: 'none',
|
||||
});
|
||||
const sortCtx =
|
||||
sorter.order === 'none'
|
||||
? {
|
||||
key: defaultSorter.key,
|
||||
order: defaultSorter.order,
|
||||
}
|
||||
: {
|
||||
key: sorter.key,
|
||||
order: sorter.order,
|
||||
};
|
||||
const sortingFn = (a: T, b: T) => defaultSortingFn(sortCtx, a, b);
|
||||
const sortedData = data.sort(sortingFn);
|
||||
|
||||
const shiftOrder = (key?: keyof T) => {
|
||||
const orders = ['asc', 'desc', 'none'] as const;
|
||||
if (key && key !== sorter.key) {
|
||||
// Key changed
|
||||
setSorter({
|
||||
...sorter,
|
||||
key,
|
||||
order: orders[0],
|
||||
});
|
||||
return;
|
||||
}
|
||||
setSorter({
|
||||
...sorter,
|
||||
order: orders[(orders.indexOf(sorter.order) + 1) % orders.length],
|
||||
});
|
||||
};
|
||||
return {
|
||||
data: sortedData,
|
||||
order: sorter.order,
|
||||
key: sorter.order !== 'none' ? sorter.key : null,
|
||||
/**
|
||||
* @deprecated In most cases, we no necessary use `setSorter` directly.
|
||||
*/
|
||||
updateSorter: (newVal: Partial<Sorter<T>>) =>
|
||||
setSorter({ ...sorter, ...newVal }),
|
||||
shiftOrder,
|
||||
resetSorter: () => setSorter(defaultSorter),
|
||||
};
|
||||
};
|
||||
@@ -9,7 +9,7 @@ import type { FC } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { Menu } from '../..';
|
||||
import { Menu } from '../../ui/menu/menu';
|
||||
import { Export } from './export';
|
||||
import { containerStyle, indicatorContainerStyle, tabStyle } from './index.css';
|
||||
import { SharePage } from './share-page';
|
||||
|
||||
@@ -1,29 +1,91 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { keyframes, style } from '@vanilla-extract/css';
|
||||
|
||||
export const modalStyle = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
backgroundColor: 'var(--affine-background-secondary-color)',
|
||||
borderRadius: '16px',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
export const titleContainerStyle = style({
|
||||
width: 'calc(100% - 72px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
height: '60px',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
export const titleStyle = style({
|
||||
fontSize: 'var(--affine-font-h6)',
|
||||
fontWeight: '600',
|
||||
marginTop: '12px',
|
||||
position: 'absolute',
|
||||
marginBottom: '12px',
|
||||
});
|
||||
const slideToLeft = keyframes({
|
||||
'0%': {
|
||||
transform: 'translateX(0)',
|
||||
opacity: 1,
|
||||
},
|
||||
'100%': {
|
||||
transform: 'translateX(-300px)',
|
||||
opacity: 0,
|
||||
},
|
||||
});
|
||||
const slideToRight = keyframes({
|
||||
'0%': {
|
||||
transform: 'translateX(0)',
|
||||
opacity: 1,
|
||||
},
|
||||
'100%': {
|
||||
transform: 'translateX(300px)',
|
||||
opacity: 0,
|
||||
},
|
||||
});
|
||||
const slideFormLeft = keyframes({
|
||||
'0%': {
|
||||
transform: 'translateX(300px)',
|
||||
opacity: 0,
|
||||
},
|
||||
'100%': {
|
||||
transform: 'translateX(0)',
|
||||
opacity: 1,
|
||||
},
|
||||
});
|
||||
const slideFormRight = keyframes({
|
||||
'0%': {
|
||||
transform: 'translateX(-300px)',
|
||||
opacity: 0,
|
||||
},
|
||||
'100%': {
|
||||
transform: 'translateX(0)',
|
||||
opacity: 1,
|
||||
},
|
||||
});
|
||||
export const formSlideToLeftStyle = style({
|
||||
animation: `${slideFormLeft} 0.3s ease-in-out forwards`,
|
||||
});
|
||||
export const formSlideToRightStyle = style({
|
||||
animation: `${slideFormRight} 0.3s ease-in-out forwards`,
|
||||
});
|
||||
export const slideToLeftStyle = style({
|
||||
animation: `${slideToLeft} 0.3s ease-in-out forwards`,
|
||||
});
|
||||
export const slideToRightStyle = style({
|
||||
animation: `${slideToRight} 0.3s ease-in-out forwards`,
|
||||
});
|
||||
|
||||
export const containerStyle = style({
|
||||
paddingTop: '25px',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
});
|
||||
export const videoContainerStyle = style({
|
||||
paddingTop: '25px',
|
||||
height: '300px',
|
||||
width: 'calc(100% - 72px)',
|
||||
display: 'flex',
|
||||
@@ -31,6 +93,7 @@ export const videoContainerStyle = style({
|
||||
flexGrow: 1,
|
||||
justifyContent: 'space-between',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
export const videoSlideStyle = style({
|
||||
width: '100%',
|
||||
@@ -46,9 +109,19 @@ export const videoStyle = style({
|
||||
border: '1px solid var(--affine-border-color)',
|
||||
transition: 'opacity 0.5s ease-in-out',
|
||||
});
|
||||
const fadeIn = keyframes({
|
||||
'0%': {
|
||||
transform: 'translateX(300px)',
|
||||
},
|
||||
'100%': {
|
||||
transform: 'translateX(0)',
|
||||
},
|
||||
});
|
||||
export const videoActiveStyle = style({
|
||||
animation: `${fadeIn} 0.5s ease-in-out forwards`,
|
||||
opacity: 0,
|
||||
});
|
||||
|
||||
export const arrowStyle = style({
|
||||
wordBreak: 'break-all',
|
||||
wordWrap: 'break-word',
|
||||
@@ -61,29 +134,52 @@ export const arrowStyle = style({
|
||||
flexGrow: 0.2,
|
||||
cursor: 'pointer',
|
||||
});
|
||||
export const descriptionContainerStyle = style({
|
||||
width: 'calc(100% - 112px)',
|
||||
height: '100px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const descriptionStyle = style({
|
||||
marginTop: '15px',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
padding: '0 56px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
lineHeight: '18px',
|
||||
marginBottom: '20px',
|
||||
position: 'absolute',
|
||||
});
|
||||
export const tabStyle = style({
|
||||
width: '40px',
|
||||
height: '20px',
|
||||
height: '40px',
|
||||
content: '""',
|
||||
borderBottom: '2px solid var(--affine-text-primary-color)',
|
||||
opacity: 0.2,
|
||||
margin: '0 10px 20px 0',
|
||||
margin: '40px 10px 40px 0',
|
||||
transition: 'all 0.15s ease-in-out',
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
':hover': {
|
||||
opacity: 1,
|
||||
},
|
||||
'::after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
bottom: '20px',
|
||||
left: '0',
|
||||
width: '100%',
|
||||
height: '2px',
|
||||
background: 'var(--affine-text-primary-color)',
|
||||
transition: 'all 0.15s ease-in-out',
|
||||
opacity: 0.2,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
});
|
||||
export const tabActiveStyle = style({
|
||||
opacity: 1,
|
||||
'::after': {
|
||||
opacity: 1,
|
||||
},
|
||||
});
|
||||
export const tabContainerStyle = style({
|
||||
width: '100%',
|
||||
|
||||
@@ -8,13 +8,18 @@ import { Modal, ModalCloseButton, ModalWrapper } from '../..';
|
||||
import {
|
||||
arrowStyle,
|
||||
containerStyle,
|
||||
descriptionContainerStyle,
|
||||
descriptionStyle,
|
||||
formSlideToLeftStyle,
|
||||
formSlideToRightStyle,
|
||||
modalStyle,
|
||||
slideToLeftStyle,
|
||||
slideToRightStyle,
|
||||
tabActiveStyle,
|
||||
tabContainerStyle,
|
||||
tabStyle,
|
||||
titleContainerStyle,
|
||||
titleStyle,
|
||||
videoActiveStyle,
|
||||
videoContainerStyle,
|
||||
videoSlideStyle,
|
||||
videoStyle,
|
||||
@@ -27,9 +32,9 @@ type TourModalProps = {
|
||||
|
||||
export const TourModal: FC<TourModalProps> = ({ open, onClose }) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [step, setStep] = useState(0);
|
||||
const [step, setStep] = useState(-1);
|
||||
const handleClose = () => {
|
||||
setStep(0);
|
||||
setStep(-1);
|
||||
onClose();
|
||||
};
|
||||
return (
|
||||
@@ -51,39 +56,59 @@ export const TourModal: FC<TourModalProps> = ({ open, onClose }) => {
|
||||
data-testid="onboarding-modal-close-button"
|
||||
/>
|
||||
<div className={modalStyle}>
|
||||
<div className={titleStyle}>
|
||||
{step === 1
|
||||
? t['com.affine.onboarding.title2']()
|
||||
: t['com.affine.onboarding.title1']()}
|
||||
<div className={titleContainerStyle}>
|
||||
{step !== -1 && (
|
||||
<div
|
||||
className={clsx(titleStyle, {
|
||||
[slideToLeftStyle]: step === 0,
|
||||
[formSlideToRightStyle]: step === 1,
|
||||
})}
|
||||
>
|
||||
{t['com.affine.onboarding.title2']()}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={clsx(titleStyle, {
|
||||
[slideToRightStyle]: step === 1,
|
||||
[formSlideToLeftStyle]: step === 0,
|
||||
})}
|
||||
>
|
||||
{t['com.affine.onboarding.title1']()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={containerStyle}>
|
||||
<div
|
||||
className={arrowStyle}
|
||||
onClick={() => setStep(0)}
|
||||
onClick={() => step === 1 && setStep(0)}
|
||||
data-testid="onboarding-modal-pre-button"
|
||||
>
|
||||
<ArrowLeftSmallIcon />
|
||||
</div>
|
||||
<div className={videoContainerStyle}>
|
||||
<div className={videoSlideStyle}>
|
||||
{step !== -1 && (
|
||||
<video
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
className={clsx(videoStyle, {
|
||||
[slideToLeftStyle]: step === 0,
|
||||
[formSlideToRightStyle]: step === 1,
|
||||
})}
|
||||
data-testid="onboarding-modal-editing-video"
|
||||
>
|
||||
<source src="/editingVideo.mp4" type="video/mp4" />
|
||||
<source src="/editingVideo.webm" type="video/webm" />
|
||||
</video>
|
||||
)}
|
||||
<video
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
className={clsx(videoStyle, {
|
||||
[videoActiveStyle]: step === 0,
|
||||
})}
|
||||
data-testid="onboarding-modal-editing-video"
|
||||
>
|
||||
<source src="/editingVideo.mp4" type="video/mp4" />
|
||||
<source src="/editingVideo.webm" type="video/webm" />
|
||||
</video>
|
||||
<video
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
className={clsx(videoStyle, {
|
||||
[videoActiveStyle]: step === 1,
|
||||
[slideToRightStyle]: step === 1,
|
||||
[formSlideToLeftStyle]: step === 0,
|
||||
})}
|
||||
data-testid="onboarding-modal-switch-video"
|
||||
>
|
||||
@@ -102,7 +127,9 @@ export const TourModal: FC<TourModalProps> = ({ open, onClose }) => {
|
||||
</div>
|
||||
<ul className={tabContainerStyle}>
|
||||
<li
|
||||
className={clsx(tabStyle, { [tabActiveStyle]: step === 0 })}
|
||||
className={clsx(tabStyle, {
|
||||
[tabActiveStyle]: step !== 1,
|
||||
})}
|
||||
onClick={() => setStep(0)}
|
||||
></li>
|
||||
<li
|
||||
@@ -110,10 +137,25 @@ export const TourModal: FC<TourModalProps> = ({ open, onClose }) => {
|
||||
onClick={() => setStep(1)}
|
||||
></li>
|
||||
</ul>
|
||||
<div className={descriptionStyle}>
|
||||
{step === 1
|
||||
? t['com.affine.onboarding.videoDescription2']()
|
||||
: t['com.affine.onboarding.videoDescription1']()}
|
||||
<div className={descriptionContainerStyle}>
|
||||
{step !== -1 && (
|
||||
<div
|
||||
className={clsx(descriptionStyle, {
|
||||
[slideToLeftStyle]: step === 0,
|
||||
[formSlideToRightStyle]: step === 1,
|
||||
})}
|
||||
>
|
||||
{t['com.affine.onboarding.videoDescription2']()}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={clsx(descriptionStyle, {
|
||||
[slideToRightStyle]: step === 1,
|
||||
[formSlideToLeftStyle]: step === 0,
|
||||
})}
|
||||
>
|
||||
{t['com.affine.onboarding.videoDescription1']()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const workspaceItemStyle = style({
|
||||
'@media': {
|
||||
'screen and (max-width: 720px)': {
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
import { SortableContext, useSortable } from '@dnd-kit/sortable';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { WorkspaceCard } from '../workspace-card';
|
||||
import { WorkspaceCard } from '../../components/card/workspace-card';
|
||||
import { workspaceItemStyle } from './index.css';
|
||||
|
||||
export type WorkspaceListProps = {
|
||||
disabled?: boolean;
|
||||
@@ -42,6 +43,7 @@ const SortableWorkspaceItem: FC<
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={workspaceItemStyle}
|
||||
data-testid="draggable-item"
|
||||
style={style}
|
||||
ref={setNodeRef}
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Button } from '..';
|
||||
import { Button } from '../ui/button/button';
|
||||
import { DropdownButton } from '../ui/button/dropdown';
|
||||
import type { ButtonProps } from '../ui/button/interface';
|
||||
import { Menu } from '../ui/menu/menu';
|
||||
import { toast } from '../ui/toast/toast';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Button',
|
||||
@@ -20,30 +23,72 @@ export const Primary = Template.bind(undefined);
|
||||
Primary.args = {
|
||||
type: 'primary',
|
||||
children: 'This is a primary button',
|
||||
onClick: () => toast('Click button'),
|
||||
};
|
||||
|
||||
export const Default = Template.bind(undefined);
|
||||
Default.args = {
|
||||
type: 'default',
|
||||
children: 'This is a default button',
|
||||
onClick: () => toast('Click button'),
|
||||
};
|
||||
|
||||
export const Light = Template.bind(undefined);
|
||||
Light.args = {
|
||||
type: 'light',
|
||||
children: 'This is a light button',
|
||||
onClick: () => toast('Click button'),
|
||||
};
|
||||
|
||||
export const Warning = Template.bind(undefined);
|
||||
Warning.args = {
|
||||
type: 'warning',
|
||||
children: 'This is a warning button',
|
||||
onClick: () => toast('Click button'),
|
||||
};
|
||||
|
||||
export const Danger = Template.bind(undefined);
|
||||
Danger.args = {
|
||||
type: 'danger',
|
||||
children: 'This is a danger button',
|
||||
onClick: () => toast('Click button'),
|
||||
};
|
||||
|
||||
export const Dropdown: StoryFn = ({ onClickDropDown, ...props }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<DropdownButton onClickDropDown={onClickDropDown} {...props}>
|
||||
Dropdown Button
|
||||
</DropdownButton>
|
||||
|
||||
<Menu
|
||||
visible={open}
|
||||
placement="bottom-end"
|
||||
trigger={['click']}
|
||||
width={235}
|
||||
disablePortal={true}
|
||||
onClickAway={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
content={<>Dropdown Menu</>}
|
||||
>
|
||||
<DropdownButton
|
||||
onClick={() => {
|
||||
toast('Click button');
|
||||
setOpen(false);
|
||||
}}
|
||||
onClickDropDown={() => setOpen(!open)}
|
||||
>
|
||||
Dropdown with Menu
|
||||
</DropdownButton>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Dropdown.args = {
|
||||
onClick: () => toast('Click button'),
|
||||
onClickDropDown: () => toast('Click dropdown'),
|
||||
};
|
||||
|
||||
export const Test: StoryFn<ButtonProps> = () => {
|
||||
|
||||
55
packages/component/src/stories/card.stories.tsx
Normal file
55
packages/component/src/stories/card.stories.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { BlockCard } from '@affine/component/card/block-card';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
|
||||
import { WorkspaceCard } from '../components/card/workspace-card';
|
||||
import { toast } from '../ui/toast';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Card',
|
||||
component: WorkspaceCard,
|
||||
};
|
||||
|
||||
const blockSuiteWorkspace = new Workspace({
|
||||
id: 'blocksuite-local',
|
||||
});
|
||||
|
||||
blockSuiteWorkspace.meta.setName('Hello World');
|
||||
|
||||
export const AffineWorkspaceCard = () => {
|
||||
return (
|
||||
<WorkspaceCard
|
||||
workspace={{
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
id: 'local',
|
||||
blockSuiteWorkspace,
|
||||
providers: [],
|
||||
}}
|
||||
onClick={() => {}}
|
||||
onSettingClick={() => {}}
|
||||
currentWorkspaceId={null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AffineBlockCard = () => {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
<BlockCard title={'New Page'} onClick={() => toast('clicked')} />
|
||||
<BlockCard
|
||||
title={'New Page'}
|
||||
desc={'Write with a blank page'}
|
||||
right={<PageIcon width={20} height={20} />}
|
||||
onClick={() => toast('clicked page')}
|
||||
/>
|
||||
<BlockCard
|
||||
title={'New Edgeless'}
|
||||
desc={'Draw with a blank whiteboard'}
|
||||
left={<PageIcon width={20} height={20} />}
|
||||
right={<EdgelessIcon width={20} height={20} />}
|
||||
onClick={() => toast('clicked edgeless')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -50,9 +50,9 @@ AffineAllPageList.args = {
|
||||
favorite: false,
|
||||
icon: <PageIcon />,
|
||||
isPublicPage: true,
|
||||
title: 'Example Public Page with long title that will be truncated',
|
||||
title: '1 Example Public Page with long title that will be truncated',
|
||||
createDate: '2021-01-01',
|
||||
updatedDate: '2021-01-01',
|
||||
updatedDate: '2021-01-02',
|
||||
bookmarkPage: () => toast('Bookmark page'),
|
||||
onClickPage: () => toast('Click page'),
|
||||
onDisablePublicSharing: () => toast('Disable public sharing'),
|
||||
@@ -64,8 +64,8 @@ AffineAllPageList.args = {
|
||||
favorite: true,
|
||||
isPublicPage: false,
|
||||
icon: <PageIcon />,
|
||||
title: 'Favorited Page',
|
||||
createDate: '2021-01-01',
|
||||
title: '2 Favorited Page',
|
||||
createDate: '2021-01-02',
|
||||
updatedDate: '2021-01-01',
|
||||
bookmarkPage: () => toast('Bookmark page'),
|
||||
onClickPage: () => toast('Click page'),
|
||||
@@ -90,7 +90,7 @@ AffineTrashPageList.args = {
|
||||
pageId: '1',
|
||||
icon: <PageIcon />,
|
||||
title: 'Example Page',
|
||||
updatedDate: '2021-01-01',
|
||||
updatedDate: '2021-02-01',
|
||||
createDate: '2021-01-01',
|
||||
trashDate: '2021-01-01',
|
||||
onClickPage: () => toast('Click page'),
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
|
||||
import { WorkspaceCard } from '../components/workspace-card';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/WorkspaceCard',
|
||||
component: WorkspaceCard,
|
||||
};
|
||||
|
||||
const blockSuiteWorkspace = new Workspace({
|
||||
id: 'blocksuite-local',
|
||||
});
|
||||
|
||||
blockSuiteWorkspace.meta.setName('Hello World');
|
||||
|
||||
export const Basic = () => {
|
||||
return (
|
||||
<WorkspaceCard
|
||||
workspace={{
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
id: 'local',
|
||||
blockSuiteWorkspace,
|
||||
providers: [],
|
||||
}}
|
||||
onClick={() => {}}
|
||||
onSettingClick={() => {}}
|
||||
currentWorkspaceId={null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -15,3 +15,9 @@ globalStyle('html', {
|
||||
globalStyle('html[data-theme="dark"]', {
|
||||
vars: darkCssVariables,
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
globalStyle('.undefined', {
|
||||
border: '5px solid red !important',
|
||||
});
|
||||
}
|
||||
|
||||
35
packages/component/src/ui/button/dropdown.tsx
Normal file
35
packages/component/src/ui/button/dropdown.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ArrowDownSmallIcon } from '@blocksuite/icons';
|
||||
import {
|
||||
type ButtonHTMLAttributes,
|
||||
forwardRef,
|
||||
type MouseEventHandler,
|
||||
} from 'react';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
|
||||
type DropdownButtonProps = {
|
||||
onClickDropDown?: MouseEventHandler<SVGSVGElement>;
|
||||
} & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
export const DropdownButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
DropdownButtonProps
|
||||
>(({ onClickDropDown, children, ...props }, ref) => {
|
||||
const handleClickDropDown: MouseEventHandler<SVGSVGElement> = e => {
|
||||
e.stopPropagation();
|
||||
onClickDropDown?.(e);
|
||||
};
|
||||
return (
|
||||
<button ref={ref} className={styles.dropdownBtn} {...props}>
|
||||
<span>{children}</span>
|
||||
<span className={styles.divider} />
|
||||
<ArrowDownSmallIcon
|
||||
className={styles.icon}
|
||||
width={16}
|
||||
height={16}
|
||||
onClick={handleClickDropDown}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
DropdownButton.displayName = 'SimpleDropdownButton';
|
||||
40
packages/component/src/ui/button/styles.css.ts
Normal file
40
packages/component/src/ui/button/styles.css.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const dropdownBtn = style({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '0 10px',
|
||||
gap: '4px',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontWeight: 600,
|
||||
background: 'var(--affine-button-gray-color)',
|
||||
boxShadow: 'var(--affine-float-button-shadow)',
|
||||
borderRadius: '8px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
// width: '100%',
|
||||
height: '32px',
|
||||
userSelect: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
cursor: 'pointer',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const divider = style({
|
||||
width: '0.5px',
|
||||
height: '16px',
|
||||
background: 'var(--affine-border-color)',
|
||||
});
|
||||
|
||||
export const icon = style({
|
||||
borderRadius: '4px',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -97,6 +97,10 @@ export const Popper = ({
|
||||
};
|
||||
});
|
||||
|
||||
const mergedClass = [anchorClassName, children.props.className]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<ClickAwayListener
|
||||
onClickAway={() => {
|
||||
@@ -122,9 +126,11 @@ export const Popper = ({
|
||||
},
|
||||
onPointerEnter: onPointerEnterHandler,
|
||||
onPointerLeave: onPointerLeaveHandler,
|
||||
className: `${anchorClassName ? anchorClassName + ' ' : ''}${
|
||||
children.props.className
|
||||
}`,
|
||||
...(mergedClass
|
||||
? {
|
||||
className: mergedClass,
|
||||
}
|
||||
: {}),
|
||||
})}
|
||||
{content && (
|
||||
<BasicStyledPopper
|
||||
|
||||
@@ -4,6 +4,7 @@ export type TableCellProps = {
|
||||
align?: 'left' | 'right' | 'center';
|
||||
ellipsis?: boolean;
|
||||
proportion?: number;
|
||||
active?: boolean;
|
||||
style?: CSSProperties;
|
||||
} & PropsWithChildren &
|
||||
HTMLAttributes<HTMLTableCellElement>;
|
||||
|
||||
@@ -21,25 +21,40 @@ export const StyledTableBody = styled('tbody')(() => {
|
||||
});
|
||||
|
||||
export const StyledTableCell = styled('td')<
|
||||
Pick<TableCellProps, 'ellipsis' | 'align' | 'proportion'>
|
||||
>(({ align = 'left', ellipsis = false, proportion }) => {
|
||||
const width = proportion ? `${proportion * 100}%` : 'auto';
|
||||
return {
|
||||
width,
|
||||
height: '52px',
|
||||
lineHeight: '52px',
|
||||
padding: '0 30px',
|
||||
boxSizing: 'border-box',
|
||||
textAlign: align,
|
||||
verticalAlign: 'middle',
|
||||
...(ellipsis ? textEllipsis(1) : {}),
|
||||
overflowWrap: 'break-word',
|
||||
};
|
||||
});
|
||||
Pick<
|
||||
TableCellProps,
|
||||
'ellipsis' | 'align' | 'proportion' | 'active' | 'onClick'
|
||||
>
|
||||
>(
|
||||
({
|
||||
align = 'left',
|
||||
ellipsis = false,
|
||||
proportion,
|
||||
active = false,
|
||||
onClick,
|
||||
}) => {
|
||||
const width = proportion ? `${proportion * 100}%` : 'auto';
|
||||
return {
|
||||
width,
|
||||
height: '52px',
|
||||
lineHeight: '52px',
|
||||
padding: '0 30px',
|
||||
boxSizing: 'border-box',
|
||||
textAlign: align,
|
||||
verticalAlign: 'middle',
|
||||
overflowWrap: 'break-word',
|
||||
userSelect: 'none',
|
||||
...(active ? { color: 'var(--affine-text-primary-color)' } : {}),
|
||||
...(ellipsis ? textEllipsis(1) : {}),
|
||||
...(onClick ? { cursor: 'pointer' } : {}),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledTableHead = styled('thead')(() => {
|
||||
return {
|
||||
fontWeight: 500,
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
tr: {
|
||||
td: {
|
||||
whiteSpace: 'nowrap',
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from './quick-search-tips';
|
||||
export * from './tooltip';
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import type { TooltipProps } from '@mui/material';
|
||||
|
||||
import { css, displayFlex, styled } from '../../styles';
|
||||
import { Popper, type PopperProps } from '../popper';
|
||||
import StyledPopperContainer from '../shared/container';
|
||||
const StyledTooltip = styled(StyledPopperContainer)(() => {
|
||||
return {
|
||||
width: '390px',
|
||||
minHeight: '92px',
|
||||
padding: '12px',
|
||||
backgroundColor: 'var(--affine-tertiary-color)',
|
||||
transform: 'all 0.15s',
|
||||
color: 'var(--affine-text-emphasis-color)',
|
||||
...displayFlex('center', 'center'),
|
||||
border: `1px solid var(--affine-text-emphasis-color)`,
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
lineHeight: '22px',
|
||||
fontWeight: 500,
|
||||
};
|
||||
});
|
||||
|
||||
const StyledCircleContainer = styled('div')(() => {
|
||||
return css`
|
||||
position: relative;
|
||||
content: '';
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(0%, 0%);
|
||||
width: 0;
|
||||
height: 40px;
|
||||
border-right: 1px solid var(--affine-text-emphasis-color);
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -100%);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--affine-text-emphasis-color);
|
||||
}
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -150%);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--affine-text-emphasis-color);
|
||||
border: 1px solid var(--affine-text-emphasis-color);
|
||||
`;
|
||||
});
|
||||
|
||||
export const QuickSearchTips = (
|
||||
props: PopperProps & Omit<TooltipProps, 'title' | 'content'>
|
||||
) => {
|
||||
const { content, placement = 'top', children } = props;
|
||||
return (
|
||||
<Popper
|
||||
{...props}
|
||||
content={
|
||||
<div>
|
||||
<StyledCircleContainer />
|
||||
<StyledTooltip placement={placement}>{content}</StyledTooltip>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Popper>
|
||||
);
|
||||
};
|
||||
@@ -9,5 +9,5 @@
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.7"
|
||||
},
|
||||
"version": "0.6.0-canary.2"
|
||||
"version": "0.6.0-canary.3"
|
||||
}
|
||||
|
||||
2
packages/env/package.json
vendored
2
packages/env/package.json
vendored
@@ -22,5 +22,5 @@
|
||||
"dependencies": {
|
||||
"lit": "^2.7.4"
|
||||
},
|
||||
"version": "0.6.0-canary.2"
|
||||
"version": "0.6.0-canary.3"
|
||||
}
|
||||
|
||||
2
packages/env/src/blocksuite.ts
vendored
2
packages/env/src/blocksuite.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import markdown from '@affine/templates/Welcome-to-AFFiNE.md';
|
||||
import markdown from '@affine/templates/AFFiNE-beta-0.5.4.md';
|
||||
import { ContentParser } from '@blocksuite/blocks/content-parser';
|
||||
import type { Page, Workspace } from '@blocksuite/store';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/graphql",
|
||||
"version": "0.6.0-canary.2",
|
||||
"version": "0.6.0-canary.3",
|
||||
"description": "Autogenerated GraphQL client for affine.pro",
|
||||
"license": "MPL-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"./*": "./src/*"
|
||||
},
|
||||
"private": true,
|
||||
"version": "0.6.0-canary.2"
|
||||
"version": "0.6.0-canary.3"
|
||||
}
|
||||
|
||||
@@ -40,5 +40,5 @@
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.4"
|
||||
},
|
||||
"version": "0.6.0-canary.2"
|
||||
"version": "0.6.0-canary.3"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"Copied link to clipboard": "Link in die Zwischenablage kopiert",
|
||||
"Local Workspace Description": "Alle Daten sind auf dem aktuellen Gerät gespeichert. Du kannst AFFiNE Cloud für diesen Workspace aktivieren, um deine Daten mit der Cloud zu synchronisieren.",
|
||||
"Download all data": "Alle Daten herunterladen",
|
||||
"It takes up little space on your device": "Es verbraucht nur wenig Speicherplatz auf deinem Gerät.",
|
||||
"It takes up little space on your device": "Es nimmt nur wenig Platz auf deinem Gerät ein.",
|
||||
"Help and Feedback": "Hilfe und Feedback",
|
||||
"Not now": "Vielleicht später",
|
||||
"Please make sure you are online": "Bitte stelle sicher, dass du online bist",
|
||||
@@ -22,9 +22,9 @@
|
||||
"AFFiNE Cloud": "AFFiNE Cloud",
|
||||
"It takes up more space on your device": "Es verbraucht mehr Speicherplatz auf deinem Gerät.",
|
||||
"Sign out description": "Nach dem Abmelden gehen alle nicht synchronisierten Inhalte verloren.",
|
||||
"Export AFFiNE backup file": "AFFiNE-Backup als Datei exportieren",
|
||||
"Members": "Mitglieder",
|
||||
"Saved then enable AFFiNE Cloud": "Alle Änderungen werden lokal gespeichert. Klicke hier, um AFFiNE Cloud zu aktivieren.",
|
||||
"Export AFFiNE backup file": "AFFiNE-Backup als Datei exportieren",
|
||||
"Joined Workspace": "Workspace beigetreten",
|
||||
"404 - Page Not Found": "404 - Seite nicht gefunden",
|
||||
"Add to Favorites": "Zu Favoriten hinzufügen",
|
||||
@@ -190,13 +190,10 @@
|
||||
"Data sync mode": "Daten-Sync Modus",
|
||||
"Download core data": "Core Daten herunterladen",
|
||||
"Check Our Docs": "Sieh dir unsere Dokumentation an",
|
||||
"Get in touch! Join our communities": "Bleib mit uns in Kontakt und trete unseren Communitys bei!",
|
||||
"Get in touch! Join our communities": "Nimm teil! Treten Sie unseren Communities bei.",
|
||||
"Move to Trash": "In Papierkorb verschieben",
|
||||
"Placeholder of delete workspace": "Bitte zur Bestätigung den Workspace-Namen eingeben",
|
||||
"Access level": "Zugriffsberechtigung",
|
||||
"Loading Page": "Lade Seite",
|
||||
"Loading All Workspaces": "Lade alle Workspaces",
|
||||
"Discover what's new!": "Erfahre was neu ist!",
|
||||
"Move to": "Verschieben zu",
|
||||
"Disable": "Deaktivieren",
|
||||
"Open Workspace Settings": "Workspace Einstellungen öffnen",
|
||||
@@ -204,21 +201,18 @@
|
||||
"Workspace Not Found": "Workspace nicht gefunden",
|
||||
"Rename": "Umbenennen",
|
||||
"Page is Loading": "Seite lädt",
|
||||
"Discover what's new!": "Erfahre was neu ist!",
|
||||
"Loading All Workspaces": "Lade alle Workspaces",
|
||||
"Loading Page": "Lade Seite",
|
||||
"Create Shared Link Description": "Erstelle einen Link, den du leicht mit jedem teilen kannst.",
|
||||
"Back to Quick Search": "Zurück zur Schnellsuche",
|
||||
"Disable Public Link": "Öffentlichen Link deaktivieren",
|
||||
"Disable Public Link ?": "Öffentlichen Link deaktivieren ?",
|
||||
"Export Shared Pages Description": "Laden eine statische Kopie dieser Seite herunter, um sie mit anderen zu teilen.",
|
||||
"Favorite pages for easy access": "Favoriten-Seiten für schnellen Zugriff",
|
||||
"Move page to": "Seite verschieben nach...",
|
||||
"Navigation Path": "Navigationspfad",
|
||||
"Pivots": "Pivots",
|
||||
"RFP": "Seiten können frei zu Pivots hinzugefügt/entfernt werden und bleiben über \"Alle Seiten\" zugänglich.",
|
||||
"Remove from Pivots": "Von Pivots entfernen",
|
||||
"Router is Loading": "Router lädt",
|
||||
"Share Menu Public Workspace Description1": "Laden andere ein, dem Workspace beizutreten oder veröffentliche ihn im Internet.",
|
||||
"Share Menu Public Workspace Description2": "Der aktuelle Workspace wurde im Internet als öffentlicher Workspace veröffentlicht.",
|
||||
"View Navigation Path": "Navigationspfad ansehen",
|
||||
"Add a subpage inside": "Unterseite hinzufügen",
|
||||
"Disable Public Link Description": "Wenn du diesen öffentlichen Link deaktivierst, können andere Personen mit diesem Link nicht mehr auf diese Seite zugreifen.",
|
||||
"Disable Public Sharing": "Öffentliche Freigabe deaktivieren",
|
||||
@@ -228,8 +222,58 @@
|
||||
"Shared Pages Description": "Die öffentliche Freigabe der Seite erfordert den AFFiNE-Cloud-Dienst.",
|
||||
"Shared Pages In Public Workspace Description": "Der gesamte Workspace wird im Web veröffentlicht und kann über <1>Workspace Einstellungen</1> bearbeitet werden.",
|
||||
"emptySharedPages": "Freigegebene Seiten werden hier angezeigt.",
|
||||
"Recent": "Neueste",
|
||||
"Synced with AFFiNE Cloud": "Synchronisiert mit AFFiNE Cloud",
|
||||
"Successfully deleted": "Erfolgreich gelöscht",
|
||||
"You cannot delete the last workspace": "Du kannst den letzten Workspace nicht löschen"
|
||||
"You cannot delete the last workspace": "Du kannst den letzten Workspace nicht löschen",
|
||||
"Move page to": "Seite verschieben nach...",
|
||||
"Navigation Path": "Navigationspfad",
|
||||
"Recent": "Neueste",
|
||||
"Export Shared Pages Description": "Laden eine statische Kopie dieser Seite herunter, um sie mit anderen zu teilen.",
|
||||
"Share Menu Public Workspace Description1": "Laden andere ein, dem Workspace beizutreten oder veröffentliche ihn im Internet.",
|
||||
"Share Menu Public Workspace Description2": "Der aktuelle Workspace wurde im Internet als öffentlicher Workspace veröffentlicht.",
|
||||
"View Navigation Path": "Navigationspfad ansehen",
|
||||
"Delete Workspace Label Hint": "Wenn dieser Workspace gelöscht wird, wird sein gesamter Inhalt für alle Benutzer dauerhaft gelöscht. Niemand wird in der Lage sein, den Inhalt dieses Workspaces wiederherzustellen.",
|
||||
"Added Successfully": "Erfolgreich hinzugefügt",
|
||||
"Continue": "Fortfahren",
|
||||
"Create your own workspace": "Eigenen Workspace erstellen",
|
||||
"Created Successfully": "Erfolgreich erstellt",
|
||||
"Customize": "Anpassen",
|
||||
"FILE_ALREADY_EXISTS": "Datei existiert bereits",
|
||||
"Move folder": "Ordner verschieben",
|
||||
"Move folder hint": "Neuen Speicherort auswählen.",
|
||||
"Add Workspace Hint": "Auswählen, was du schon hast",
|
||||
"Change avatar hint": "Avatar von allen Mitgliedern ändern.",
|
||||
"Change workspace name hint": "Name von allen Mitgliedern ändern.",
|
||||
"DB_FILE_ALREADY_LOADED": "Datenbankdatei bereits geladen",
|
||||
"DB_FILE_INVALID": "Ungültige Datenbankdatei",
|
||||
"DB_FILE_PATH_INVALID": "Pfad der Datenbankdatei ungültig",
|
||||
"Default Location": "Standard-Speicherort",
|
||||
"Default db location hint": "Standardmäßig wird unter {{location}} gespeichert.",
|
||||
"Move folder success": "Ordnerverschiebung erfolgreich",
|
||||
"Open folder": "Ordner öffnen",
|
||||
"Save": "Speichern",
|
||||
"Set database location": "Datenbankstandort festlegen",
|
||||
"UNKNOWN_ERROR": "Unbekannter Fehler",
|
||||
"Open folder hint": "Prüfe, wo sich der Speicherordner befindet.",
|
||||
"Storage Folder": "Speicherordner",
|
||||
"Storage Folder Hint": "Speicherort überprüfen oder ändern.",
|
||||
"Sync across devices with AFFiNE Cloud": "Geräteübergreifende Synchronisierung mit AFFiNE Cloud",
|
||||
"Name Your Workspace": "Workspace benennen",
|
||||
"Update Available": "Update verfügbar",
|
||||
"com.affine.edgelessMode": "Edgeless-Modus",
|
||||
"com.affine.onboarding.title2": "Intuitive und robuste, blockbasierte Bearbeitung",
|
||||
"com.affine.onboarding.videoDescription1": "Wechsle mühelos zwischen dem Seitenmodus für die strukturierte Dokumentenerstellung und dem Whiteboard-Modus für den Ausdruck kreativer Ideen in freier Form.",
|
||||
"com.affine.onboarding.videoDescription2": "Verwende eine modulare Schnittstelle, um strukturierte Dokumente zu erstellen, indem du Textblöcke, Bilder und andere Inhalte einfach per Drag-and-drop anordnen kannst.",
|
||||
"com.affine.onboarding.title1": "Hyperfusion von Whiteboard und Dokumenten",
|
||||
"com.affine.pageMode": "Seitenmodus",
|
||||
"Update workspace name success": "Update vom Workspace-Namen erfolgreich",
|
||||
"Use on current device only": "Nur auf dem aktuellen Gerät verwenden",
|
||||
"Workspace database storage description": "Wähle den Ort, an dem du deinen Workspace erstellen möchten. Die Daten vom Workspace werden standardmäßig lokal gespeichert.",
|
||||
"dark": "dunkel",
|
||||
"system": "system",
|
||||
"Restart Install Client Update": "Neustart zum Installieren des Updates",
|
||||
"Add Workspace": "Workspace hinzufügen",
|
||||
"Export success": "Export erfolgreich",
|
||||
"light": "hell",
|
||||
"others": "Andere"
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
"It takes up little space on your device": "It takes up little space on your device.",
|
||||
"AFFiNE Cloud": "AFFiNE Cloud",
|
||||
"Members": "Members",
|
||||
"Add to favorites": "Add to favorites",
|
||||
"Add to favorites": "Add to favourites",
|
||||
"It takes up more space on your device": "It takes up more space on your device.",
|
||||
"Export AFFiNE backup file": "Export AFFiNE backup file",
|
||||
"Saved then enable AFFiNE Cloud": "All changes are saved locally, click to enable AFFiNE Cloud.",
|
||||
"Help and Feedback": "Help and Feedback",
|
||||
"Export AFFiNE backup file": "Export AFFiNE backup file",
|
||||
"Not now": "Not now",
|
||||
"Export Description": "You can export the entire Workspace data for backup, and the exported data can be re-imported.",
|
||||
"Remove from workspace": "Remove from workspace",
|
||||
@@ -18,7 +18,6 @@
|
||||
"No item": "No item",
|
||||
"Import": "Import",
|
||||
"Trash": "Trash",
|
||||
"others": "Others",
|
||||
"New Page": "New Page",
|
||||
"New Keyword Page": "New '{{query}}' page",
|
||||
"Find 0 result": "Find 0 result",
|
||||
@@ -27,7 +26,7 @@
|
||||
"Expand sidebar": "Expand sidebar",
|
||||
"Paper": "Paper",
|
||||
"Edgeless": "Edgeless",
|
||||
"Added to Favorites": "Added to Favorites",
|
||||
"Added to Favorites": "Added to Favourites",
|
||||
"Convert to ": "Convert to ",
|
||||
"Page": "Page",
|
||||
"Export": "Export",
|
||||
@@ -42,13 +41,11 @@
|
||||
"Delete page?": "Delete page?",
|
||||
"Delete permanently?": "Delete permanently?",
|
||||
"will be moved to Trash": "{{title}} will be moved to Trash",
|
||||
"Favorite": "Favorite",
|
||||
"Favorite": "Favourite",
|
||||
"Moved to Trash": "Moved to Trash",
|
||||
"Permanently deleted": "Permanently deleted",
|
||||
"restored": "{{title}} restored",
|
||||
"Cancel": "Cancel",
|
||||
"Keyboard Shortcuts": "Keyboard shortcuts",
|
||||
"Contact Us": "Contact us",
|
||||
"Official Website": "Official Website",
|
||||
"Get in touch!": "Get in touch!",
|
||||
"AFFiNE Community": "AFFiNE Community",
|
||||
@@ -62,7 +59,7 @@
|
||||
"Strikethrough": "Strikethrough",
|
||||
"Inline code": "Inline code",
|
||||
"Code block": "Code block",
|
||||
"Favorited": "Favorited",
|
||||
"Favorited": "Favourited",
|
||||
"Body text": "Body text",
|
||||
"Heading": "Heading {{number}}",
|
||||
"Increase indent": "Increase indent",
|
||||
@@ -70,9 +67,11 @@
|
||||
"Markdown Syntax": "Markdown Syntax",
|
||||
"Divider": "Divider",
|
||||
"Once deleted, you can't undo this action": "Once deleted, you can't undo this action.",
|
||||
"Remove from favorites": "Remove from favorites",
|
||||
"Removed from Favorites": "Removed from Favorites",
|
||||
"Add to Favorites": "Add to Favorites",
|
||||
"Remove from favorites": "Remove from favourites",
|
||||
"Removed from Favorites": "Removed from Favourites",
|
||||
"Add to Favorites": "Add to Favourites",
|
||||
"Contact Us": "Contact us",
|
||||
"Keyboard Shortcuts": "Keyboard shortcuts",
|
||||
"Jump to": "Jump to",
|
||||
"404 - Page Not Found": "404 - Page Not Found",
|
||||
"New Workspace": "New Workspace",
|
||||
@@ -154,7 +153,7 @@
|
||||
"core": "core",
|
||||
"all": "all",
|
||||
"Data sync mode": "Data sync mode",
|
||||
"Favorites": "Favorites",
|
||||
"Favorites": "Favourites",
|
||||
"Check Our Docs": "Check Our Docs",
|
||||
"Get in touch! Join our communities": "Get in touch! Join our communities.",
|
||||
"Download data": "Download {{CoreOrAll}} data",
|
||||
@@ -187,7 +186,7 @@
|
||||
"Sign out": "Sign out",
|
||||
"All data has been stored in the cloud": "All data has been stored in the cloud. ",
|
||||
"Download all data": "Download all data",
|
||||
"emptyFavorite": "Click Add to Favorites and the page will appear here.",
|
||||
"emptyFavorite": "Click Add to Favourites and the page will appear here.",
|
||||
"Edit": "Edit",
|
||||
"Sign out description": "Signing out will cause the unsynchronised content to be lost.",
|
||||
"Retain cached cloud data": "Retain cached cloud data",
|
||||
@@ -219,7 +218,7 @@
|
||||
"Create Shared Link Description": "Create a link you can easily share with anyone.",
|
||||
"Shared Pages In Public Workspace Description": "The entire Workspace is published on the web and can be edited via <1>Workspace Settings</1>.",
|
||||
"Organize pages to build knowledge": "Organize pages to build knowledge",
|
||||
"Favorite pages for easy access": "Favorite pages for easy access",
|
||||
"Favorite pages for easy access": "Favourite pages for easy access",
|
||||
"Loading All Workspaces": "Loading All Workspaces",
|
||||
"Router is Loading": "Router is Loading",
|
||||
"Finding Workspace ID": "Finding Workspace ID",
|
||||
@@ -232,10 +231,7 @@
|
||||
"Synced with AFFiNE Cloud": "Synced with AFFiNE Cloud",
|
||||
"Recent": "Recent",
|
||||
"Successfully deleted": "Successfully deleted",
|
||||
"Update Available": "Update available",
|
||||
"Restart Install Client Update": "Restart to install update",
|
||||
"Add Workspace": "Add Workspace",
|
||||
"Add Workspace Hint": "Select where you already have",
|
||||
"Export success": "Export success",
|
||||
"Sync across devices with AFFiNE Cloud": "Sync across devices with AFFiNE Cloud",
|
||||
"Update workspace name success": "Update workspace name success",
|
||||
@@ -246,8 +242,8 @@
|
||||
"Move folder success": "Move folder success",
|
||||
"Use on current device only": "Use on current device only",
|
||||
"Open folder hint": "Check the where the storage folder is located.",
|
||||
"Add Workspace Hint": "Select the existed database file",
|
||||
"Created Successfully": "Created Successfully",
|
||||
"Delete Workspace Label Hint": "After deleting this Workspace, you will permanently delete all of its content for everyone. No one will be able to recover the content of this Workspace",
|
||||
"Continue": "Continue",
|
||||
"Workspace database storage description": "Select where you want to create your workspace. The data of workspace is saved locally by default.",
|
||||
"Added Successfully": "Added Successfully",
|
||||
@@ -255,24 +251,34 @@
|
||||
"UNKNOWN_ERROR": "Unknown error",
|
||||
"Default Location": "Default Location",
|
||||
"Open folder": "Open folder",
|
||||
"Change workspace name hint": "Change name for all members.",
|
||||
"Move folder": "Move folder",
|
||||
"Set database location": "Set database location",
|
||||
"Change avatar hint": "Change avatar for all members.",
|
||||
"Move folder hint": "Select a new storage location.",
|
||||
"Storage Folder": "Storage Folder",
|
||||
"DB_FILE_INVALID": "Invalid Database file",
|
||||
"FILE_ALREADY_EXISTS": "File already exists",
|
||||
"Name Your Workspace": "Name Your Workspace",
|
||||
"Change avatar hint": "New avatar will be shown for everyone.",
|
||||
"Change workspace name hint": "New name will be shown for everyone.",
|
||||
"Delete Workspace Label Hint": "After deleting this Workspace, you will permanently delete all of its content for everyone. No one will be able to recover the content of this Workspace.",
|
||||
"DB_FILE_PATH_INVALID": "Database file path invalid",
|
||||
"Default db location hint": "By default will be saved to {{location}}",
|
||||
"light": "light",
|
||||
"dark": "dark",
|
||||
"system": "system",
|
||||
"com.affine.pageMode": "Page Mode",
|
||||
"com.affine.edgelessMode": "Edgeless Mode",
|
||||
"com.affine.onboarding.title1": "Hyper merged whiteboard and docs",
|
||||
"com.affine.onboarding.title2": "Intuitive & robust block-based editing",
|
||||
"com.affine.onboarding.videoDescription1": "Easily switch between Page mode for structured document creation and Whiteboard mode for the freeform visual expression of creative ideas.",
|
||||
"com.affine.onboarding.videoDescription2": "Create structured documents with ease, using a modular interface to drag and drop blocks of text, images, and other content."
|
||||
"com.affine.onboarding.videoDescription2": "Create structured documents with ease, using a modular interface to drag and drop blocks of text, images, and other content.",
|
||||
"com.affine.banner.content": "Enjoying the demo? <1>Download the AFFiNE Client</1> for the full experience.",
|
||||
"com.affine.cloudTempDisable.title": "AFFiNE Cloud is upgrading now.",
|
||||
"com.affine.cloudTempDisable.description": "We are upgrading the AFFiNE Cloud service and it is temporarily unavailable on the client side. If you wish to stay updated on the progress and be notified on availability, you can fill out the <1>AFFiNE Cloud Signup</1>.",
|
||||
"com.affine.helpIsland.gettingStarted": "Getting started",
|
||||
"FILE_ALREADY_EXISTS": "File already exists",
|
||||
"others": "Others",
|
||||
"com.affine.updater.update-available": "Update available",
|
||||
"com.affine.updater.downloading": "Downloading",
|
||||
"com.affine.updater.restart-to-update": "Restart to install update",
|
||||
"com.affine.updater.open-download-page": "Open download page",
|
||||
"dark": "Dark",
|
||||
"system": "System",
|
||||
"light": "Light"
|
||||
}
|
||||
|
||||
@@ -65,9 +65,7 @@
|
||||
"New Workspace": "Nouvel espace de travail ",
|
||||
"No item": "Aucun objet ",
|
||||
"Official Website": "Site officiel ",
|
||||
"Once deleted, you can't undo this action": {
|
||||
"": "Une fois supprimé, vous ne pourrez plus retourner en arrière."
|
||||
},
|
||||
"Once deleted, you can't undo this action": "Une fois supprimé, vous ne pourrez plus retourner en arrière.",
|
||||
"Open in new tab": "Ouvrir dans un nouvel onglet",
|
||||
"Page": "Page",
|
||||
"Paper": "Papier",
|
||||
@@ -181,6 +179,7 @@
|
||||
"recommendBrowser": "Pour une expérience optimale, nous vous recommandons le navigateur <1>Chrome</1>.",
|
||||
"upgradeBrowser": "Veuillez installer la dernière version de Chrome pour bénéficier d'une expérience optimale.",
|
||||
"core": "l'essentiel",
|
||||
"Export AFFiNE backup file": "Exporter un fichier de sauvegarde AFFiNE",
|
||||
"Download data": "Télécharger les données {{CoreOrAll}}",
|
||||
"Download all data": "Télécharger toutes les données",
|
||||
"It takes up more space on your device": "Cela prend davantage d’espace sur votre appareil.",
|
||||
@@ -188,7 +187,6 @@
|
||||
"will delete member": "supprimera le membre",
|
||||
"is a Local Workspace": "est un espace de travail local",
|
||||
"Cloud Workspace Description": "Toutes les données vont être synchronisées et sauvegardées sur le compte AFFiNE <1>{{email}}</1>",
|
||||
"Export AFFiNE backup file": "Exporter un fichier de sauvegarde AFFiNE",
|
||||
"Export Description": "Vous pouvez exporter l'intégralité des données de l'espace de travail à titre de sauvegarde ; les données ainsi exportées peuvent être réimportées.",
|
||||
"Download data Description1": "Cela prend davantage d’espace sur votre appareil.",
|
||||
"It takes up little space on your device": "Cela prend peu d’espace sur votre appareil.",
|
||||
@@ -233,5 +231,49 @@
|
||||
"You cannot delete the last workspace": "Vous ne pouvez pas supprimer le dernier Espace de travail",
|
||||
"Recent": "Récent",
|
||||
"Synced with AFFiNE Cloud": "Synchronisé avec AFFiNE Cloud",
|
||||
"Successfully deleted": "Supprimé avec succès"
|
||||
"Successfully deleted": "Supprimé avec succès",
|
||||
"Add Workspace": "Ajouter à l'espace de travail",
|
||||
"Add Workspace Hint": "Sélectionnez ce que vous avez déjà",
|
||||
"Added Successfully": "Ajouté avec succès",
|
||||
"Change avatar hint": "Changer l'avatar pour tous les membres",
|
||||
"Change workspace name hint": "Changer le nom pour tous les membres",
|
||||
"Continue": "Continuer",
|
||||
"Create your own workspace": "Créer votre propre espace de travail",
|
||||
"Created Successfully": "Créé avec succès",
|
||||
"Customize": "Customiser",
|
||||
"DB_FILE_ALREADY_LOADED": "Les fichiers de la base de donnée ont déjà été chargés",
|
||||
"DB_FILE_INVALID": "Fichier de la base de donnée invalide",
|
||||
"DB_FILE_PATH_INVALID": "Le chemin d'accès du fichier de la base de donnée est invalide",
|
||||
"Default Location": "Emplacement par défaut",
|
||||
"Default db location hint": "Par défaut, elle sera enregistrée sous {location}}",
|
||||
"Delete Workspace Label Hint": "Après la suppression de cet espace de travail, vous supprimerez de manière permanente tout le contenu de tous les utilisateurs. Personne ne pourra restaurer le contenu de cet espace de travail",
|
||||
"Export success": "Exporté avec succès",
|
||||
"Move folder": "Changer le dossier de place",
|
||||
"Move folder hint": "Sélectionnez le nouveau chemin d'accès pour le stockage ",
|
||||
"Move folder success": "Le déplacement du fichier a été réalisé avec succès",
|
||||
"Name Your Workspace": "Nommer l'espace de travail",
|
||||
"Open folder": "Ouvrir le dossier",
|
||||
"Open folder hint": "Vérifiez l'emplacement du dossier de stockage.",
|
||||
"Restart Install Client Update": "Redémarrez pour installer la mise à jour",
|
||||
"Save": "Enregistrer",
|
||||
"Set database location": "Définir l'emplacement de la base de données",
|
||||
"Storage Folder": "Dossier du stockage ",
|
||||
"Storage Folder Hint": "Vérifier ou changer l'emplacement du lieu de stockage",
|
||||
"Sync across devices with AFFiNE Cloud": "Synchroniser parmi plusieurs appareils avec AFFiNE Cloud",
|
||||
"UNKNOWN_ERROR": "Erreur inconnue",
|
||||
"Update workspace name success": "La mise à jour du nom de l'espace de travail a été faite avec succès",
|
||||
"Use on current device only": "Utiliser seulement l'appareil actuel",
|
||||
"dark": "Sombre",
|
||||
"light": "Clair",
|
||||
"system": "Système",
|
||||
"Workspace database storage description": "Sélectionnez l'endroit où vous souhaitez créer votre espace de travail. Les données de l'espace de travail sont enregistrées localement par défaut.",
|
||||
"FILE_ALREADY_EXISTS": "Le fichier existe déjà",
|
||||
"Update Available": "Mis à jour disponible",
|
||||
"com.affine.edgelessMode": "Mode sans bords",
|
||||
"com.affine.onboarding.videoDescription2": "Créez facilement des documents structurés, à l'aide d'une interface modulaire où l'on peut faire glisser et déposer des blocs de texte, des images et d'autres contenus.",
|
||||
"com.affine.onboarding.title2": "Un mode d'édition intuitif et robuste basé sur des block",
|
||||
"com.affine.onboarding.title1": "Tableau blancs et document fusionnés",
|
||||
"com.affine.onboarding.videoDescription1": "Basculez facilement entre le mode Page pour de la création de documents structurés et le mode Tableau blanc pour de l'expression visuelle libre d'idées créatives.",
|
||||
"com.affine.pageMode": "Mode page",
|
||||
"others": "Autres"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import de from './de.json';
|
||||
import en from './en.json';
|
||||
import fr from './fr.json';
|
||||
import ja from './ja.json';
|
||||
import ru from './ru.json';
|
||||
import zh_Hans from './zh-Hans.json';
|
||||
|
||||
@@ -25,7 +26,7 @@ export const LOCALES = [
|
||||
originalName: '简体中文',
|
||||
flagEmoji: '🇨🇳',
|
||||
base: false,
|
||||
completeRate: 0.7651515151515151,
|
||||
completeRate: 1,
|
||||
res: zh_Hans,
|
||||
},
|
||||
{
|
||||
@@ -35,7 +36,7 @@ export const LOCALES = [
|
||||
originalName: 'français',
|
||||
flagEmoji: '🇫🇷',
|
||||
base: false,
|
||||
completeRate: 0.8787878787878788,
|
||||
completeRate: 0.9857142857142858,
|
||||
res: fr,
|
||||
},
|
||||
{
|
||||
@@ -45,7 +46,7 @@ export const LOCALES = [
|
||||
originalName: 'Deutsch',
|
||||
flagEmoji: '🇩🇪',
|
||||
base: false,
|
||||
completeRate: 0.8787878787878788,
|
||||
completeRate: 0.9857142857142858,
|
||||
res: de,
|
||||
},
|
||||
{
|
||||
@@ -55,7 +56,17 @@ export const LOCALES = [
|
||||
originalName: 'русский',
|
||||
flagEmoji: '🇷🇺',
|
||||
base: false,
|
||||
completeRate: 0.7348484848484849,
|
||||
completeRate: 0.6928571428571428,
|
||||
res: ru,
|
||||
},
|
||||
{
|
||||
id: 1000040014,
|
||||
name: 'Japanese',
|
||||
tag: 'ja',
|
||||
originalName: '日本語',
|
||||
flagEmoji: '🇯🇵',
|
||||
base: false,
|
||||
completeRate: 1.0107142857142857,
|
||||
res: ja,
|
||||
},
|
||||
] as const;
|
||||
|
||||
286
packages/i18n/src/resources/ja.json
Normal file
286
packages/i18n/src/resources/ja.json
Normal file
@@ -0,0 +1,286 @@
|
||||
{
|
||||
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.": "",
|
||||
"404 - Page Not Found": "404 - ページが見つかりません",
|
||||
"AFFiNE Community": "AFFiNEコミュニティ",
|
||||
"Add to favorites": "お気に入りに追加する",
|
||||
"All pages": "すべてのページ",
|
||||
"Available Offline": "オフラインで利用可能",
|
||||
"Back Home": "ホームに戻る",
|
||||
"Bold": "太字",
|
||||
"Back to Quick Search": "クイック検索に戻る",
|
||||
"ClearData": "ローカルにあるデータをクリアする",
|
||||
"AFFiNE Cloud": "AFFiNEクラウド",
|
||||
"Add to Favorites": "お気に入りに追加",
|
||||
"Added to Favorites": "お気に入りに追加されました",
|
||||
"Added Successfully": "正常に追加されました",
|
||||
"All changes are saved locally": "すべての変更はローカルに保存されます",
|
||||
"All data has been stored in the cloud": "すべてのデータはクラウドに保存されています",
|
||||
"Cancel": "キャンセル",
|
||||
"Body text": "本文テキスト",
|
||||
"Cloud Workspace": "クラウドワークスペース",
|
||||
"Code block": "コードブロック",
|
||||
"Confirm": "確認",
|
||||
"Contact Us": "お問い合わせ",
|
||||
"Connector": "コネクター(近日公開)",
|
||||
"Copied link to clipboard": "リンクをクリップボードにコピーしました",
|
||||
"Copy Link": "リンクをコピー",
|
||||
"Create": "作成",
|
||||
"Create Or Import": "作成またはインポート",
|
||||
"Created Successfully": "正常に作成されました",
|
||||
"Create your own workspace": "独自のワークスペースを作成する",
|
||||
"Create Shared Link Description": "誰とでも簡単に共有できるリンクを作成します",
|
||||
"Customize": "カスタマイズ",
|
||||
"DB_FILE_INVALID": "無効なデータベースファイル",
|
||||
"DB_FILE_ALREADY_LOADED": "データベースファイルはすでにロードされています",
|
||||
"DB_FILE_PATH_INVALID": "データベースファイルのパスが無効です",
|
||||
"Data sync mode": "データ同期モード",
|
||||
"Default db location hint": "デフォルトでは {{location}} に保存されます",
|
||||
"Default Location": "デフォルトの場所",
|
||||
"Delete": "削除",
|
||||
"Delete Member?": "メンバーを削除しますか?",
|
||||
"Delete Workspace": "ワークスペースの削除",
|
||||
"Delete Workspace placeholder": "確認のため、「Delete」と入力してください",
|
||||
"Delete Workspace Label Hint": "このワークスペースを削除すると、すべてのユーザーのコンテンツが永久に削除されます。このワークスペースのコンテンツを復元することはできません",
|
||||
"Delete page?": "ページを削除しますか?",
|
||||
"Delete permanently": "完全に削除",
|
||||
"Delete permanently?": "完全に削除しますか?",
|
||||
"Disable": "無効",
|
||||
"Disable Public Link": "パブリックリンクを無効にする",
|
||||
"Disable Public Link ?": "パブリックリンクを無効にしますか?",
|
||||
"Disable Public Link Description": "このパブリック リンクを無効にすると、リンクを知っている誰もがこのページにアクセスできなくなります",
|
||||
"Disable Public Sharing": "パブリック共有を無効にする",
|
||||
"Download all data": "すべてのデータをダウンロードする",
|
||||
"Edit": "編集",
|
||||
"Edgeless": "エッジレス",
|
||||
"Download data Description1": "端末の容量を大きく占有します",
|
||||
"Download data Description2": "端末の容量をあまり占有しません",
|
||||
"Enable": "有効",
|
||||
"Enable AFFiNE Cloud": "AFFiNEクラウドを有効にする",
|
||||
"Enabled success": "有効化に成功",
|
||||
"Enable AFFiNE Cloud Description": "有効にすると、このワークスペース内のデータがAFFiNEクラウド経由でバックアップおよび同期されます",
|
||||
"Expand sidebar": "サイドバーを展開",
|
||||
"Export": "エクスポート",
|
||||
"Export Description": "ワークスペースにあるデータ全体をバックアップ用にエクスポートしたり、エクスポートしたデータを再インポートしたりできます",
|
||||
"Export success": "エクスポートに成功",
|
||||
"Export Shared Pages Description": "ページの静的コピーをダウンロードして、他の人と共有することができます",
|
||||
"Export to HTML": "HTMLにエクスポート",
|
||||
"Export to Markdown": "Markdownにエクスポート",
|
||||
"Created": "作成日",
|
||||
"FILE_ALREADY_EXISTS": "ファイルが既に存在します",
|
||||
"Failed to publish workspace": "ワークスペースの公開に失敗しました",
|
||||
"Favorite": "お気に入り",
|
||||
"Favorite pages for easy access": "お気に入りのページへ簡単にアクセス",
|
||||
"Favorited": "お気に入りに追加しました",
|
||||
"Favorites": "お気に入り",
|
||||
"Find 0 result": "検索結果 0 件",
|
||||
"Find results": "検索結果 {{number}} 件",
|
||||
"Finding Workspace ID": "ワークスペースIDの検索",
|
||||
"Finding Current Workspace": "現在のワークスペースを検索",
|
||||
"Force Sign Out": "強制サインアウト",
|
||||
"General": "一般",
|
||||
"Got it": "了解",
|
||||
"Get in touch!": "ご連絡ください!",
|
||||
"Get in touch! Join our communities": "連絡してください! コミュニティに参加しましょう",
|
||||
"Help and Feedback": "ヘルプとフィードバック",
|
||||
"Import": "インポート",
|
||||
"How is AFFiNE Alpha different?": "AFFiNE Alphaはどう違うのですか?",
|
||||
"Inline code": "インラインコード",
|
||||
"Increase indent": "インデントを増やす",
|
||||
"Invite": "招待",
|
||||
"Invite Members": "メンバーを招待",
|
||||
"Invite placeholder": "メールの検索(Gmailのみサポート)",
|
||||
"Loading Page": "ページ読み込み中",
|
||||
"Loading": "読み込み中...",
|
||||
"Leave Workspace": "ワークスペースから退出",
|
||||
"Leave": "退出",
|
||||
"Italic": "斜体",
|
||||
"It takes up little space on your device": "端末の容量をあまり占有しません",
|
||||
"It takes up little space on your device.": "端末の容量をあまり占有しません",
|
||||
"It takes up more space on your device": "端末の容量を大きく占有します",
|
||||
"It takes up more space on your device.": "端末の容量を大きく占有します",
|
||||
"Joined Workspace": "ワークスペースに参加",
|
||||
"Keyboard Shortcuts": "キーボードショートカット",
|
||||
"Leave Workspace Description": "退会すると、このワークスペースのコンテンツにアクセスできなくなります",
|
||||
"Loading All Workspaces": "すべてのワークスペースを読み込み中",
|
||||
"Local Workspace": "ローカルワークスペース",
|
||||
"Link": "ハイパーリンク (選択したテキストを含む)",
|
||||
"Member": "メンバー",
|
||||
"Members": "メンバー",
|
||||
"Move folder": "フォルダを移動",
|
||||
"Move to Trash": "ゴミ箱に移動",
|
||||
"My Workspaces": "マイワークスペース",
|
||||
"Move folder success": "フォルダの移動に成功",
|
||||
"Move folder hint": "新しい保存場所を選択",
|
||||
"New Page": "新規ページ",
|
||||
"Navigation Path": "ナビゲーションパス",
|
||||
"Name Your Workspace": "ワークスペースに名前をつける",
|
||||
"Ooops!": "しまった!",
|
||||
"Open Workspace Settings": "ワークスペース設定を開く",
|
||||
"Open folder": "フォルダを開く",
|
||||
"New Workspace": "新規ワークスペース",
|
||||
"No item": "項目なし",
|
||||
"Non-Gmail": "Gmail以外はサポートされていません",
|
||||
"NotLoggedIn": "現在ログインしていません",
|
||||
"Official Website": "公式サイト",
|
||||
"Once deleted, you can't undo this action": "削除すると、この操作を元に戻すことはできません",
|
||||
"Once deleted, you can't undo this action.": "一度削除すると、この操作を元に戻すことはできません",
|
||||
"Markdown Syntax": "Markdown構文",
|
||||
"Moved to Trash": "ゴミ箱に移動した",
|
||||
"Page": "ページ",
|
||||
"Owner": "オーナー",
|
||||
"Organize pages to build knowledge": "ページを整理して知識を深める",
|
||||
"Open in new tab": "新しいタブで開く",
|
||||
"Open folder hint": "保存フォルダがどこにあるか確認してください",
|
||||
"Add Workspace": "ワークスペースの追加",
|
||||
"Add a subpage inside": "内部にサブページを追加",
|
||||
"Add Workspace Hint": "すでに持っている場所を選択",
|
||||
"Access level": "アクセスレベル",
|
||||
"Continue with Google": "Googleでログイン",
|
||||
"Continue": "続行",
|
||||
"Pending": "保留中",
|
||||
"Paper": "用紙",
|
||||
"Page is Loading": "ページを読み込んでいます",
|
||||
"Pen": "ペン(近日公開)",
|
||||
"Publish": "公開",
|
||||
"Publish to web": "Webに公開",
|
||||
"Published to Web": "Webに公開しました",
|
||||
"Published Description": "現在のワークスペースはWebに公開されており、誰でもリンクを通じてこのワークスペースの内容を表示できます",
|
||||
"Please make sure you are online": "オンラインであることを確認してください",
|
||||
"Permanently deleted": "完全に削除されました",
|
||||
"Pivots": "ピボット",
|
||||
"Placeholder of delete workspace": "確認のためにワークスペース名を入力してください",
|
||||
"Change avatar hint": "メンバー全員のアバターを変更",
|
||||
"Change workspace name hint": "メンバー全員の名前を変更",
|
||||
"Collaboration": "コラボレーション",
|
||||
"Recent": "最近",
|
||||
"Redo": "やり直し",
|
||||
"RFP": "ピボットのページは自由に追加・削除することができ、「すべてのページ」からもアクセスできます",
|
||||
"Quick search placeholder": "クイック検索...",
|
||||
"Quick search": "クイック検索",
|
||||
"Save": "保存",
|
||||
"Select": "選択",
|
||||
"Restore it": "復元",
|
||||
"Restart Install Client Update": "再起動してアップデートをインストールする",
|
||||
"Rename": "名前の変更",
|
||||
"Settings": "設定",
|
||||
"Set up an AFFiNE account to sync data": "データを同期するためにAFFiNEアカウントを設定する",
|
||||
"Sync": "同期",
|
||||
"Not now": "あとで登録する",
|
||||
"Check Our Docs": "ドキュメントを確認しよう",
|
||||
"Collapse sidebar": "サイドバーを折りたたむ",
|
||||
"Download core data": "主要なデータのダウンロード",
|
||||
"Jump to": "ジャンプ先",
|
||||
"Download data": "{{CoreOrAll}} データのダウンロード",
|
||||
"Router is Loading": "ルーターを読み込み中",
|
||||
"Heading": "見出し {{number}}",
|
||||
"Member has been removed": "{{name}}は削除されました",
|
||||
"Collaboration Description": "他のメンバーとコラボレーションするにはAFFiNEクラウドサービスが必要です",
|
||||
"Move page to": "ページの移動...",
|
||||
"Move page to...": "ページの移動...",
|
||||
"Move to": "移動先",
|
||||
"Sign out": "サインアウト",
|
||||
"Sign in and Enable": "サインインして有効化",
|
||||
"Shortcuts": "ショートカット",
|
||||
"Share with link": "リンクを共有",
|
||||
"Shared Pages": "共有ページ",
|
||||
"Set a Workspace name": "ワークスペース名を設定",
|
||||
"Set database location": "データベースの場所を設定",
|
||||
"Retain local cached data": "ローカルにキャッシュされたデータを保持",
|
||||
"Saved then enable AFFiNE Cloud": "すべての変更はローカルに保存されます。クリックしてAFFiNEクラウドを有効にします。",
|
||||
"Remove from workspace": "ワークスペースから削除",
|
||||
"Remove from favorites": "お気に入りから削除",
|
||||
"Removed from Favorites": "お気に入りから削除しました",
|
||||
"Retain cached cloud data": "ローカルにキャッシュされたデータを保持",
|
||||
"Remove from Pivots": "ピボットの削除",
|
||||
"Reduce indent": "インデントを減らす",
|
||||
"Publishing": "Webへの公開はAFFiNEクラウドサービスが必要です",
|
||||
"Publishing Description": "Web に公開すると、誰もがリンクを通じてこのワークスペースのコンテンツを表示できるようになります",
|
||||
"Quick search placeholder2": "{{workspace}} を検索中",
|
||||
"Skip": "スキップ",
|
||||
"Shape": "シェイプ",
|
||||
"Share Menu Public Workspace Description1": "ワークスペースへ他の人を招待したり、Webに公開することができます",
|
||||
"Shared Pages Description": "ページの公開にはAFFiNEクラウドサービスが必要です",
|
||||
"Share Menu Public Workspace Description2": "現在のワークスペースは、公開ワークスペースとしてWebに公開されています",
|
||||
"Undo": "元に戻す",
|
||||
"Untitled": "無題",
|
||||
"Underline": "下線",
|
||||
"UNKNOWN_ERROR": "未知のエラー",
|
||||
"Trash": "ゴミ箱",
|
||||
"Title": "タイトル",
|
||||
"Tips": "ヒント:",
|
||||
"TrashButtonGroupTitle": "完全に削除",
|
||||
"Synced with AFFiNE Cloud": "AFFiNEクラウドと同期しています",
|
||||
"Sync across devices with AFFiNE Cloud": "AFFiNEクラウドでデバイス間を同期する",
|
||||
"Stop publishing": "公開をやめる",
|
||||
"Storage Folder": "ストレージフォルダー",
|
||||
"Storage Folder Hint": "保存場所を確認または変更する",
|
||||
"Upload": "アップロード",
|
||||
"Update Available": "アップデート可能",
|
||||
"Update workspace name success": "ワークスペース名の更新に成功",
|
||||
"Successfully deleted": "正常に削除されました",
|
||||
"Text": "テキスト(近日公開)",
|
||||
"New Keyword Page": "新しい「{{query}}」ページ",
|
||||
"Convert to ": "変換する",
|
||||
"Updated": "更新日",
|
||||
"TrashButtonGroupDescription": "一度削除すると、この操作を元に戻すことはできません。よろしいですか?",
|
||||
"Sign out description": "サインアウトすると、同期していないコンテンツは失われます",
|
||||
"Shared Pages In Public Workspace Description": "ワークスペース全体はWeb上で公開され、<1>ワークスペース設定</1>で編集することができます",
|
||||
"Users": "ユーザー",
|
||||
"Stay logged out": "ログアウト状態を維持する",
|
||||
"Sticky": "付箋(近日公開)",
|
||||
"Workspace Avatar": "ワークスペースのアバター",
|
||||
"Workspace Icon": "ワークスペースのアイコン",
|
||||
"Workspace Name": "ワークスペース名",
|
||||
"Workspace Not Found": "ワークスペースが見つかりません",
|
||||
"View Navigation Path": "ナビゲーションパスを表示",
|
||||
"Use on current device only": "現在の端末でのみ使用",
|
||||
"Workspace Owner": "ワークスペースの所有者",
|
||||
"Workspace Settings": "ワークスペースの設定",
|
||||
"Workspace Type": "ワークスペースのタイプ",
|
||||
"all": "すべて",
|
||||
"You cannot delete the last workspace": "最後のワークスペースを削除することはできません",
|
||||
"Workspace database storage description": "ワークスペースを作成する場所を選択します。ワークスペースのデータは、デフォルトでローカルに保存されます",
|
||||
"Workspace description": "ワークスペースは、一人で、あるいはチームで、創造し、計画するための仮想空間です",
|
||||
"core": "主要な",
|
||||
"com.affine.pageMode": "ページモード",
|
||||
"com.affine.edgelessMode": "エッジレスモード",
|
||||
"com.affine.onboarding.title1": "ハイパーマージされたホワイトボードとドキュメント",
|
||||
"com.affine.onboarding.title2": "直感的で堅牢なブロックベースの編集",
|
||||
"com.affine.onboarding.videoDescription1": "構造化されたドキュメントを作成するためのページモードと、創造的なアイデアを自由な形式で視覚的に表現するためのホワイトボードモードを簡単に切り替えることができます",
|
||||
"com.affine.onboarding.videoDescription2": "モジュール式インターフェイスを使用してテキスト、画像、その他のコンテンツのブロックをドラッグ&ドロップすることで、構造化ドキュメントを簡単に作成できます",
|
||||
"com.affine.cloudTempDisable.title": "AFFiNEクラウドは現在アップグレード中です。",
|
||||
"com.affine.cloudTempDisable.description": "AFFiNEクラウドサービスのバージョンアップを行っており、クライアント側で一時的に利用できない状態になっています。進捗状況などに関する通知をご希望の方は<1>AFFiNEコミュニティ</1>にご参加ください。",
|
||||
"com.affine.banner.content": "デモをお楽しみですか?完全なエクスペリエンスを得るには<1>AFFiNEクライアントをダウンロード</1>してください。",
|
||||
"dark": "ダーク",
|
||||
"light": "ライト",
|
||||
"emptyAllPages": "このワークスペースに何もありません。新しいページを作成して編集を開始します",
|
||||
"emptyFavorite": "「お気に入りに追加」をクリックすると、ここにページが表示されます",
|
||||
"emptySharedPages": "共有されたページがここに表示されます",
|
||||
"emptyTrash": "「ゴミ箱に移動」をクリックすると、ここにページが表示されます",
|
||||
"is a Cloud Workspace": "はクラウドワークスペースです",
|
||||
"is a Local Workspace": "はローカルワークスペースです",
|
||||
"login success": "ログイン成功",
|
||||
"others": "その他",
|
||||
"mobile device": "モバイル端末で閲覧しているようです",
|
||||
"mobile device description": "モバイルへの対応は現在も進めており、デスクトップ端末でのご利用を推奨しています",
|
||||
"recommendBrowser": "最適な環境でご利用いただくために、<1>Chrome</1>ブラウザを推奨します",
|
||||
"system": "システム",
|
||||
"will delete member": "はメンバーを削除します",
|
||||
"upgradeBrowser": "快適にご利用いただくために、Chromeの最新バージョンへのアップグレードをお願いします",
|
||||
"restored": "{{title}}を復元しました",
|
||||
"still designed": "(このページはまだ設計中です)",
|
||||
"will be moved to Trash": "{{title}}はゴミ箱に移動されます",
|
||||
"Wait for Sync": "同期を待つ",
|
||||
"Strikethrough": "打ち消し線",
|
||||
"Cloud Workspace Description": "すべてのデータは、AFFiNEアカウント<1>{{email}}</1>に同期して保存されます",
|
||||
"Delete Workspace Description": "削除 (<1>{{workspace}}</1>) は元に戻すことができません。慎重に続行してください。すべての内容が失われます",
|
||||
"Delete Workspace Description2": "(<1>{{workspace}}</1>)を削除すると、ローカルデータとクラウドデータの両方が削除されます。この操作は元に戻すことができません。注意して続行してください",
|
||||
"Export Workspace": "ワークスペースのエクスポート <1>{{workspace}}</1>は近日公開予定です",
|
||||
"Sync Description": "{{workspaceName}}はローカルワークスペースです。すべてのデータは現在のデバイスに保存されます。このワークスペースに対してAFFiNEクラウドを有効にして、データをクラウドと同期させることができます",
|
||||
"Sync Description2": "<1>{{workspaceName}}</1>はクラウドワークスペースです。すべてのデータは同期され、AFFiNEクラウドに保存されます",
|
||||
"Divider": "区分線",
|
||||
"Export AFFiNE backup file": "AFFiNEバックアップファイルのエクスポート",
|
||||
"Sign in": "AFFiNEクラウドにサインイン",
|
||||
"Local Workspace Description": "すべてのデータは現在のデバイスに保存されます。このワークスペースのAFFiNEクラウドを有効にすると、クラウドとデータを同期しておくことができます",
|
||||
"Discover what's new!": "新着情報を見る"
|
||||
}
|
||||
@@ -6,19 +6,19 @@
|
||||
"will delete member": "将删除成员",
|
||||
"Download data Description1": "此操作会在你的设备上占用更多空间。",
|
||||
"Download data Description2": "此操作会在你的设备上占用少许空间。",
|
||||
"It takes up more space on your device": "此操作会在你的设备上占用更多空间。",
|
||||
"It takes up more space on your device": "它会在你的设备上占用更多空间。",
|
||||
"Help and Feedback": "帮助与反馈",
|
||||
"Remove from workspace": "从工作区移除",
|
||||
"Retain cached cloud data": "保留缓存的云数据",
|
||||
"Workspace Owner": "工作区所有者",
|
||||
"Cloud Workspace": "云端工作区",
|
||||
"Cloud Workspace Description": "所有数据将被同步并保存在AFFiNE账户(<1>{{email}}</1>)中",
|
||||
"Copied link to clipboard": "复制链接到剪贴板",
|
||||
"Available Offline": "可供离线使用",
|
||||
"Back Home": "返回首页",
|
||||
"Enabled success": "启用成功",
|
||||
"Published Description": "当前工作区已被发布到Web,所有人都可以通过链接来查看此工作区内容。",
|
||||
"All data has been stored in the cloud": "所有数据已被保存在云端。",
|
||||
"Cloud Workspace Description": "所有数据将被同步并保存在 AFFiNE 账户(<1>{{email}}</1>)中",
|
||||
"Published Description": "当前工作区已被发布到 Web,所有人都可以通过链接来查看此工作区内容。",
|
||||
"Download data": "下载 {{CoreOrAll}} 数据",
|
||||
"Force Sign Out": "强制登出",
|
||||
"Joined Workspace": "加入工作区",
|
||||
@@ -73,18 +73,15 @@
|
||||
"Access level": "访问权限",
|
||||
"Added to Favorites": "已收藏",
|
||||
"Collaboration": "协作",
|
||||
"Collaboration Description": "与其他成员协作需要AFFiNE云服务支持。",
|
||||
"Continue with Google": "谷歌登录以继续",
|
||||
"Delete page?": "确定要删除页面?",
|
||||
"Delete permanently": "永久删除",
|
||||
"Delete permanently?": "是否永久删除?",
|
||||
"Edgeless": "无界",
|
||||
"Enable AFFiNE Cloud": "启用 AFFiNE 云服务",
|
||||
"Enable AFFiNE Cloud Description": "如启用,此工作区中的数据将通过AFFiNE Cloud进行备份和同步。",
|
||||
"Favorites": "收藏夹",
|
||||
"Get in touch!": "保持联络!",
|
||||
"Got it": "知道了",
|
||||
"How is AFFiNE Alpha different?": "AFFiNE Alpha有何不同?",
|
||||
"Invite": "邀请",
|
||||
"Invite Members": "邀请成员",
|
||||
"Invite placeholder": "搜索邮件(仅支持Gmail)",
|
||||
@@ -94,17 +91,22 @@
|
||||
"Leave Workspace": "退出工作区",
|
||||
"Leave Workspace Description": "退出后,您将无法再访问此工作区的内容。",
|
||||
"Link": "超链接(选定文本)",
|
||||
"Collaboration Description": "与其他成员协作需要 AFFiNE 云服务支持。",
|
||||
"Moved to Trash": "已移到垃圾箱",
|
||||
"My Workspaces": "我的工作区",
|
||||
"Enable AFFiNE Cloud Description": "如启用,此工作区中的数据将通过 AFFiNE Cloud 进行备份和同步。",
|
||||
"How is AFFiNE Alpha different?": "AFFiNE Alpha 有何不同?",
|
||||
"Non-Gmail": "不支持非 Gmail 邮箱",
|
||||
"Publishing": "发布到 web 需要 AFFiNE 云服务。",
|
||||
"New Page": "新建页面",
|
||||
"New Workspace": "新建工作区",
|
||||
"No item": "无项目",
|
||||
"Non-Gmail": "不支持非Gmail邮箱",
|
||||
"NotLoggedIn": "当前未登录",
|
||||
"Official Website": "官网",
|
||||
"Once deleted, you can't undo this action": "一旦删除,将无法撤销!",
|
||||
"Once deleted, you can't undo this action": "一旦删除,您将无法撤销此操作。",
|
||||
"Ooops!": "啊哦!",
|
||||
"Created": "已创建",
|
||||
"Publishing Description": "发布到 web 后,所有人都可以通过链接查看此工作区的内容。",
|
||||
"Created": "创建时间",
|
||||
"Delete Workspace Description": "正在删除 (<1>{{workspace}}</1>) ,此操作无法撤销,所有内容将会丢失。",
|
||||
"Export Workspace": "导出工作区 <1>{{workspace}}</1> 即将上线",
|
||||
"Find 0 result": "找到 0 个结果",
|
||||
@@ -115,9 +117,7 @@
|
||||
"Pending": "待定",
|
||||
"Permanently deleted": "已永久删除",
|
||||
"Publish": "发布",
|
||||
"Publishing": "发布到web需要AFFiNE云服务。",
|
||||
"Publish to web": "发布到web",
|
||||
"Publishing Description": "发布到web后,所有人都可以通过链接查看此工作区的内容。",
|
||||
"Quick search": "快速搜索",
|
||||
"Quick search placeholder": "快速搜索...",
|
||||
"Quick search placeholder2": "在{{workspace}} 中搜索",
|
||||
@@ -169,20 +169,19 @@
|
||||
"Restore it": "恢复TA",
|
||||
"all": "全部",
|
||||
"core": "核心",
|
||||
"Sync Description": "{{workspaceName}}是本地工作区,所有数据都存储在当前设备上。您可以为此工作区启用AFFiNE Cloud,以使数据与云端保持同步。",
|
||||
"Delete Workspace Description2": "正在删除(<1>{{workspace}}</1>),将同时删除本地和云端数据。此操作无法撤消,请谨慎操作。",
|
||||
"Sync Description": "{{workspaceName}}是本地工作区,所有数据都存储在当前设备上。您可以为此工作区启用 AFFiNE Cloud,以使数据与云端保持同步。",
|
||||
"Failed to publish workspace": "工作区发布失败",
|
||||
"Local Workspace Description": "所有数据都本地存储在当前设备。您可以为此工作区启用AFFiNE Cloud,以保证数据时刻被云端同步。",
|
||||
"Member": "成员",
|
||||
"Member has been removed": "{{name}} 已被移除。",
|
||||
"Owner": "所有者",
|
||||
"Published to Web": "公开到互联网",
|
||||
"Data sync mode": "数据同步模式",
|
||||
"Export AFFiNE backup file": "导出 AFFiNE 备份文件",
|
||||
"AFFiNE Cloud": "AFFiNE 云服务",
|
||||
"Export Description": "您可以导出整个工作区数据进行备份,导出的数据可以重新被导入。",
|
||||
"It takes up little space on your device": "此操作会在你的设备上占用少许空间。",
|
||||
"It takes up little space on your device": "它会在你的设备上占用少许空间。",
|
||||
"Add to favorites": "加入收藏",
|
||||
"Export AFFiNE backup file": "导出 AFFiNE 备份文件",
|
||||
"AFFiNE Cloud": "AFFiNE Cloud",
|
||||
"Retain local cached data": "保留本地缓存数据",
|
||||
"Set a Workspace name": "设置工作区名字",
|
||||
"Workspace Avatar": "工作区头像",
|
||||
@@ -194,12 +193,91 @@
|
||||
"Move to Trash": "移到垃圾箱",
|
||||
"Add a subpage inside": "添加一个子页面",
|
||||
"Discover what's new!": "发现最近更新!",
|
||||
"Get in touch! Join our communities": "加入社区,保持联络!",
|
||||
"Move page to": "将此页面移动...",
|
||||
"Get in touch! Join our communities": "保持联系!加入我们的社区。",
|
||||
"Move page to": "将此页面移动到...",
|
||||
"Move to": "移动到",
|
||||
"Pivots": "枢纽",
|
||||
"Placeholder of delete workspace": "请输入工作区名字以确认",
|
||||
"RFP": "页面可以从枢纽上被自由添加或删除,但仍然可以在“所有页面”中访问。",
|
||||
"Remove from Pivots": "从枢纽中删除",
|
||||
"Rename": "重命名"
|
||||
"Rename": "重命名",
|
||||
"Local Workspace Description": "所有数据都本地存储在当前设备。您可以为此工作区启用 AFFiNE Cloud,以保证数据时刻被云端同步。",
|
||||
"Add Workspace Hint": "请选择已有的数据库文件",
|
||||
"Back to Quick Search": "返回快速搜索",
|
||||
"Add Workspace": "导入工作区",
|
||||
"Added Successfully": "导入成功",
|
||||
"Change avatar hint": "新的头像将对所有人显示。",
|
||||
"Change workspace name hint": "新的名称将对所有人显示。",
|
||||
"Continue": "继续",
|
||||
"Create Shared Link Description": "创建一个可以轻松分享给任何人的链接",
|
||||
"Create your own workspace": "创建属于你的工作区",
|
||||
"Created Successfully": "创建成功",
|
||||
"Customize": "自定义",
|
||||
"DB_FILE_ALREADY_LOADED": "数据库文件已加载",
|
||||
"DB_FILE_INVALID": "无效的数据库文件",
|
||||
"DB_FILE_PATH_INVALID": "数据库文件路径无效",
|
||||
"Default Location": "默认位置",
|
||||
"Default db location hint": "默认情况下将保存到 {{location}}",
|
||||
"Delete Workspace Label Hint": "在删除此工作区后,您将永久删除所有内容,任何人都无法恢复此工作区的内容。",
|
||||
"Disable": "禁用",
|
||||
"Disable Public Link": "禁用公共链接",
|
||||
"Disable Public Link ?": "禁用公共链接 ?",
|
||||
"Disable Public Link Description": "禁用此公共链接将阻止任何拥有此链接的人访问此页面。",
|
||||
"Disable Public Sharing": "禁用公开分享",
|
||||
"Export Shared Pages Description": "下载页面的静态副本以与他人分享。",
|
||||
"Export success": "导出成功",
|
||||
"FILE_ALREADY_EXISTS": "文件已存在",
|
||||
"Favorite pages for easy access": "将页面添加到收藏夹以便轻松访问",
|
||||
"Finding Current Workspace": "正在查找当前工作区",
|
||||
"Finding Workspace ID": "正在查找当前工作区 ID",
|
||||
"Loading All Workspaces": "正在读取全部工作区",
|
||||
"Loading Page": "正在读取页面",
|
||||
"Move folder": "移动文件夹",
|
||||
"Move folder hint": "选择新的存储位置",
|
||||
"Move folder success": "移动文件夹成功",
|
||||
"Name Your Workspace": "给您的工作区命名",
|
||||
"Navigation Path": "导航路径",
|
||||
"Open Workspace Settings": "打开工作区设置",
|
||||
"Open folder": "打开文件夹",
|
||||
"Open folder hint": "检查存储文件夹的位置。",
|
||||
"Organize pages to build knowledge": "组织页面以建立知识库。",
|
||||
"Page is Loading": "页面正在读取中",
|
||||
"Recent": "最近",
|
||||
"Restart Install Client Update": "重启以安装更新",
|
||||
"Router is Loading": "路由正在加载",
|
||||
"Save": "保存",
|
||||
"Set database location": "设置数据库位置",
|
||||
"Share Menu Public Workspace Description1": "邀请其他人加入工作区或将其发布到网络。",
|
||||
"Share Menu Public Workspace Description2": "当前工作区已被发布到网络作为公共工作区。",
|
||||
"Shared Pages": "已分享页面",
|
||||
"Shared Pages Description": "公开分享页面需要 AFFiNE Cloud 服务。",
|
||||
"Shared Pages In Public Workspace Description": "整个工作区已在网络上发布,可以通过<1>工作区设置</1>进行编辑。",
|
||||
"Storage Folder": "存储文件夹",
|
||||
"Storage Folder Hint": "检查或更改存储位置。",
|
||||
"Successfully deleted": "成功删除。",
|
||||
"Sync across devices with AFFiNE Cloud": "使用 AFFiNE Cloud 在多个设备间进行同步",
|
||||
"Synced with AFFiNE Cloud": "AFFiNE Cloud 同步完成",
|
||||
"UNKNOWN_ERROR": "未知错误",
|
||||
"Update Available": "有可用的更新",
|
||||
"Update workspace name success": "成功更新工作区名称",
|
||||
"Use on current device only": "仅在当前设备上使用",
|
||||
"View Navigation Path": "查看导航路径",
|
||||
"Workspace Not Found": "未找到工作区",
|
||||
"Workspace database storage description": "选择您要创建工作区的位置。工作区的数据默认情况下会保存在本地。",
|
||||
"You cannot delete the last workspace": "您不能删除最后一个工作区",
|
||||
"com.affine.banner.content": "正在享受演示吗?<1>下载 AFFiNE 客户端</1>以获得完整体验。",
|
||||
"com.affine.cloudTempDisable.title": "AFFiNE Cloud 正在进行升级。",
|
||||
"com.affine.cloudTempDisable.description": "我们正在升级 AFFiNE Cloud 服务,客户端暂时不可启用它。如果您希望随时了解进度并收到关于云服务的可用性通知,您可以填写我们的<1>表单</1>。",
|
||||
"com.affine.edgelessMode": "无界模式",
|
||||
"com.affine.helpIsland.gettingStarted": "开始使用",
|
||||
"com.affine.onboarding.title1": "白板和文档的超融合",
|
||||
"com.affine.onboarding.title2": "直观且强大的块级编辑",
|
||||
"com.affine.onboarding.videoDescription1": "在页面模式和白板模式之间轻松切换,你可以在页面模式下创建结构化文档,并在白板模式下自由表达创意思想。",
|
||||
"com.affine.onboarding.videoDescription2": "轻松创建结构化文档,使用模块化界面将文本块、图像和其他内容拖放到页面中。",
|
||||
"com.affine.pageMode": "页面模式",
|
||||
"emptySharedPages": "共享的页面将显示在此处。",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"others": "其他",
|
||||
"system": "跟随系统"
|
||||
}
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"@blocksuite/store": "*",
|
||||
"lottie-web": "*"
|
||||
},
|
||||
"version": "0.6.0-canary.2"
|
||||
"version": "0.6.0-canary.3"
|
||||
}
|
||||
|
||||
@@ -17,10 +17,11 @@ napi = { version = "2", default-features = false, features = [
|
||||
] }
|
||||
napi-derive = "2"
|
||||
notify = { version = "5", features = ["serde"] }
|
||||
once_cell = "1"
|
||||
parking_lot = "0.12"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
tokio = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
uuid = { version = "1", default-features = false, features = [
|
||||
"serde",
|
||||
"v4",
|
||||
|
||||
3
packages/native/fs-watcher.d.ts
vendored
Normal file
3
packages/native/fs-watcher.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { FsWatcher } from './index';
|
||||
|
||||
export function createFSWatcher(): typeof FsWatcher;
|
||||
6
packages/native/fs-watcher.js
Normal file
6
packages/native/fs-watcher.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports.createFSWatcher = function createFSWatcher() {
|
||||
// require it in the function level so that it won't break the `generate-main-exposed-meta.mjs`
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { FsWatcher } = require('./index');
|
||||
return FsWatcher;
|
||||
};
|
||||
11
packages/native/index.d.ts
vendored
11
packages/native/index.d.ts
vendored
@@ -22,21 +22,20 @@ export const enum WatcherKind {
|
||||
NullWatcher = 'NullWatcher',
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
export function watch(
|
||||
p: string,
|
||||
options?: WatchOptions | undefined | null
|
||||
): FSWatcher;
|
||||
export function moveFile(src: string, dst: string): Promise<void>;
|
||||
export class Subscription {
|
||||
toString(): string;
|
||||
unsubscribe(): void;
|
||||
}
|
||||
export type FSWatcher = FsWatcher;
|
||||
export class FsWatcher {
|
||||
get kind(): WatcherKind;
|
||||
static watch(p: string, options?: WatchOptions | undefined | null): FsWatcher;
|
||||
static kind(): WatcherKind;
|
||||
toString(): string;
|
||||
subscribe(
|
||||
callback: (event: import('./event').NotifyEvent) => void,
|
||||
errorCallback?: (err: Error) => void
|
||||
): Subscription;
|
||||
close(): void;
|
||||
static unwatch(p: string): void;
|
||||
static close(): void;
|
||||
}
|
||||
|
||||
@@ -263,9 +263,9 @@ if (!nativeBinding) {
|
||||
throw new Error(`Failed to load native binding`);
|
||||
}
|
||||
|
||||
const { WatcherKind, Subscription, watch, FsWatcher } = nativeBinding;
|
||||
const { WatcherKind, Subscription, FsWatcher, moveFile } = nativeBinding;
|
||||
|
||||
module.exports.WatcherKind = WatcherKind;
|
||||
module.exports.Subscription = Subscription;
|
||||
module.exports.watch = watch;
|
||||
module.exports.FsWatcher = FsWatcher;
|
||||
module.exports.moveFile = moveFile;
|
||||
|
||||
@@ -35,5 +35,5 @@
|
||||
"test": "cross-env TS_NODE_TRANSPILE_ONLY=1 TS_NODE_PROJECT=./tsconfig.json node --test --loader ts-node/esm --experimental-specifier-resolution=node ./__tests__/**/*.mts",
|
||||
"version": "napi version"
|
||||
},
|
||||
"version": "0.6.0-canary.2"
|
||||
"version": "0.6.0-canary.3"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{collections::HashMap, path::Path, sync::Arc};
|
||||
use std::{collections::BTreeMap, path::Path, sync::Arc};
|
||||
|
||||
use napi::{
|
||||
bindgen_prelude::{FromNapiValue, ToNapiValue},
|
||||
@@ -6,8 +6,31 @@ use napi::{
|
||||
};
|
||||
use napi_derive::napi;
|
||||
use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
static GLOBAL_WATCHER: Lazy<napi::Result<GlobalWatcher>> = Lazy::new(|| {
|
||||
let event_emitter = Arc::new(Mutex::new(EventEmitter {
|
||||
listeners: Default::default(),
|
||||
error_callbacks: Default::default(),
|
||||
}));
|
||||
let event_emitter_in_handler = event_emitter.clone();
|
||||
let watcher: RecommendedWatcher =
|
||||
notify::recommended_watcher(move |res: notify::Result<Event>| {
|
||||
event_emitter_in_handler.lock().on(res);
|
||||
})
|
||||
.map_err(anyhow::Error::from)?;
|
||||
Ok(GlobalWatcher {
|
||||
inner: Mutex::new(watcher),
|
||||
event_emitter,
|
||||
})
|
||||
});
|
||||
|
||||
struct GlobalWatcher {
|
||||
inner: Mutex<RecommendedWatcher>,
|
||||
event_emitter: Arc<Mutex<EventEmitter>>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
#[derive(Default)]
|
||||
pub struct WatchOptions {
|
||||
@@ -50,7 +73,6 @@ impl From<notify::WatcherKind> for WatcherKind {
|
||||
pub struct Subscription {
|
||||
id: uuid::Uuid,
|
||||
error_uuid: Option<uuid::Uuid>,
|
||||
event_emitter: Arc<Mutex<EventEmitter>>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
@@ -62,61 +84,52 @@ impl Subscription {
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn unsubscribe(&mut self) {
|
||||
let mut event_emitter = self.event_emitter.lock();
|
||||
pub fn unsubscribe(&mut self) -> napi::Result<()> {
|
||||
let mut event_emitter = GLOBAL_WATCHER
|
||||
.as_ref()
|
||||
.map_err(|err| err.clone())?
|
||||
.event_emitter
|
||||
.lock();
|
||||
event_emitter.listeners.remove(&self.id);
|
||||
if let Some(error_uuid) = &self.error_uuid {
|
||||
event_emitter.error_callbacks.remove(error_uuid);
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn watch(p: String, options: Option<WatchOptions>) -> Result<FSWatcher, anyhow::Error> {
|
||||
let event_emitter = Arc::new(Mutex::new(EventEmitter {
|
||||
listeners: Default::default(),
|
||||
error_callbacks: Default::default(),
|
||||
}));
|
||||
let event_emitter_in_handler = event_emitter.clone();
|
||||
let mut watcher: RecommendedWatcher =
|
||||
notify::recommended_watcher(move |res: notify::Result<Event>| {
|
||||
event_emitter_in_handler.lock().on(res);
|
||||
})
|
||||
.map_err(anyhow::Error::from)?;
|
||||
|
||||
let options = options.unwrap_or_default();
|
||||
watcher
|
||||
.watch(
|
||||
Path::new(&p),
|
||||
if options.recursive == Some(false) {
|
||||
RecursiveMode::NonRecursive
|
||||
} else {
|
||||
RecursiveMode::Recursive
|
||||
},
|
||||
)
|
||||
.map_err(anyhow::Error::from)?;
|
||||
Ok(FSWatcher {
|
||||
inner: watcher,
|
||||
event_emitter,
|
||||
})
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub struct FSWatcher {
|
||||
inner: RecommendedWatcher,
|
||||
event_emitter: Arc<Mutex<EventEmitter>>,
|
||||
path: String,
|
||||
recursive: RecursiveMode,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl FSWatcher {
|
||||
#[napi(getter)]
|
||||
pub fn kind(&self) -> WatcherKind {
|
||||
#[napi(factory)]
|
||||
pub fn watch(p: String, options: Option<WatchOptions>) -> Self {
|
||||
let options = options.unwrap_or_default();
|
||||
FSWatcher {
|
||||
path: p,
|
||||
recursive: if options.recursive == Some(false) {
|
||||
RecursiveMode::NonRecursive
|
||||
} else {
|
||||
RecursiveMode::Recursive
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn kind() -> WatcherKind {
|
||||
RecommendedWatcher::kind().into()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn to_string(&self) -> napi::Result<String> {
|
||||
Ok(format!("{:?}", self.inner))
|
||||
Ok(format!(
|
||||
"{:?}",
|
||||
GLOBAL_WATCHER.as_ref().map_err(|err| err.clone())?.inner
|
||||
))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
@@ -125,10 +138,23 @@ impl FSWatcher {
|
||||
#[napi(ts_arg_type = "(event: import('./event').NotifyEvent) => void")]
|
||||
callback: ThreadsafeFunction<serde_json::Value, ErrorStrategy::Fatal>,
|
||||
#[napi(ts_arg_type = "(err: Error) => void")] error_callback: Option<ThreadsafeFunction<()>>,
|
||||
) -> Subscription {
|
||||
) -> napi::Result<Subscription> {
|
||||
GLOBAL_WATCHER
|
||||
.as_ref()
|
||||
.map_err(|err| err.clone())?
|
||||
.inner
|
||||
.lock()
|
||||
.watch(Path::new(&self.path), self.recursive)
|
||||
.map_err(anyhow::Error::from)?;
|
||||
let uuid = uuid::Uuid::new_v4();
|
||||
let mut event_emitter = self.event_emitter.lock();
|
||||
event_emitter.listeners.insert(uuid, callback);
|
||||
let mut event_emitter = GLOBAL_WATCHER
|
||||
.as_ref()
|
||||
.map_err(|err| err.clone())?
|
||||
.event_emitter
|
||||
.lock();
|
||||
event_emitter
|
||||
.listeners
|
||||
.insert(uuid, (self.path.clone(), callback));
|
||||
let mut error_uuid = None;
|
||||
if let Some(error_callback) = error_callback {
|
||||
let uuid = uuid::Uuid::new_v4();
|
||||
@@ -136,32 +162,51 @@ impl FSWatcher {
|
||||
error_uuid = Some(uuid);
|
||||
}
|
||||
drop(event_emitter);
|
||||
Subscription {
|
||||
Ok(Subscription {
|
||||
id: uuid,
|
||||
error_uuid,
|
||||
event_emitter: self.event_emitter.clone(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn close(&mut self) -> napi::Result<()> {
|
||||
// drop the previous watcher
|
||||
self.inner = notify::recommended_watcher(|_| {}).map_err(anyhow::Error::from)?;
|
||||
self.event_emitter.lock().stop();
|
||||
pub fn unwatch(p: String) -> napi::Result<()> {
|
||||
let mut watcher = GLOBAL_WATCHER
|
||||
.as_ref()
|
||||
.map_err(|err| err.clone())?
|
||||
.inner
|
||||
.lock();
|
||||
watcher
|
||||
.unwatch(Path::new(&p))
|
||||
.map_err(anyhow::Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn close() -> napi::Result<()> {
|
||||
let global_watcher = GLOBAL_WATCHER.as_ref().map_err(|err| err.clone())?;
|
||||
global_watcher.event_emitter.lock().stop();
|
||||
let mut inner = global_watcher.inner.lock();
|
||||
*inner = notify::recommended_watcher(|_| {}).map_err(anyhow::Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct EventEmitter {
|
||||
listeners: HashMap<uuid::Uuid, ThreadsafeFunction<serde_json::Value, ErrorStrategy::Fatal>>,
|
||||
error_callbacks: HashMap<uuid::Uuid, ThreadsafeFunction<()>>,
|
||||
listeners: BTreeMap<
|
||||
uuid::Uuid,
|
||||
(
|
||||
String,
|
||||
ThreadsafeFunction<serde_json::Value, ErrorStrategy::Fatal>,
|
||||
),
|
||||
>,
|
||||
error_callbacks: BTreeMap<uuid::Uuid, ThreadsafeFunction<()>>,
|
||||
}
|
||||
|
||||
impl EventEmitter {
|
||||
fn on(&self, event: notify::Result<Event>) {
|
||||
match event {
|
||||
Ok(e) => match serde_json::value::to_value(e) {
|
||||
Ok(e) => match serde_json::value::to_value(&e) {
|
||||
Err(err) => {
|
||||
let err: napi::Error = anyhow::Error::from(err).into();
|
||||
for on_error in self.error_callbacks.values() {
|
||||
@@ -169,8 +214,10 @@ impl EventEmitter {
|
||||
}
|
||||
}
|
||||
Ok(v) => {
|
||||
for on_event in self.listeners.values() {
|
||||
on_event.call(v.clone(), ThreadsafeFunctionCallMode::NonBlocking);
|
||||
for (path, on_event) in self.listeners.values() {
|
||||
if e.paths.iter().any(|p| p.to_str() == Some(path)) {
|
||||
on_event.call(v.clone(), ThreadsafeFunctionCallMode::NonBlocking);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -188,3 +235,9 @@ impl EventEmitter {
|
||||
self.error_callbacks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn move_file(src: String, dst: String) -> napi::Result<()> {
|
||||
tokio::fs::rename(src, dst).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user