Merge pull request #187 from teaxyz/electron-migration
Electron migration
121
.github/workflows/ci.yml
vendored
|
@ -5,32 +5,23 @@ jobs:
|
|||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
svelte: ${{steps.svelte.outputs.src}}
|
||||
tauri: ${{steps.tauri.outputs.src}}
|
||||
desktop: ${{steps.desktop.outputs.src}}
|
||||
preview_folder: ${{steps.preview.outputs.folder}}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: svelte
|
||||
id: desktop
|
||||
with:
|
||||
filters: |
|
||||
src:
|
||||
- 'modules/gui/src/**'
|
||||
- 'modules/desktop/**'
|
||||
- 'modules/ui/**'
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: tauri
|
||||
with:
|
||||
filters: |
|
||||
src:
|
||||
- 'modules/gui/src-tauri/**'
|
||||
- 'modules/gui/src/**'
|
||||
- 'modules/ui/src/**'
|
||||
- name: get s3 preview folder
|
||||
id: preview
|
||||
run: echo "folder=${{ github.event.number }}-merge" >> $GITHUB_OUTPUT
|
||||
no_preview:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.svelte == 'false'
|
||||
if: needs.changes.outputs.desktop == 'false'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: comment preview site
|
||||
|
@ -104,18 +95,18 @@ jobs:
|
|||
https://${{steps.preview_setup.outputs.domain}}
|
||||
```
|
||||
copy-paste into a browser to view
|
||||
|
||||
build_tauri:
|
||||
build_desktop:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.tauri == 'true'
|
||||
if: needs.changes.outputs.desktop == 'true'
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- os: macos-11
|
||||
name: darwin+x86-64
|
||||
- os: ubuntu-latest
|
||||
name: linux+x86-64
|
||||
# TODO: #181 build for linux
|
||||
# - os: ubuntu-latest
|
||||
# name: linux+x86-64
|
||||
- os: [self-hosted, macOS, ARM64]
|
||||
name: darwin+aarch64
|
||||
# err: Package atk was not found in the pkg-config search path.
|
||||
|
@ -125,40 +116,45 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: teaxyz/setup@v0
|
||||
if: startsWith(matrix.platform.name, 'darwin')
|
||||
|
||||
- name: cache gui build
|
||||
- name: cache node_modules build
|
||||
# TODO: cache issue in our self-hosted macos runner ESPIPE: invalid seek, read
|
||||
# but its ok to ignore, its still the fastest builder
|
||||
# NOTE: enabling cache in the self hosted runner slows down the pipeline by 4m because post-cache builder error ^
|
||||
if: startsWith(matrix.platform.name, 'linux') || matrix.platform.name == 'darwin+x86-64'
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
key: ${{matrix.platform.name}}-pnpm-cargo
|
||||
key: ${{matrix.platform.name}}-pnpm
|
||||
path: |
|
||||
./pnpm
|
||||
./target
|
||||
# - name: build tauri for Linux
|
||||
# uses: ./devops/linux-build
|
||||
- name: build tauri for MacOS
|
||||
./.pnpm-store
|
||||
./node_modules
|
||||
./modules/desktop/node_modules
|
||||
./modules/ui/node_modules
|
||||
- name: cache electron build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
key: ${{matrix.platform.name}}-electron
|
||||
path: |
|
||||
./modules/desktop/.svelte-kit
|
||||
./modules/desktop/build
|
||||
./modules/desktop/dist
|
||||
|
||||
- name: build
|
||||
if: startsWith(matrix.platform.name, 'darwin')
|
||||
# FROM: https://tauri.app/v1/guides/distribution/sign-macos
|
||||
run: tea -ES xc dist
|
||||
env:
|
||||
CARGO_TARGET_DIR: ${{ github.workspace }}/target
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.GUI_APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.GUI_APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.GUI_APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
run: tea -ES xc build
|
||||
|
||||
- name: build tauri for Linux
|
||||
if: startsWith(matrix.platform.name, 'linux')
|
||||
# TODO: https://tauri.app/v1/guides/distribution/sign-linux
|
||||
uses: ./devops/linux-builder
|
||||
|
||||
- run: tar -czvf artifacts.tgz -C ./target/release/bundle .
|
||||
|
||||
# PUBLISH_FOR_PULL_REQUEST: true
|
||||
USE_HARD_LINKS: false
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
CSC_LINK: ${{ secrets.GUI_APPLE_CERTIFICATE }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.GUI_APPLE_CERTIFICATE_PASSWORD }}
|
||||
CSC_NAME: ${{ secrets.APPLE_IDENTITY_NO_PREFIX }}
|
||||
# APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
# APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
- run: mkdir -p target
|
||||
- run: cp ./modules/desktop/dist/*.zip ./target/tea.zip
|
||||
- run: tar -czvf artifacts.tgz -C ./target/ .
|
||||
- name: upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
@ -166,8 +162,9 @@ jobs:
|
|||
path: artifacts.tgz
|
||||
if-no-files-found: error
|
||||
|
||||
notorize_tauri:
|
||||
needs: [build_tauri]
|
||||
notorize_app:
|
||||
needs: [build_desktop]
|
||||
# NOTE: atm notarization is only doable in gh macos-11 not in our self-hosted runner
|
||||
runs-on: macos-11
|
||||
strategy:
|
||||
matrix:
|
||||
|
@ -181,24 +178,22 @@ jobs:
|
|||
|
||||
- run: tar xzf artifacts.tgz
|
||||
|
||||
- run: cd ./macos/ && zip -r tea.zip tea.app
|
||||
|
||||
# Notarize. Can take up to 10 minutes (and fail) asynchronously
|
||||
- run: xcrun altool --notarize-app --username "$APPLE_ID" --password "$APPLE_PASSWORD" --primary-bundle-id "com.tea.xyz" --file ./macos/tea.zip
|
||||
- run: xcrun altool --notarize-app --username "$APPLE_ID" --password "$APPLE_PASSWORD" --primary-bundle-id "xyz.tea.gui" --file ./tea.zip
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
|
||||
upload:
|
||||
needs: [build_tauri, notorize_tauri]
|
||||
needs: [notorize_app, changes]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- name: darwin+x86-64
|
||||
id: mac_latest
|
||||
- name: linux+x86-64
|
||||
id: linux
|
||||
# - name: linux+x86-64
|
||||
# id: linux
|
||||
- name: darwin+aarch64
|
||||
id: mac_m1
|
||||
# - name: linux+aarch64
|
||||
|
@ -228,11 +223,11 @@ jobs:
|
|||
;;
|
||||
"darwin+aarch64")
|
||||
BUILD_PLATFORM="aarch64"
|
||||
EXTENSION="dmg"
|
||||
EXTENSION="zip"
|
||||
;;
|
||||
"darwin+x86-64")
|
||||
BUILD_PLATFORM="x64"
|
||||
EXTENSION="dmg"
|
||||
EXTENSION="zip"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown platform $platform"
|
||||
|
@ -242,12 +237,6 @@ jobs:
|
|||
echo "build_platform=$BUILD_PLATFORM" >> $GITHUB_OUTPUT
|
||||
echo "extension=$EXTENSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: zip .app for MacOS
|
||||
if: startsWith(matrix.platform.name, 'darwin+')
|
||||
run: |
|
||||
cd ./macos/
|
||||
zip -r tea.zip tea.app
|
||||
|
||||
- uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
|
@ -261,18 +250,9 @@ jobs:
|
|||
extension: ${{ steps.build_platform.outputs.extension }}
|
||||
run: |
|
||||
aws s3 cp \
|
||||
"./$extension/tea_0.1.0_$platform.$extension" \
|
||||
"./tea.$extension" \
|
||||
"s3://preview.gui.tea.xyz/$prefix/tea_$platform.$extension"
|
||||
|
||||
- name: cp package zip for MacOS
|
||||
if: startsWith(matrix.platform.name, 'darwin')
|
||||
env:
|
||||
prefix: ${{ needs.changes.outputs.preview_folder }}
|
||||
platform: ${{ steps.build_platform.outputs.build_platform }}
|
||||
run: |
|
||||
aws s3 cp \
|
||||
./macos/tea.zip "s3://preview.gui.tea.xyz/$prefix/tea_$platform.zip"
|
||||
|
||||
- name: comment install for Linux
|
||||
if: startsWith(matrix.platform.name, 'linux')
|
||||
uses: mshick/add-pr-comment@v2
|
||||
|
@ -280,8 +260,6 @@ jobs:
|
|||
message-id: ${{ matrix.platform.id }}-comment
|
||||
message: |
|
||||
**installer for Linux ${{ matrix.platform.name }} is at**:
|
||||
<a href="http://preview.gui.tea.xyz.s3-website-us-east-1.amazonaws.com/${{ needs.changes.outputs.preview_folder }}/tea_${{steps.build_platform.outputs.build_platform}}.${{steps.build_platform.outputs.extension}}" target="_blank">here</a>
|
||||
|
||||
```bash
|
||||
http://preview.gui.tea.xyz.s3-website-us-east-1.amazonaws.com/${{ needs.changes.outputs.preview_folder }}/tea_${{ steps.build_platform.outputs.build_platform }}.${{ steps.build_platform.outputs.extension }}
|
||||
```
|
||||
|
@ -294,12 +272,7 @@ jobs:
|
|||
message-id: ${{ matrix.platform.id }}-comment
|
||||
message: |
|
||||
**installers for MacOS ${{ matrix.platform.name }} is at**:
|
||||
<a href="http://preview.gui.tea.xyz.s3-website-us-east-1.amazonaws.com/${{ needs.changes.outputs.preview_folder }}/tea_${{steps.build_platform.outputs.build_platform}}.zip" target="_blank">.zip</a>
|
||||
or
|
||||
<a href="http://preview.gui.tea.xyz.s3-website-us-east-1.amazonaws.com/${{ needs.changes.outputs.preview_folder }}/tea_${{steps.build_platform.outputs.build_platform}}.dmg" target="_blank">.dmg</a>
|
||||
|
||||
```bash
|
||||
http://preview.gui.tea.xyz.s3-website-us-east-1.amazonaws.com/${{ needs.changes.outputs.preview_folder }}/tea_${{ steps.build_platform.outputs.build_platform }}.zip
|
||||
http://preview.gui.tea.xyz.s3-website-us-east-1.amazonaws.com/${{ needs.changes.outputs.preview_folder }}/tea_${{ steps.build_platform.outputs.build_platform }}.dmg
|
||||
```
|
||||
copy-paste into a browser to download
|
129
.github/workflows/main.yml
vendored
|
@ -9,92 +9,78 @@ jobs:
|
|||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
svelte: ${{steps.svelte.outputs.src}}
|
||||
tauri: ${{steps.tauri.outputs.src}}
|
||||
desktop: ${{steps.desktop.outputs.src}}
|
||||
preview_folder: ${{steps.preview.outputs.folder}}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: svelte
|
||||
id: desktop
|
||||
with:
|
||||
filters: |
|
||||
src:
|
||||
- 'modules/gui/src/**'
|
||||
- 'modules/desktop/**'
|
||||
- 'modules/ui/**'
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: tauri
|
||||
with:
|
||||
filters: |
|
||||
src:
|
||||
- 'modules/gui/src-tauri/**'
|
||||
- 'modules/gui/src/**'
|
||||
- 'modules/ui/src/**'
|
||||
build_svelte:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.svelte == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: cache gui linux
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
key: linux+x86-64-20-pnpm-cargo-main
|
||||
path: |
|
||||
./pnpm
|
||||
./target
|
||||
- name: build tauri for Linux
|
||||
if: startsWith(matrix.platform.name, 'linux')
|
||||
uses: docker://getneil/tea-builder:latest
|
||||
- name: get s3 preview folder
|
||||
id: preview
|
||||
run: echo "folder=${{ github.event.number }}-merge" >> $GITHUB_OUTPUT
|
||||
|
||||
build_tauri:
|
||||
build_desktop:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.tauri == 'true'
|
||||
if: needs.changes.outputs.desktop == 'true'
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- os: macos-11
|
||||
name: darwin+x86-64
|
||||
- os: ubuntu-latest
|
||||
name: linux+x86-64
|
||||
# TODO: #181 build for linux
|
||||
# - os: ubuntu-latest
|
||||
# name: linux+x86-64
|
||||
- os: [self-hosted, macOS, ARM64]
|
||||
name: darwin+aarch64
|
||||
# err: Package atk was not found in the pkg-config search path.
|
||||
# requires atk >= 2.18
|
||||
# - os: [self-hosted, linux, ARM64]
|
||||
# name: linux+aarch64
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: teaxyz/setup@v0
|
||||
if: startsWith(matrix.platform.name, 'darwin')
|
||||
- name: cache gui build
|
||||
- name: cache node_modules build
|
||||
# TODO: cache issue in our self-hosted macos runner ESPIPE: invalid seek, read
|
||||
# but its ok to ignore, its still the fastest builder
|
||||
# NOTE: enabling cache in the self hosted runner slows down the pipeline by 4m because post-cache builder error ^
|
||||
if: startsWith(matrix.platform.name, 'linux') || matrix.platform.name == 'darwin+x86-64'
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
key: ${{matrix.platform.name}}-pnpm-cargo-main
|
||||
key: ${{matrix.platform.name}}-pnpm-prod
|
||||
path: |
|
||||
./pnpm
|
||||
./target
|
||||
- name: build tauri for MacOS
|
||||
./.pnpm-store
|
||||
./node_modules
|
||||
./modules/desktop/node_modules
|
||||
./modules/ui/node_modules
|
||||
- name: cache electron build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
key: ${{matrix.platform.name}}-electron-prod
|
||||
path: |
|
||||
./modules/desktop/.svelte-kit
|
||||
./modules/desktop/build
|
||||
./modules/desktop/dist
|
||||
|
||||
- name: build
|
||||
if: startsWith(matrix.platform.name, 'darwin')
|
||||
run: tea -ES xc build
|
||||
run: tea -ES xc dist
|
||||
env:
|
||||
CARGO_TARGET_DIR: ${{ github.workspace }}/target
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.GUI_APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.GUI_APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.GUI_APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
|
||||
- name: build tauri for Linux
|
||||
if: startsWith(matrix.platform.name, 'linux')
|
||||
# TODO: https://tauri.app/v1/guides/distribution/sign-linux
|
||||
uses: ./devops/linux-builder
|
||||
|
||||
- run: tar -czvf artifacts.tgz -C ./target/release/bundle .
|
||||
|
||||
USE_HARD_LINKS: false
|
||||
CSC_LINK: ${{ secrets.GUI_APPLE_CERTIFICATE }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.GUI_APPLE_CERTIFICATE_PASSWORD }}
|
||||
CSC_NAME: ${{ secrets.APPLE_IDENTITY_NO_PREFIX }}
|
||||
# APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
# APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
- run: mkdir -p target
|
||||
- run: cp ./modules/desktop/dist/*.zip ./target/tea.zip
|
||||
- run: tar -czvf artifacts.tgz -C ./target/ .
|
||||
- name: upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
@ -102,8 +88,8 @@ jobs:
|
|||
path: artifacts.tgz
|
||||
if-no-files-found: error
|
||||
|
||||
notorize_tauri:
|
||||
needs: [build_tauri]
|
||||
notarize_desktop:
|
||||
needs: [build_desktop]
|
||||
runs-on: macos-11
|
||||
strategy:
|
||||
matrix:
|
||||
|
@ -126,13 +112,13 @@ jobs:
|
|||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
|
||||
upload:
|
||||
needs: [build_tauri, notorize_tauri]
|
||||
needs: [build_desktop, notarize_desktop]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- darwin+x86-64
|
||||
- linux+x86-64
|
||||
# - linux+x86-64
|
||||
- darwin+aarch64
|
||||
# - linux+aarch64
|
||||
steps:
|
||||
|
@ -171,11 +157,11 @@ jobs:
|
|||
;;
|
||||
"darwin+aarch64")
|
||||
BUILD_PLATFORM="aarch64"
|
||||
EXTENSION="dmg"
|
||||
EXTENSION="zip"
|
||||
;;
|
||||
"darwin+x86-64")
|
||||
BUILD_PLATFORM="x64"
|
||||
EXTENSION="dmg"
|
||||
EXTENSION="zip"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown platform $platform"
|
||||
|
@ -185,12 +171,6 @@ jobs:
|
|||
echo "build_platform=$BUILD_PLATFORM" >> $GITHUB_OUTPUT
|
||||
echo "extension=$EXTENSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: zip .app for MacOS
|
||||
if: startsWith(matrix.platform, 'darwin+')
|
||||
run: |
|
||||
cd ./macos/
|
||||
zip -r tea.zip tea.app
|
||||
|
||||
- name: cp package images from prod to gui bucket
|
||||
env:
|
||||
platform: ${{ steps.build_platform.outputs.build_platform }}
|
||||
|
@ -198,20 +178,22 @@ jobs:
|
|||
extension: ${{steps.build_platform.outputs.extension}}
|
||||
run: |
|
||||
aws s3 cp \
|
||||
"./$extension/tea_0.1.0_$platform.$extension" \
|
||||
"./tea.$extension" \
|
||||
"s3://preview.gui.tea.xyz/release/tea_${{ steps.date.outputs.unix_seconds }}_$platform.$extension"
|
||||
|
||||
- name: cp package zip for MacOS
|
||||
if: startsWith(matrix.platform, 'darwin')
|
||||
env:
|
||||
platform: ${{ steps.build_platform.outputs.build_platform }}
|
||||
build_platform: ${{ matrix.platform }}
|
||||
extension: ${{ steps.build_platform.outputs.extension }}
|
||||
run: |
|
||||
aws s3 cp ./macos/tea.zip \
|
||||
"s3://preview.gui.tea.xyz/release/tea_${{ steps.date.outputs.unix_seconds }}_$platform.zip"
|
||||
"s3://preview.gui.tea.xyz/release/tea_${{ steps.date.outputs.unix_seconds }}_$platform.$extension"
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Slack Notification
|
||||
run: ./.github/notify-slack.js
|
||||
env:
|
||||
|
@ -219,12 +201,3 @@ jobs:
|
|||
PLATFORM: ${{ matrix.platform }}
|
||||
EXT: ${{ steps.build_platform.outputs.extension }}
|
||||
DOWNLOAD_URL: http://preview.gui.tea.xyz.s3-website-us-east-1.amazonaws.com/release/tea_${{ steps.date.outputs.unix_seconds }}_${{ steps.build_platform.outputs.build_platform }}.${{ steps.build_platform.outputs.extension }}
|
||||
|
||||
- name: Slack Notification for .app Mac
|
||||
run: ./.github/notify-slack.js
|
||||
if: startsWith(matrix.platform, 'darwin')
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
EXT: .zip(.app)
|
||||
DOWNLOAD_URL: http://preview.gui.tea.xyz.s3-website-us-east-1.amazonaws.com/release/tea_${{ steps.date.outputs.unix_seconds }}_${{ steps.build_platform.outputs.build_platform }}.zip
|
||||
|
|
152
.github/workflows/release.yml
vendored
|
@ -4,15 +4,15 @@ on:
|
|||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
build_release_tauri:
|
||||
build_desktop:
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- os: macos-11
|
||||
name: darwin+x86-64
|
||||
- os: ubuntu-latest
|
||||
name: linux+x86-64
|
||||
# - os: ubuntu-latest
|
||||
# name: linux+x86-64
|
||||
- os: [self-hosted, macOS, ARM64]
|
||||
name: darwin+aarch64
|
||||
# - os: [self-hosted, linux, ARM64]
|
||||
|
@ -20,45 +20,99 @@ jobs:
|
|||
container: ${{ matrix.platform.container }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: build platform output
|
||||
id: build_platform
|
||||
env:
|
||||
platform: ${{ matrix.platform.name }}
|
||||
run: |
|
||||
EXTENSION=dmg
|
||||
BUILD_PLATFORM=$(echo $platform | sed -e "s/darwin+//g" | sed -e "s/linux+//g")
|
||||
[[ $BUILD_PLATFORM = "x86-64" ]] && BUILD_PLATFORM="x64" || BUILD_PLATFORM=$BUILD_PLATFORM
|
||||
[[ $platform = "linux+x86-64" ]] && BUILD_PLATFORM="amd64" || BUILD_PLATFORM=$BUILD_PLATFORM
|
||||
[[ $platform = "linux+x86-64" ]] && EXTENSION="deb"
|
||||
echo "build_platform=$BUILD_PLATFORM" >> $GITHUB_OUTPUT
|
||||
echo "extension=$EXTENSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: build tauri for MacOS
|
||||
- uses: teaxyz/setup@v0
|
||||
- name: build
|
||||
if: startsWith(matrix.platform.name, 'darwin')
|
||||
# FROM: https://tauri.app/v1/guides/distribution/sign-macos
|
||||
run: tea -ES xc dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.GUI_APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.GUI_APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.GUI_APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
# todo: uncomment when m1 with latest xcode is deployed
|
||||
# this only helps in notarization
|
||||
# APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
# APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
run: |
|
||||
sh <(curl https://tea.xyz) -ES xc build
|
||||
USE_HARD_LINKS: false
|
||||
CSC_LINK: ${{ secrets.GUI_APPLE_CERTIFICATE }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.GUI_APPLE_CERTIFICATE_PASSWORD }}
|
||||
CSC_NAME: ${{ secrets.APPLE_IDENTITY_NO_PREFIX }}
|
||||
- run: mkdir -p target
|
||||
- run: cp ./modules/desktop/dist/*.zip ./target/tea.zip
|
||||
- run: tar -czvf artifacts.tgz -C ./target/ .
|
||||
- name: upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.platform.name }}
|
||||
path: artifacts.tgz
|
||||
if-no-files-found: error
|
||||
|
||||
- name: build tauri for Linux
|
||||
# uses: teaxyz/setup@v0
|
||||
if: startsWith(matrix.platform.name, 'linux')
|
||||
# TODO: https://tauri.app/v1/guides/distribution/sign-linux
|
||||
uses: docker://getneil/tea-builder:latest
|
||||
notarize_desktop:
|
||||
needs: [build_desktop]
|
||||
runs-on: macos-11
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- darwin+x86-64
|
||||
- darwin+aarch64
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.platform }}
|
||||
|
||||
- run: tar xzf artifacts.tgz
|
||||
|
||||
- run: cd ./macos/ && zip -r tea.zip tea.app
|
||||
|
||||
# Notarize. Can take up to 10 minutes (and fail) asynchronously
|
||||
- run: xcrun altool --notarize-app --username "$APPLE_ID" --password "$APPLE_PASSWORD" --primary-bundle-id "com.tea.xyz" --file ./macos/tea.zip
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
|
||||
upload:
|
||||
needs: [build_desktop, notarize_desktop]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- darwin+x86-64
|
||||
# - linux+x86-64
|
||||
- darwin+aarch64
|
||||
# - linux+aarch64
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.platform }}
|
||||
- name: Get current unix ts - seconds
|
||||
id: date
|
||||
run: echo "unix_seconds=$(date +'%s')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: build platform output
|
||||
id: build_platform
|
||||
env:
|
||||
platform: ${{ matrix.platform }}
|
||||
run: |
|
||||
BUILD_PLATFORM=$(echo $platform | sed -e "s/darwin+//g" | sed -e "s/linux+//g")
|
||||
EXTENSION=dmg
|
||||
case $platform in
|
||||
"linux+x86-64")
|
||||
BUILD_PLATFORM="amd64"
|
||||
EXTENSION="deb"
|
||||
;;
|
||||
"linux+aarch64")
|
||||
BUILD_PLATFORM="aarch64"
|
||||
EXTENSION="deb"
|
||||
;;
|
||||
"darwin+aarch64")
|
||||
BUILD_PLATFORM="aarch64"
|
||||
EXTENSION="zip"
|
||||
;;
|
||||
"darwin+x86-64")
|
||||
BUILD_PLATFORM="x64"
|
||||
EXTENSION="zip"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown platform $platform"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo "build_platform=$BUILD_PLATFORM" >> $GITHUB_OUTPUT
|
||||
echo "extension=$EXTENSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
|
@ -76,27 +130,11 @@ jobs:
|
|||
extension: ${{ steps.build_platform.outputs.extension }}
|
||||
tag: ${{ steps.tag.outputs.tag }}
|
||||
run: |
|
||||
aws s3 cp "./modules/gui/src-tauri/target/release/bundle/$extension/tea_0.1.0_$platform.$extension" \
|
||||
aws s3 cp "./tea.$extension" \
|
||||
"s3://preview.gui.tea.xyz/release/tea_gui_latest_$platform.$extension"
|
||||
aws s3 cp "./modules/gui/src-tauri/target/release/bundle/$extension/tea_0.1.0_$platform.$extension" \
|
||||
aws s3 cp "./tea.$extension" \
|
||||
"s3://preview.gui.tea.xyz/release/tea_gui_${{ steps.tag.outputs.tag }}_$platform.$extension"
|
||||
|
||||
- name: zip .app for MacOS
|
||||
if: startsWith(matrix.platform.name, 'darwin')
|
||||
run: |
|
||||
cd ./modules/gui/src-tauri/target/release/bundle/macos/ && zip -r tea.zip tea.app
|
||||
|
||||
- name: publish .zip(.app) for MacOS
|
||||
if: startsWith(matrix.platform.name, 'darwin')
|
||||
env:
|
||||
platform: ${{ steps.build_platform.outputs.build_platform }}
|
||||
extension: ${{ steps.build_platform.outputs.extension }}
|
||||
tag: ${{ steps.tag.outputs.tag }}
|
||||
run: |
|
||||
aws s3 cp "./modules/gui/src-tauri/target/release/bundle/macos/tea.zip" \
|
||||
"s3://preview.gui.tea.xyz/release/tea_gui_latest_$platform.zip"
|
||||
aws s3 cp "./modules/gui/src-tauri/target/release/bundle/macos/tea.zip" \
|
||||
"s3://preview.gui.tea.xyz/release/tea_gui_${{ steps.tag.outputs.tag }}_$platform.zip"
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
@ -108,13 +146,3 @@ jobs:
|
|||
VERSION: ${{steps.tag.outputs.tag}}
|
||||
EXT: ${{ steps.build_platform.outputs.extension }}
|
||||
DOWNLOAD_URL: http://preview.gui.tea.xyz.s3-website-us-east-1.amazonaws.com/release/tea_gui_${{steps.tag.outputs.tag}}_${{steps.build_platform.outputs.build_platform}}.${{ steps.build_platform.outputs.extension }}
|
||||
|
||||
- name: Slack Notification for .app Mac
|
||||
run: ./.github/notify-slack.js
|
||||
if: startsWith(matrix.platform.name, 'darwin')
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
PLATFORM: ${{ matrix.platform.name }}
|
||||
VERSION: ${{steps.tag.outputs.tag}}
|
||||
EXT: .zip(.app)
|
||||
DOWNLOAD_URL: http://preview.gui.tea.xyz.s3-website-us-east-1.amazonaws.com/release/tea_gui_${{steps.tag.outputs.tag}}_${{steps.build_platform.outputs.build_platform}}.zip
|
||||
|
|
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
node-linker=hoisted
|
22
README.md
|
@ -15,6 +15,7 @@ For better documentation checkout this [notion](https://www.notion.so/teaxyz/tea
|
|||
| rust-lang.org | >=1.62 |
|
||||
| rust-lang.org/cargo | >=0.66 |
|
||||
| xcfile.dev | >=0.0.110 |
|
||||
| python.org | >=3.10 |
|
||||
|
||||
## Tasks
|
||||
|
||||
|
@ -26,18 +27,31 @@ pnpm install
|
|||
### build
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm build:gui
|
||||
pnpm build:desktop
|
||||
```
|
||||
|
||||
### dev
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### dist
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm add -g vite
|
||||
pnpm --filter desktop exec pnpm dist
|
||||
```
|
||||
|
||||
## Development
|
||||
To develop the GUI within Tauri Webview
|
||||
To develop the GUI within electron view
|
||||
```
|
||||
$ pnpm dev:gui
|
||||
$ pnpm dev:desktop
|
||||
```
|
||||
|
||||
To develop the GUI within your local browser at localhost:8080
|
||||
```
|
||||
$ pnpm web:gui
|
||||
$ pnpm web:desktop
|
||||
```
|
||||
|
||||
# Creating a release
|
||||
|
|
17
modules/desktop/.eslintignore
Normal file
|
@ -0,0 +1,17 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
coverage/*
|
||||
build/*
|
||||
dist/*
|
||||
electron/dist/*
|
31
modules/desktop/.eslintrc.cjs
Normal file
|
@ -0,0 +1,31 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
globals: {
|
||||
NodeJS: true
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
|
||||
plugins: ['svelte3', '@typescript-eslint'],
|
||||
ignorePatterns: ['*.cjs'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
processor: 'svelte3/svelte3'
|
||||
}
|
||||
],
|
||||
settings: {
|
||||
'svelte3/typescript': () => require('typescript')
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow-with-description' }]
|
||||
}
|
||||
};
|
11
modules/desktop/.gitignore
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
coverage/*
|
||||
dist/*
|
||||
electron/dist/*
|
1
modules/desktop/.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
node-linker=hoisted
|
19
modules/desktop/.prettierignore
Normal file
|
@ -0,0 +1,19 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
build
|
||||
coverage/*
|
||||
**/*.plist
|
||||
build/*
|
||||
dist/*
|
||||
electron/dist/*
|
10
modules/desktop/.prettierrc
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
58
modules/desktop/README.md
Normal file
|
@ -0,0 +1,58 @@
|
|||
# @tea/desktop
|
||||
|
||||
Desktop app of [tea](https://tea.xyz) for installing packages/softwares
|
||||
|
||||
More interesting and possibly updated documentations are at this [NOTION](https://www.notion.so/teaxyz/tea-gui-fdd9f50aa980432fa370b2cf6a03cb50) page. It is ideal to review it also, its more updated.
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
# use if you need interaction with the rust handlers
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
pnpm playwright install
|
||||
pnpm test
|
||||
|
||||
```
|
||||
|
||||
## Intuition Building Links
|
||||
|
||||
- [Rust module system is weird?](https://www.sheshbabu.com/posts/rust-module-system/)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/Dax89/electron-sveltekit/blob/master/icon.png" width="256">
|
||||
</p>
|
||||
<p align="center">
|
||||
A minimal project template for Electron and SvelteKit configured with <a href="https://www.npmjs.com/package/@sveltejs/adapter-static">adapter-static</a>.
|
||||
</p>
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||
## Commands
|
||||
|
||||
- `pnpm dev`: Runs SvelteKit in dev mode
|
||||
- `pnpm preview`: Runs SvelteKit in production mode
|
||||
- `pnpm electron`: Runs SvelteKit with electron in dev mode
|
||||
- `pnpm build`: Runs SvelteKit compiler
|
||||
- `pnpm dev:package`: Creates an Electron package (you can inspect the contents)
|
||||
- `pnpm package`: Creates a distributable Electron package
|
43
modules/desktop/electron-builder.config.cjs
Normal file
|
@ -0,0 +1,43 @@
|
|||
// const { notarize } = require('@electron/notarize');
|
||||
// const fs = require('fs');
|
||||
// const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
appId: 'xyz.tea.gui',
|
||||
productName: 'tea',
|
||||
asar: false,
|
||||
directories: { output: 'dist' },
|
||||
files: ['electron/dist/electron.cjs', { from: 'build', to: '' }]
|
||||
// TODO: if xcrun altool exists eventually in our self-hosted macos
|
||||
// SOLUTION: is notarize separately in next pipeline step
|
||||
// afterSign: async (params) => {
|
||||
// if (process.platform !== 'darwin') {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// console.log('afterSign hook triggered', params);
|
||||
|
||||
// const appBundleId = 'xyz.tea.gui';
|
||||
|
||||
// let appPath = path.join(params.appOutDir, `${params.packager.appInfo.productFilename}.app`);
|
||||
// if (!fs.existsSync(appPath)) {
|
||||
// console.log('skip');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// console.log(`Notarizing ${appBundleId} found at ${appPath} with Apple ID ${process.env.APPLE_ID}`);
|
||||
|
||||
// try {
|
||||
// await notarize({
|
||||
// appBundleId,
|
||||
// appPath,
|
||||
// appleId: process.env.APPLE_ID,
|
||||
// appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD
|
||||
// });
|
||||
// } catch (error) {
|
||||
// console.error(error);
|
||||
// }
|
||||
|
||||
// console.log(`Done notarizing ${appId}`);
|
||||
// }
|
||||
};
|
135
modules/desktop/electron/electron.ts
Normal file
|
@ -0,0 +1,135 @@
|
|||
import windowStateManager from 'electron-window-state';
|
||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||
import contextMenu from 'electron-context-menu';
|
||||
import serve from 'electron-serve';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
import { getInstalledPackages } from './libs/teaDir';
|
||||
import { readSessionData, writeSessionData } from './libs/auth';
|
||||
import type { Session } from '../src/libs/types';
|
||||
import { installPackage } from './libs/cli';
|
||||
|
||||
// try {
|
||||
// //@ts-ignore only used in dev should not be packaged inprod
|
||||
// /* eslint-disable */
|
||||
// const er = require('electron-reloader');
|
||||
// er(module);
|
||||
// } catch (e) {
|
||||
// console.error(e);
|
||||
// }
|
||||
|
||||
const serveURL = serve({ directory: '.' });
|
||||
const port = process.env.PORT || 3000;
|
||||
const dev = !app.isPackaged;
|
||||
let mainWindow: BrowserWindow | null;
|
||||
|
||||
function createWindow() {
|
||||
const windowState = windowStateManager({
|
||||
defaultWidth: 800,
|
||||
defaultHeight: 600
|
||||
});
|
||||
|
||||
const mainWindow = new BrowserWindow({
|
||||
backgroundColor: 'whitesmoke',
|
||||
autoHideMenuBar: true,
|
||||
trafficLightPosition: {
|
||||
x: 17,
|
||||
y: 32
|
||||
},
|
||||
minHeight: 450,
|
||||
minWidth: 500,
|
||||
webPreferences: {
|
||||
// enableRemoteModule: true,
|
||||
contextIsolation: false,
|
||||
nodeIntegration: true,
|
||||
spellcheck: false,
|
||||
webSecurity: false,
|
||||
devTools: dev
|
||||
// preload: path.join(app.getAppPath(), 'preload.cjs')
|
||||
},
|
||||
x: windowState.x,
|
||||
y: windowState.y,
|
||||
width: windowState.width,
|
||||
height: windowState.height
|
||||
});
|
||||
|
||||
windowState.manage(mainWindow);
|
||||
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
});
|
||||
|
||||
mainWindow.on('close', () => {
|
||||
windowState.saveState(mainWindow);
|
||||
});
|
||||
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
contextMenu({
|
||||
showLookUpSelection: false,
|
||||
showSearchWithGoogle: false,
|
||||
showCopyImage: false,
|
||||
prepend: (defaultActions, params, browserWindow) => [
|
||||
{
|
||||
label: 'Make App 💻'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
function loadVite(port) {
|
||||
mainWindow?.loadURL(`http://localhost:${port}`).catch((e) => {
|
||||
console.log('Error loading URL, retrying', e);
|
||||
setTimeout(() => {
|
||||
loadVite(port);
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
function createMainWindow() {
|
||||
mainWindow = createWindow();
|
||||
mainWindow.once('close', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
if (dev) loadVite(port);
|
||||
else serveURL(mainWindow);
|
||||
}
|
||||
|
||||
app.once('ready', createMainWindow);
|
||||
app.on('activate', () => {
|
||||
if (!mainWindow) {
|
||||
createMainWindow();
|
||||
}
|
||||
});
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
|
||||
ipcMain.on('to-main', (event, count) => {
|
||||
return mainWindow?.webContents.send('from-main', `next count is ${count + 1}`);
|
||||
});
|
||||
|
||||
ipcMain.handle('get-installed-packages', async () => {
|
||||
console.log('get installed pkgs: ipc');
|
||||
const pkgs = await getInstalledPackages();
|
||||
return pkgs;
|
||||
});
|
||||
|
||||
ipcMain.handle('get-session', async () => {
|
||||
console.log('get session');
|
||||
const session = await readSessionData();
|
||||
console.log('session:', session);
|
||||
return session;
|
||||
});
|
||||
|
||||
ipcMain.handle('update-session', async (_, data) => {
|
||||
await writeSessionData(data as Session);
|
||||
});
|
||||
|
||||
ipcMain.handle('install-package', async (_, data) => {
|
||||
const result = await installPackage(data.full_name);
|
||||
return result;
|
||||
});
|
46
modules/desktop/electron/libs/auth.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { mkdirp } from 'mkdirp';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { getTeaPath } from './teaDir';
|
||||
import * as v1Client from './v1Client';
|
||||
|
||||
const sessionFilePath = path.join(getTeaPath(), 'tea.xyz/gui/tmp.dat');
|
||||
const sessionFolder = path.join(getTeaPath(), 'tea.xyz/gui');
|
||||
|
||||
interface Session {
|
||||
device_id?: string;
|
||||
key?: string;
|
||||
user?: any;
|
||||
}
|
||||
|
||||
export async function initSessionData() {
|
||||
fs.readFileSync(sessionFilePath);
|
||||
|
||||
await mkdirp(sessionFolder);
|
||||
const req = await v1Client.get<{ deviceId: string }>('/auth/registerDevice');
|
||||
}
|
||||
|
||||
export async function readSessionData(): Promise<Session> {
|
||||
try {
|
||||
const sessionBuffer = await fs.readFileSync(sessionFilePath);
|
||||
const session = JSON.parse(sessionBuffer.toString()) as Session;
|
||||
return session;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const req = await v1Client.get<{ deviceId: string }>('/auth/registerDevice');
|
||||
const data = { device_id: req.deviceId };
|
||||
await writeSessionData(data);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeSessionData(data: Session) {
|
||||
try {
|
||||
await mkdirp(sessionFolder);
|
||||
await fs.writeFileSync(sessionFilePath, JSON.stringify(data), {
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
29
modules/desktop/electron/libs/cli.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { spawn } from 'child_process';
|
||||
import { clean } from 'semver';
|
||||
|
||||
export async function installPackage(full_name: string) {
|
||||
return await new Promise((resolve, reject) => {
|
||||
let version = '';
|
||||
let lastError = '';
|
||||
const teaInstallation = spawn('tea', [`+${full_name}`, 'true']);
|
||||
|
||||
teaInstallation.stdout.on('data', (data) => {
|
||||
console.log('stdout:', data);
|
||||
});
|
||||
|
||||
teaInstallation.stderr.on('data', (err) => {
|
||||
lastError = err.toString();
|
||||
if (lastError && lastError.includes('installed') && lastError.includes(full_name)) {
|
||||
version = lastError.split('/').pop() || '';
|
||||
}
|
||||
});
|
||||
|
||||
teaInstallation.on('exit', (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ version: clean(version) });
|
||||
} else {
|
||||
reject(new Error(lastError));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
94
modules/desktop/electron/libs/teaDir.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
// import { readDir, BaseDirectory } from '@tauri-apps/api/fs';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { app } from 'electron';
|
||||
import semver from 'semver';
|
||||
|
||||
type Dir = {
|
||||
name: string;
|
||||
path: string;
|
||||
children?: Dir[];
|
||||
};
|
||||
|
||||
export const getTeaPath = () => {
|
||||
const homePath = app.getPath('home');
|
||||
const teaPath = path.join(homePath, './.tea');
|
||||
return teaPath;
|
||||
};
|
||||
|
||||
export async function getInstalledPackages() {
|
||||
const pkgsPath = getTeaPath();
|
||||
|
||||
const folders = await deepReadDir({
|
||||
dir: pkgsPath,
|
||||
continueDeeper: (name: string) => !semver.valid(name),
|
||||
filter: (name: string) => !!semver.valid(name)
|
||||
});
|
||||
|
||||
const pkgs = folders
|
||||
.map((p: string) => p.split('.tea/')[1])
|
||||
.filter((p: string) => !p.includes('tea.xyz'))
|
||||
.map((p: string) => {
|
||||
const path = p.trim().split('/');
|
||||
const version = path.pop();
|
||||
return {
|
||||
version: semver.clean(version || ''),
|
||||
full_name: path.join('/')
|
||||
};
|
||||
});
|
||||
|
||||
return pkgs;
|
||||
}
|
||||
|
||||
const semverTest =
|
||||
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/g;
|
||||
|
||||
export const getPkgBottles = (packageDir: Dir): string[] => {
|
||||
const bottles: string[] = [];
|
||||
|
||||
const pkg = packageDir.path.split('.tea/')[1];
|
||||
const version = pkg.split('/v')[1];
|
||||
|
||||
const isVersion = semverTest.test(version) || !isNaN(+version) || version === '*';
|
||||
|
||||
if (version && isVersion) {
|
||||
bottles.push(pkg);
|
||||
} else if (packageDir?.children?.length) {
|
||||
const childBottles = packageDir.children
|
||||
.map(getPkgBottles)
|
||||
.reduce((arr, bottles) => [...arr, ...bottles], []);
|
||||
bottles.push(...childBottles);
|
||||
}
|
||||
|
||||
return bottles.filter((b) => b !== undefined).sort(); // ie: ["gohugo.io/v*", "gohugo.io/v0", "gohugo.io/v0.108", "gohugo.io/v0.108.0"]
|
||||
};
|
||||
|
||||
const deepReadDir = async ({
|
||||
dir,
|
||||
continueDeeper,
|
||||
filter
|
||||
}: {
|
||||
dir: string;
|
||||
continueDeeper?: (name: string) => boolean;
|
||||
filter?: (name: string) => boolean;
|
||||
}) => {
|
||||
const arrayOfFiles: string[] = [];
|
||||
try {
|
||||
const files = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const f of files) {
|
||||
const nextPath = path.join(dir, f.name);
|
||||
const deeper = continueDeeper ? continueDeeper(f.name) : true;
|
||||
if (f.isDirectory() && deeper) {
|
||||
const nextFiles = await deepReadDir({ dir: nextPath, continueDeeper, filter });
|
||||
arrayOfFiles.push(...nextFiles);
|
||||
} else if (filter && filter(f.name)) {
|
||||
arrayOfFiles.push(nextPath);
|
||||
} else if (!filter) {
|
||||
arrayOfFiles.push(nextPath);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
return arrayOfFiles;
|
||||
};
|
17
modules/desktop/electron/libs/v1Client.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import axios from 'axios';
|
||||
import path from 'path';
|
||||
|
||||
const base = 'https://api.tea.xyz';
|
||||
export async function get<T>(urlPath: string) {
|
||||
const url = new URL(path.join('v1', urlPath), base).toString();
|
||||
// TODO: add headers
|
||||
const req = await axios.request<T>({
|
||||
method: 'GET',
|
||||
url,
|
||||
headers: {}
|
||||
});
|
||||
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export default get;
|
13
modules/desktop/electron/preload.cjs
Normal file
|
@ -0,0 +1,13 @@
|
|||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
send: (channel, data) => {
|
||||
ipcRenderer.send(channel, data);
|
||||
},
|
||||
sendSync: (channel, data) => {
|
||||
ipcRenderer.sendSync(channel, data);
|
||||
},
|
||||
receive: (channel, func) => {
|
||||
ipcRenderer.on(channel, (event, ...args) => func(...args));
|
||||
}
|
||||
});
|
38
modules/desktop/electron/vite.config.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { join } from 'path';
|
||||
|
||||
const PACKAGE_ROOT = __dirname;
|
||||
const PROJECT_ROOT = join(PACKAGE_ROOT, '../..');
|
||||
|
||||
/**
|
||||
* @type {import('vite').UserConfig}
|
||||
* @see https://vitejs.dev/config/
|
||||
*/
|
||||
const config = {
|
||||
root: PACKAGE_ROOT,
|
||||
envDir: PROJECT_ROOT,
|
||||
resolve: {
|
||||
alias: {
|
||||
'/@/': join(PACKAGE_ROOT, 'src') + '/'
|
||||
}
|
||||
},
|
||||
build: {
|
||||
ssr: true,
|
||||
sourcemap: 'inline',
|
||||
outDir: 'dist',
|
||||
assetsDir: '.',
|
||||
minify: process.env.MODE !== 'development',
|
||||
lib: {
|
||||
entry: 'electron.ts',
|
||||
formats: ['cjs']
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: '[name].cjs'
|
||||
}
|
||||
},
|
||||
emptyOutDir: true,
|
||||
reportCompressedSize: false
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
BIN
modules/desktop/icon.png
Normal file
After Width: | Height: | Size: 164 KiB |
105
modules/desktop/package.json
Normal file
|
@ -0,0 +1,105 @@
|
|||
{
|
||||
"name": "@tea/desktop",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "tea gui app",
|
||||
"author": "tea.xyz",
|
||||
"main": "electron/dist/electron.cjs",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=dev npm run dev:all",
|
||||
"dev:all": "concurrently -n=svelte,electron -c='#ff3e00',blue \"pnpm dev:main\" \"pnpm dev:svelte\" \"pnpm dev:electron\"",
|
||||
"dev:svelte": "vite dev",
|
||||
"dev:electron": "electron electron/dist/electron.cjs",
|
||||
"dev:main": "cd ./electron && vite build --config ./vite.config.js --watch",
|
||||
"build:main": "cd ./electron && vite --config ./vite.config.js build --base .",
|
||||
"pack": "electron-builder --dir --config electron-builder.config.cjs",
|
||||
"dist": "pnpm build && electron-builder --config electron-builder.config.cjs",
|
||||
"package": "pnpm build && electron-builder --config electron-builder.config.cjs",
|
||||
"dev:package": "pnpm build && electron-builder --config electron-builder.config.cjs --dir",
|
||||
"electron": "concurrently --kill-others \"vite dev\" \"electron electron/dist/electron.cjs\"",
|
||||
"olddev": "vite dev",
|
||||
"build": "pnpm build:main && vite build && cp build/app.html build/index.html",
|
||||
"preview": "vite preview",
|
||||
"unit:test": "vitest",
|
||||
"coverage": "vitest run --coverage",
|
||||
"test": "playwright test",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/notarize": "^1.2.3",
|
||||
"@playwright/experimental-ct-svelte": "^1.29.2",
|
||||
"@playwright/test": "1.25.0",
|
||||
"@sveltejs/adapter-auto": "^1.0.0",
|
||||
"@sveltejs/adapter-node": "^1.0.0-next.101",
|
||||
"@sveltejs/adapter-static": "^1.0.0-next.48",
|
||||
"@sveltejs/kit": "^1.0.0-next.562",
|
||||
"@tea/ui": "workspace:*",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/svelte": "^3.2.2",
|
||||
"@types/testing-library__jest-dom": "^5.14.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
||||
"@typescript-eslint/parser": "^5.27.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"concurrently": "^7.6.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "22.1.0",
|
||||
"electron-builder": "^23.6.0",
|
||||
"electron-reloader": "^1.2.3",
|
||||
"eslint": "^8.16.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"jsdom": "^21.0.0",
|
||||
"postcss": "^8.4.19",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-svelte": "^2.7.0",
|
||||
"prettier-plugin-tailwindcss": "^0.2.0",
|
||||
"svelte": "^3.55.1",
|
||||
"svelte-check": "^2.8.0",
|
||||
"svelte-preprocess": "^5.0.1",
|
||||
"svelte2tsx": "^0.5.20",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"tslib": "^2.3.1",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^4.0.0",
|
||||
"vitest": "^0.28.3"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@electron/asar": "^3.2.3",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@vitest/coverage-c8": "^0.27.1",
|
||||
"axios": "^1.3.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"buffer": "^6.0.3",
|
||||
"electron-context-menu": "^3.6.1",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-serve": "^1.1.0",
|
||||
"electron-vite": "^1.0.18",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"fuse.js": "^6.6.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lorem-ipsum": "^2.0.8",
|
||||
"mkdirp": "^2.1.3",
|
||||
"semver": "^7.3.8",
|
||||
"svelte-markdown": "^0.2.3",
|
||||
"svelte-watch-resize": "^1.0.3",
|
||||
"upath": "^2.0.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@tea/ui"
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
"appId": "xyz.tea.gui",
|
||||
"productName": "tea",
|
||||
"linux": {
|
||||
"icon": "./icon.png"
|
||||
}
|
||||
},
|
||||
"homepage": "https://tea.xyz",
|
||||
"repository": "https://github.com/teaxyz/gui.git"
|
||||
}
|
10
modules/desktop/playwright.config.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
webServer: {
|
||||
command: 'npm run build && npm run preview',
|
||||
port: 4173
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
3718
modules/desktop/pnpm-lock.yaml
Normal file
12
modules/desktop/postcss.config.cjs
Normal file
|
@ -0,0 +1,12 @@
|
|||
const { theme, plugins } = require('@tea/ui/tailwind.config.cjs');
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {
|
||||
content: ['./src/**/*.{html,svelte,ts,js}', '../ui/src/**/*.{html,svelte,ts,js}'],
|
||||
theme,
|
||||
plugins: [...plugins]
|
||||
},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
BIN
modules/desktop/screenshot.png
Normal file
After Width: | Height: | Size: 30 KiB |
4
modules/desktop/setupTest.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import matchers from '@testing-library/jest-dom/matchers';
|
||||
import { expect } from 'vitest';
|
||||
|
||||
expect.extend(matchers);
|
43
modules/desktop/src/app.css
Normal file
|
@ -0,0 +1,43 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: 'pp-neue-machina';
|
||||
src: url('/fonts/PPNeueMachina-InktrapLight.woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'sono';
|
||||
src: url('/fonts/Sono-Light.woff2');
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: #1a1a1a;
|
||||
color: #fff;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
font-family: sono, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
.text-primary,
|
||||
header,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
button,
|
||||
.click-copy {
|
||||
font-family: 'pp-neue-machina' !important;
|
||||
}
|
||||
|
||||
.pk-version {
|
||||
font-family: 'sono';
|
||||
}
|
9
modules/desktop/src/app.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
declare namespace App {
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
}
|
12
modules/desktop/src/app.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body>
|
||||
<div>%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
9
modules/desktop/src/components/Badges/Badges.svelte
Normal file
|
@ -0,0 +1,9 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
import Placeholder from '$components/Placeholder/Placeholder.svelte';
|
||||
|
||||
export let arg1: string;
|
||||
</script>
|
||||
|
||||
<Placeholder label="Badges" />
|
||||
<h1>{arg1 || 'tes'}</h1>
|
|
@ -0,0 +1,5 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
</script>
|
||||
|
||||
<section class="border-gray h-56 border bg-black" />
|
|
@ -0,0 +1,24 @@
|
|||
<script lang="ts">
|
||||
import PanelHeader from '@tea/ui/PanelHeader/PanelHeader.svelte';
|
||||
import MiniPackageCard from '@tea/ui/MiniPackageCard/MiniPackageCard.svelte';
|
||||
import type { Category } from '$libs/types';
|
||||
import { onMount } from 'svelte';
|
||||
import { getCategorizedPackages } from '@api';
|
||||
|
||||
let categories: Category[] = [];
|
||||
|
||||
onMount(async () => {
|
||||
categories = await getCategorizedPackages();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#each categories as category}
|
||||
<PanelHeader ctaLabel={category.cta_label} ctaLink={'#'} title={category.label} />
|
||||
<ul class="border-gray grid grid-cols-3 border border-r-0 bg-black">
|
||||
{#each category.packages as pkg}
|
||||
<div class="border-gray border border-t-0 border-l-0 p-4">
|
||||
<MiniPackageCard {pkg} ctaLabel="DETAILS" link={`/packages/${pkg.slug}`} />
|
||||
</div>
|
||||
{/each}
|
||||
</ul>
|
||||
{/each}
|
27
modules/desktop/src/components/CliBanner/CliBanner.svelte
Normal file
|
@ -0,0 +1,27 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
import Button from '@tea/ui/Button/Button.svelte';
|
||||
let copyButtonText = 'COPY';
|
||||
const copyValue = `sh <(curl https://tea.xyz)`;
|
||||
|
||||
const onCopy = () => {
|
||||
copyButtonText = 'COPIED!';
|
||||
navigator.clipboard.writeText(copyValue);
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="border-gray mt-4 border bg-black">
|
||||
<header class="flex flex-col items-center py-8">
|
||||
<figure>
|
||||
<img alt="tea" src="/images/tea-icon.png" class="rounded-md" />
|
||||
</figure>
|
||||
<p class="text-primary">tea.cli ver. 0.6.0</p>
|
||||
</header>
|
||||
<footer class="border-gray flex h-20 border-t text-white">
|
||||
<input class="flex-grow bg-black pl-4" disabled value="sh <(curl tea.xyz)>" />
|
||||
<Button class="w-16 border-0 border-l-2 text-sm" onClick={onCopy}>{copyButtonText}</Button>
|
||||
<Button class="w-56 border-0 border-l-2 text-sm" onClick={() => console.log('cli')}
|
||||
>OPEN IN TERMINAL</Button
|
||||
>
|
||||
</footer>
|
||||
</section>
|
|
@ -0,0 +1,24 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
import type { AirtablePost } from '@tea/ui/types';
|
||||
import Posts from '@tea/ui/Posts/Posts.svelte';
|
||||
import PanelHeader from '@tea/ui/PanelHeader/PanelHeader.svelte';
|
||||
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
|
||||
import { postsStore } from '$libs/stores';
|
||||
|
||||
export let title = 'Workshops';
|
||||
export let ctaLabel = 'View all';
|
||||
|
||||
let courses: AirtablePost[] = [];
|
||||
|
||||
postsStore.subscribeByTag('course', (posts) => (courses = posts));
|
||||
</script>
|
||||
|
||||
<PanelHeader {title} {ctaLabel} ctaLink="/" />
|
||||
{#if courses.length}
|
||||
<Posts posts={courses} linkTarget="_blank" />
|
||||
{:else}
|
||||
<section class="border-gray h-64 border bg-black p-4">
|
||||
<Preloader />
|
||||
</section>
|
||||
{/if}
|
|
@ -0,0 +1,31 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
import { postsStore } from '$libs/stores';
|
||||
import type { Course } from '$libs/types';
|
||||
|
||||
import Gallery from '@tea/ui/Gallery/Gallery.svelte';
|
||||
|
||||
let courses: Course[] = [];
|
||||
|
||||
postsStore.subscribeByTag('featured_course', (posts) => {
|
||||
courses = posts.map((post) => {
|
||||
return {
|
||||
title: post.title,
|
||||
sub_title: post.sub_title,
|
||||
banner_image_url: post.thumb_image_url,
|
||||
link: post.link
|
||||
} as Course;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<Gallery
|
||||
title="FEATURED COURSES"
|
||||
items={courses.map((course) => ({
|
||||
title: course.title,
|
||||
subTitle: course.sub_title,
|
||||
imageUrl: course.banner_image_url,
|
||||
link: course.link
|
||||
}))}
|
||||
linkTarget="_blank"
|
||||
/>
|
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Package } from '@tea/ui/types';
|
||||
|
||||
import Gallery from '@tea/ui/Gallery/Gallery.svelte';
|
||||
import {
|
||||
featuredPackages as featuredPackagesStore,
|
||||
initializeFeaturedPackages
|
||||
} from '$libs/stores';
|
||||
|
||||
let featuredPackages: Package[] = [];
|
||||
|
||||
featuredPackagesStore.subscribe((v) => {
|
||||
featuredPackages = v;
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (!featuredPackages.length) {
|
||||
initializeFeaturedPackages();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Gallery
|
||||
title="FEATURED PACKAGES"
|
||||
items={featuredPackages.map((pkg) => ({
|
||||
title: pkg.full_name,
|
||||
subTitle: pkg.maintainer || '',
|
||||
imageUrl: pkg.thumb_image_url,
|
||||
link: `/packages/${pkg.slug}`
|
||||
}))}
|
||||
/>
|
|
@ -0,0 +1,85 @@
|
|||
<script lang="ts">
|
||||
import Button from '@tea/ui/Button/Button.svelte';
|
||||
</script>
|
||||
|
||||
<footer class="font-machina relative h-auto w-full bg-black">
|
||||
<section class="p-4 px-16 py-16">
|
||||
<h3 class="text-primary mb-5 text-2xl">QUICK LINKS</h3>
|
||||
<menu class="flex gap-4">
|
||||
<div class="border-gray flex-grow border border-l-0 border-r-0">
|
||||
<a href="/">
|
||||
<Button>
|
||||
<div class="text-primary flex justify-between hover:text-black">
|
||||
<div class="uppercase">About the tea store</div>
|
||||
<div>→</div>
|
||||
</div>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
<div class="border-gray flex-grow border border-l-0 border-r-0">
|
||||
<a href="/">
|
||||
<Button>
|
||||
<div class="text-primary flex justify-between hover:text-black">
|
||||
<div class="uppercase">REPORT A PROBLEM</div>
|
||||
<div>→</div>
|
||||
</div>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
<div class="border-gray flex-grow border border-l-0 border-r-0">
|
||||
<a href="https://tea.xyz" target="_blank" rel="noreferrer">
|
||||
<Button>
|
||||
<div class="text-primary flex justify-between hover:text-black">
|
||||
<div class="uppercase">VISIT TEA.XYZ</div>
|
||||
<div>→</div>
|
||||
</div>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</menu>
|
||||
</section>
|
||||
|
||||
<section class="border-gray h-16 border border-r-0 p-4 px-16">
|
||||
<div class="text-gray flex gap-4 text-xs">
|
||||
<a
|
||||
href="https://tea.xyz/terms-of-use/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="hover:text-white"
|
||||
>
|
||||
TERMS & SERVICES
|
||||
</a>
|
||||
<a
|
||||
href="https://tea.xyz/privacy-policy/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="hover:text-white"
|
||||
>
|
||||
PRIVACY POLICY
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
h3 {
|
||||
font-family: 'pp-neue-machina', sans-serif;
|
||||
color: #00ffd0;
|
||||
}
|
||||
|
||||
p,
|
||||
.nav-item {
|
||||
font-family: 'sono', sans-serif;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
font-family: 'pp-neue-machina', sans-serif;
|
||||
text-transform: uppercase;
|
||||
transition: 0.1s ease-in;
|
||||
}
|
||||
|
||||
.list-group-item:hover {
|
||||
padding-left: 1vw;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,45 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
import ArticleCard from '@tea/ui/ArticleCard/ArticleCard.svelte';
|
||||
|
||||
const doStuff = () => {
|
||||
console.log('do stuff!');
|
||||
};
|
||||
</script>
|
||||
|
||||
<header class="border-gray text-primary border bg-black p-4">GETTING STARTED WITH TEA</header>
|
||||
<section class="grid grid-cols-3 bg-black">
|
||||
<div class="border-gray border p-4">
|
||||
<ArticleCard
|
||||
content={{
|
||||
title: 'installing tea',
|
||||
copy: "It's time to take your first sip! Click below to visit our tea-cli documentation page.",
|
||||
img_url: '/images/bored-ape.png',
|
||||
cta_label: 'Get Started',
|
||||
link: '/cli'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="border-gray border p-4">
|
||||
<ArticleCard
|
||||
content={{
|
||||
title: 'authenticating',
|
||||
copy: 'Using tea without authenticating is like playing a video game without the DLC. Join us today!',
|
||||
img_url: '/images/bored-ape.png',
|
||||
cta_label: 'Get Started',
|
||||
link: ''
|
||||
}}
|
||||
onClick={doStuff}
|
||||
/>
|
||||
</div>
|
||||
<div class="border-gray border p-4">
|
||||
<ArticleCard
|
||||
content={{
|
||||
title: 'give us a star',
|
||||
copy: 'Revolutions are built on the will of the people. Show your support for a more equitable internet.',
|
||||
img_url: '/images/bored-ape.png',
|
||||
cta_label: 'Get Started'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
|
@ -0,0 +1,38 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
import type { GUIPackage } from '$libs/types';
|
||||
import { PackageStates } from '$libs/types';
|
||||
import PanelHeader from '@tea/ui/PanelHeader/PanelHeader.svelte';
|
||||
import { packagesStore } from '$libs/stores';
|
||||
import MiniPackageCard from '@tea/ui/MiniPackageCard/MiniPackageCard.svelte';
|
||||
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
|
||||
let packages: GUIPackage[] = [];
|
||||
|
||||
packagesStore.subscribe((v) => {
|
||||
packages = v.filter((p) => p.state === PackageStates.INSTALLED);
|
||||
});
|
||||
</script>
|
||||
|
||||
<PanelHeader title="My installs" ctaLabel="Check for updates >" ctaLink="#" />
|
||||
|
||||
<ul class="border-gray grid grid-cols-3 border border-r-0 bg-black">
|
||||
{#if packages.length > 0}
|
||||
{#each packages as pkg}
|
||||
<div class="border-gray border border-t-0 border-l-0 p-4">
|
||||
<MiniPackageCard
|
||||
{pkg}
|
||||
ctaLabel="DETAILS"
|
||||
onClickCTA={async () => {
|
||||
console.log('do something with:', pkg.full_name);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each Array(12) as _}
|
||||
<section class="h-50 border-gray border p-4">
|
||||
<Preloader />
|
||||
</section>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
21
modules/desktop/src/components/News/News.svelte
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
import { postsStore } from '$libs/stores';
|
||||
import type { AirtablePost } from '@tea/ui/types';
|
||||
import Posts from '@tea/ui/Posts/Posts.svelte';
|
||||
import PanelHeader from '@tea/ui/PanelHeader/PanelHeader.svelte';
|
||||
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
|
||||
|
||||
let news: AirtablePost[] = [];
|
||||
|
||||
postsStore.subscribeByTag('news', (posts) => (news = posts));
|
||||
</script>
|
||||
|
||||
<PanelHeader title="Open-source News" ctaLabel="Read more articles >" ctaLink="/" />
|
||||
{#if news.length}
|
||||
<Posts posts={news} linkTarget="_blank" />
|
||||
{:else}
|
||||
<section class="border-gray h-64 border bg-black p-4">
|
||||
<Preloader />
|
||||
</section>
|
||||
{/if}
|
|
@ -0,0 +1,52 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
import '@tea/ui/icons/icons.css';
|
||||
import type { Package, Bottle } from '@tea/ui/types';
|
||||
import Button from '@tea/ui/Button/Button.svelte';
|
||||
import StarRating from '@tea/ui/StarRating/StarRating.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { getPackageBottles } from '@api';
|
||||
|
||||
export let pkg: Package;
|
||||
let bottles: Bottle[] = [];
|
||||
let packageRating = 0;
|
||||
let copyButtonText = 'COPY';
|
||||
const copyValue = `sh <(curl tea.xyz ) +${pkg.full_name}`;
|
||||
|
||||
const onCopy = () => {
|
||||
copyButtonText = 'COPIED!';
|
||||
navigator.clipboard.writeText(copyValue);
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
bottles = await getPackageBottles(pkg.full_name);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="border-gray mt-4 border bg-black">
|
||||
<header class="flex p-2">
|
||||
<figure class="grow-1 w-1/3">
|
||||
<img width={260} src={pkg.thumb_image_url} alt={pkg.full_name} />
|
||||
</figure>
|
||||
<article class="w-2/3 p-4 pt-8">
|
||||
<h3 class="text-primary text-3xl">{pkg.full_name}</h3>
|
||||
<h3>• {pkg.maintainer || ''}{pkg.maintainer ? ' |' : ''} {bottles.length} bottles</h3>
|
||||
<div class="mt-4">
|
||||
<StarRating maxRating={5} rating={packageRating} />
|
||||
</div>
|
||||
<p class="font-sono mt-4 text-sm">{pkg.desc}</p>
|
||||
</article>
|
||||
</header>
|
||||
|
||||
<footer class="border-gray flex h-20 border-t text-white">
|
||||
<input class="click-copy flex-grow bg-black pl-4" disabled value={copyValue} />
|
||||
<Button class="w-16 border-0 border-l-2 text-sm" onClick={onCopy}>{copyButtonText}</Button>
|
||||
<Button class="w-56 border-0 border-l-2 text-sm" onClick={() => console.log('cli')}
|
||||
>OPEN IN TERMINAL</Button
|
||||
>
|
||||
</footer>
|
||||
</section>
|
|
@ -0,0 +1,8 @@
|
|||
<script lang="ts">
|
||||
import type { Package } from '@tea/ui/types';
|
||||
export let pkg: Package;
|
||||
</script>
|
||||
|
||||
<section class="h-64 w-full">
|
||||
<h1>{pkg.full_name}</h1>
|
||||
</section>
|
|
@ -0,0 +1,56 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
import { afterUpdate } from 'svelte';
|
||||
import ReviewCard from '@tea/ui/ReviewCard/ReviewCard.svelte';
|
||||
|
||||
import type { Review } from '@tea/ui/types';
|
||||
export let reviews: Review[];
|
||||
|
||||
export let showLimit = 9;
|
||||
let showMore = false;
|
||||
|
||||
const getColReviews = (n: number) => {
|
||||
const showReviews = reviews.filter((_item, i) => (i - n) % 3 === 0);
|
||||
return showMore ? showReviews : showReviews.slice(0, showLimit / 3);
|
||||
};
|
||||
|
||||
let col1: Review[] = [];
|
||||
let col2: Review[] = [];
|
||||
let col3: Review[] = [];
|
||||
|
||||
afterUpdate(() => {
|
||||
col1 = getColReviews(0);
|
||||
col2 = getColReviews(1);
|
||||
col3 = getColReviews(2);
|
||||
});
|
||||
|
||||
// TODO: problem with reviews with differing heights
|
||||
// ideally they should work like metro-ui to not have extreme height diff between columns
|
||||
</script>
|
||||
|
||||
<header class="border-gray text-primary border bg-black p-4">REVIEWS ({reviews.length})</header>
|
||||
<section class="font-machina flex flex-row flex-wrap bg-black">
|
||||
<div class="border-gray w-1/3 border-0 border-l-2 border-b-2 p-4">
|
||||
{#each col1 as review}
|
||||
<ReviewCard {review} />
|
||||
<div class="mt-4" />
|
||||
{/each}
|
||||
</div>
|
||||
<div class="border-gray w-1/3 border-0 border-l-2 border-b-2 p-4">
|
||||
{#each col2 as review}
|
||||
<ReviewCard {review} />
|
||||
<div class="mt-4" />
|
||||
{/each}
|
||||
</div>
|
||||
<div class="border-gray w-1/3 border-0 border-x-2 border-b-2 p-4">
|
||||
{#each col3 as review}
|
||||
<ReviewCard {review} />
|
||||
<div class="mt-4" />
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{#if showLimit <= reviews.length && showMore === false}
|
||||
<footer class="border-gray border bg-black p-4">
|
||||
<button on:click={() => (showMore = true)}>SHOW MORE</button>
|
||||
</footer>
|
||||
{/if}
|
64
modules/desktop/src/components/Packages/Packages.svelte
Normal file
|
@ -0,0 +1,64 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
import type { GUIPackage } from '$libs/types';
|
||||
import { PackageStates } from '$libs/types';
|
||||
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
|
||||
import PackageCard from '@tea/ui/PackageCard/PackageCard.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
// TODO: replace with getting foundation essentials
|
||||
import { getTopPackages } from '$libs/api/mock';
|
||||
import { installPackage } from '@api';
|
||||
|
||||
export let title = 'Packages';
|
||||
|
||||
let packages: GUIPackage[] = [];
|
||||
|
||||
const getCTALabel = (state: PackageStates): string => {
|
||||
return {
|
||||
[PackageStates.AVAILABLE]: 'INSTALL',
|
||||
[PackageStates.INSTALLED]: 'INSTALLED',
|
||||
[PackageStates.INSTALLING]: 'INSTALLING',
|
||||
[PackageStates.UNINSTALLED]: 'RE-INSTALL'
|
||||
}[state];
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if (!packages.length) {
|
||||
packages = await getTopPackages();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<header class="border-gray text-primary flex items-center justify-between border bg-black p-4">
|
||||
<span>{title}</span>
|
||||
<a href="/packages" class="font-sono text-sm underline">View all packages</a>
|
||||
</header>
|
||||
<ul class="grid grid-cols-3 bg-black">
|
||||
{#if packages.length > 0}
|
||||
{#each packages as pkg}
|
||||
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
|
||||
<PackageCard
|
||||
{pkg}
|
||||
link={`/packages/${pkg.slug}`}
|
||||
ctaLabel={getCTALabel(pkg.state)}
|
||||
onClickCTA={async () => {
|
||||
try {
|
||||
pkg.state = PackageStates.INSTALLING;
|
||||
await installPackage(pkg.full_name);
|
||||
pkg.state = PackageStates.INSTALLED;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each Array(9) as _}
|
||||
<section class="h-50 border-gray border p-4">
|
||||
<Preloader />
|
||||
</section>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
14
modules/desktop/src/components/PageHeader/PageHeader.svelte
Normal file
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
export let coverUrl = '';
|
||||
let clazz = '';
|
||||
export { clazz as class };
|
||||
</script>
|
||||
|
||||
<figure class={`font-machina relative mb-8 h-32 w-full uppercase ${clazz}`}>
|
||||
{#if coverUrl}
|
||||
<img src={coverUrl} class="absolute z-0 h-32 w-full object-cover" alt="cover" />
|
||||
{/if}
|
||||
<div class="text-primary absolute bottom-0 z-10 text-6xl leading-[32px]">
|
||||
<slot />
|
||||
</div>
|
||||
</figure>
|
|
@ -0,0 +1,24 @@
|
|||
<script lang="ts">
|
||||
export let label = '';
|
||||
</script>
|
||||
|
||||
<section class="bg-gray p-8">
|
||||
<header>{label}</header>
|
||||
<slot />
|
||||
</section>
|
||||
|
||||
<style>
|
||||
section {
|
||||
position: relative;
|
||||
min-height: 240px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
/* background-color: #ccc; */
|
||||
display: flex;
|
||||
}
|
||||
header {
|
||||
color: rgb(50, 48, 48);
|
||||
font-size: 3em;
|
||||
}
|
||||
</style>
|
34
modules/desktop/src/components/Preflight/Preflight.svelte
Normal file
|
@ -0,0 +1,34 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
const openGithub = () => {
|
||||
// open('https://github.com/teaxyz');
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="card social-box" style="width: 100%; float:right;">
|
||||
<header class="border-gray text-primary border-b pt-7 pb-7 pl-5">PRE-FLIGHT</header>
|
||||
<div class="listbox-item border-gray border-b p-6">
|
||||
<a href="/cli">
|
||||
<p>Install Tea</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="listbox-item border-gray border-b p-6">
|
||||
<div>
|
||||
<p>Authenticate</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="listbox-item p-6">
|
||||
<button on:click={openGithub}>Give tea a star</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
border: 2px solid #949494;
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.listbox-item {
|
||||
height: 75px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
import { authStore } from '$libs/stores';
|
||||
import type { Developer } from '@tea/ui/types';
|
||||
|
||||
let user: Developer | null = null;
|
||||
const authPage = `http://localhost:3000/v1/auth/user?device_id=${authStore.deviceId}`; // https://api.tea.xyz/v1/auth/user?device_id=device_id
|
||||
|
||||
authStore.subscribe((u) => (user = u));
|
||||
</script>
|
||||
|
||||
{#if user}
|
||||
<section class="border-gray border-2 bg-black p-2">
|
||||
<div class="profile_banner border-gray container flex border bg-black">
|
||||
<img class="w-1/5" src={user.avatar_url || '/images/bored-ape.png'} alt="profile" />
|
||||
<div class="flex w-4/5 items-center p-5">
|
||||
<div class="w-1/2 pl-5">
|
||||
<p class="text-gray uppercase">Authenticated with GitHub</p>
|
||||
<p />
|
||||
<p class="text-primary text-4xl">@{user.login}</p>
|
||||
</div>
|
||||
<div class="border-gray h-full border-l" />
|
||||
<div class="w-1/2 pl-10">
|
||||
<p class="text-gray uppercase leading-loose">
|
||||
Country: <span>{user?.country}</span><br />Wallet:
|
||||
{#if user.wallet}
|
||||
<span>{user.wallet}</span>
|
||||
{:else}
|
||||
<a class="text-green underline" href="/">Connect Now</a>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
|
@ -0,0 +1,103 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
import { packagesStore } from '$libs/stores';
|
||||
import SortingButtons from './SortingButtons.svelte';
|
||||
import type { GUIPackage } from '$libs/types';
|
||||
import { PackageStates } from '$libs/types';
|
||||
import PackageCard from '@tea/ui/PackageCard/PackageCard.svelte';
|
||||
import SearchInput from '@tea/ui/SearchInput/SearchInput.svelte';
|
||||
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
|
||||
|
||||
import { installPackage } from '@api';
|
||||
|
||||
let packages: GUIPackage[] = [];
|
||||
|
||||
let sortBy = 'popularity';
|
||||
let sortDirection: 'asc' | 'desc' = 'desc';
|
||||
|
||||
const searchLimit = 10;
|
||||
|
||||
const setPackages = (pkgs: GUIPackage[], isSearch?: boolean) => {
|
||||
packages = isSearch
|
||||
? pkgs
|
||||
: pkgs.sort((a, b) => {
|
||||
if (sortBy === 'popularity') {
|
||||
const aPop = +a.dl_count + a.installs;
|
||||
const bPop = +b.dl_count + b.installs;
|
||||
return sortDirection === 'asc' ? aPop - bPop : bPop - aPop;
|
||||
} else {
|
||||
// most recent
|
||||
const aDate = new Date(a.last_modified);
|
||||
const bDate = new Date(b.last_modified);
|
||||
return sortDirection === 'asc' ? +aDate - +bDate : +bDate - +aDate;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onSearch = async (term: string) => {
|
||||
if (term !== '' && term.length > 1) {
|
||||
const matchingPackages: GUIPackage[] = await packagesStore.search(term, searchLimit);
|
||||
setPackages(matchingPackages, true);
|
||||
} else {
|
||||
setPackages(packagesStore.packages, false);
|
||||
}
|
||||
};
|
||||
|
||||
const onSort = (opt: string, dir: 'asc' | 'desc') => {
|
||||
sortBy = opt;
|
||||
sortDirection = dir;
|
||||
setPackages(packages);
|
||||
};
|
||||
|
||||
const getCTALabel = (state: PackageStates): string => {
|
||||
return {
|
||||
[PackageStates.AVAILABLE]: 'INSTALL',
|
||||
[PackageStates.INSTALLED]: 'INSTALLED',
|
||||
[PackageStates.INSTALLING]: 'INSTALLING',
|
||||
[PackageStates.UNINSTALLED]: 'RE-INSTALL'
|
||||
}[state];
|
||||
};
|
||||
|
||||
packagesStore.subscribe(setPackages);
|
||||
</script>
|
||||
|
||||
<div class="border-gray border bg-black">
|
||||
<section class="flex items-center justify-between">
|
||||
<div>
|
||||
<SearchInput size="medium" {onSearch} />
|
||||
</div>
|
||||
<div class="pr-4">
|
||||
<section class="border-gray h-12 w-48 border">
|
||||
<SortingButtons {onSort} />
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<ul class="grid grid-cols-3">
|
||||
{#if packages.length > 0}
|
||||
{#each packages as pkg}
|
||||
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
|
||||
<PackageCard
|
||||
{pkg}
|
||||
link={`/packages/${pkg.slug}`}
|
||||
ctaLabel={getCTALabel(pkg.state)}
|
||||
onClickCTA={async () => {
|
||||
try {
|
||||
pkg.state = PackageStates.INSTALLING;
|
||||
await installPackage(pkg.full_name);
|
||||
pkg.state = PackageStates.INSTALLED;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each Array(12) as _}
|
||||
<section class="h-50 border-gray border p-4">
|
||||
<Preloader />
|
||||
</section>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
|
@ -0,0 +1,132 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
|
||||
export let onSort: (opt: string, dir: 'asc' | 'desc') => void;
|
||||
|
||||
let sortBy = 'popularity';
|
||||
let sortDirection: 'asc' | 'desc' = 'desc';
|
||||
|
||||
const sortOptions = ['popularity', 'most recent'];
|
||||
|
||||
const setSortBy = (opt: string) => {
|
||||
sortBy = opt;
|
||||
if (onSort) {
|
||||
onSort(sortBy, sortDirection);
|
||||
}
|
||||
};
|
||||
const setSortDir = (opt: string, dir: 'asc' | 'desc') => {
|
||||
sortDirection = dir;
|
||||
setSortBy(opt);
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="sorting-container font-machina bg-black text-white">
|
||||
<div class="dropdown">
|
||||
<div class="dropdown-title">SORT ORDER</div>
|
||||
<ul class="dropdown-content column flex">
|
||||
{#each sortOptions as option}
|
||||
<li class="flex items-center">
|
||||
<button
|
||||
class={`sort-btn uppercase ${sortBy === option ? 'active' : ''}`}
|
||||
on:click={() => setSortBy(option)}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
<div class="direction-arrows">
|
||||
<button
|
||||
on:click={() => setSortDir(option, 'asc')}
|
||||
class={sortBy === option && sortDirection === 'asc' ? 'active' : ''}>↑</button
|
||||
>
|
||||
<button
|
||||
on:click={() => setSortDir(option, 'desc')}
|
||||
class={sortBy === option && sortDirection === 'desc' ? 'active' : ''}>↓</button
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.direction-arrows {
|
||||
float: right;
|
||||
}
|
||||
.direction-arrows button {
|
||||
opacity: 0.3;
|
||||
}
|
||||
.direction-arrows button.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sorting-container {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
max-width: 240px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 34px;
|
||||
transition: 0.1s linear;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: #1a1a1a;
|
||||
width: 100%;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
|
||||
z-index: 1;
|
||||
color: white;
|
||||
list-style: none;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.dropdown-content li {
|
||||
position: relative;
|
||||
padding: 0px 10px;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.dropdown-content li .sort-btn {
|
||||
height: 100%;
|
||||
width: calc(100% - 40px);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.dropdown-content li .sort-btn.active {
|
||||
font-weight: bold;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dropdown-content li .direction-arrows {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.dropdown-content li:hover {
|
||||
background: #00ffd0;
|
||||
color: black;
|
||||
}
|
||||
.dropdown:hover .dropdown-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-title {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,163 @@
|
|||
<script lang="ts">
|
||||
import { searchStore } from '$libs/stores';
|
||||
import type { GUIPackage } from '$libs/types';
|
||||
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
|
||||
import PackageCard from '@tea/ui/PackageCard/PackageCard.svelte';
|
||||
import { PackageStates } from '$libs/types';
|
||||
import Posts from '@tea/ui/Posts/Posts.svelte';
|
||||
|
||||
import { installPackage } from '@api';
|
||||
import type { AirtablePost } from '@tea/ui/types';
|
||||
let term: string;
|
||||
let packages: GUIPackage[] = [];
|
||||
let articles: AirtablePost[] = []; // news, blogs, etc
|
||||
let workshops: AirtablePost[] = []; // workshops, course
|
||||
let loading = true;
|
||||
|
||||
searchStore.subscribe((v) => {
|
||||
term = v;
|
||||
});
|
||||
searchStore.packagesSearch.subscribe((pkgs) => {
|
||||
packages = pkgs;
|
||||
});
|
||||
searchStore.postsSearch.subscribe((posts) => {
|
||||
let partialArticles: AirtablePost[] = [];
|
||||
let partialWorkshops: AirtablePost[] = [];
|
||||
for (let post of posts) {
|
||||
if (post.tags.includes('news')) {
|
||||
partialArticles.push(post);
|
||||
}
|
||||
if (post.tags.includes('course') || post.tags.includes('featured_course')) {
|
||||
partialWorkshops.push(post);
|
||||
}
|
||||
}
|
||||
|
||||
articles = partialArticles;
|
||||
workshops = partialWorkshops;
|
||||
});
|
||||
|
||||
searchStore.searching.subscribe((v) => (loading = v));
|
||||
|
||||
const getCTALabel = (state: PackageStates): string => {
|
||||
return {
|
||||
[PackageStates.AVAILABLE]: 'INSTALL',
|
||||
[PackageStates.INSTALLED]: 'INSTALLED',
|
||||
[PackageStates.INSTALLING]: 'INSTALLING',
|
||||
[PackageStates.UNINSTALLED]: 'RE-INSTALL'
|
||||
}[state];
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
term = '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class={term ? 'show' : ''}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<figure on:click={onClose} />
|
||||
<div class="border-gray z-20 border bg-black">
|
||||
<header class="flex justify-between p-4">
|
||||
<div class="text-primary text-2xl">Showing search results for `{term}`</div>
|
||||
|
||||
<button on:click={onClose}>✕</button>
|
||||
</header>
|
||||
<menu class="bg-accent flex h-8 w-full gap-4 px-4 text-xs">
|
||||
<button>packages ({packages.length})</button>
|
||||
<button>articles ({articles.length})</button>
|
||||
<button>workshops ({workshops.length})</button>
|
||||
</menu>
|
||||
<header class="text-primary p-4 text-lg">
|
||||
Top Package Results ({packages.length})
|
||||
</header>
|
||||
<ul class="grid grid-cols-3">
|
||||
{#if packages.length > 0}
|
||||
{#each packages as pkg}
|
||||
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
|
||||
<PackageCard
|
||||
{pkg}
|
||||
link={`/packages/${pkg.slug}`}
|
||||
ctaLabel={getCTALabel(pkg.state)}
|
||||
onClickCTA={async () => {
|
||||
try {
|
||||
pkg.state = PackageStates.INSTALLING;
|
||||
await installPackage(pkg.full_name);
|
||||
pkg.state = PackageStates.INSTALLED;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{:else if loading}
|
||||
{#each Array(12) as _}
|
||||
<section class="h-50 border-gray border p-4">
|
||||
<Preloader />
|
||||
</section>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
<header class="text-primary p-4 text-lg">
|
||||
Top Article Results ({articles.length})
|
||||
</header>
|
||||
{#if articles.length}
|
||||
<Posts posts={articles} linkTarget="_blank" />
|
||||
{:else if loading}
|
||||
<section class="border-gray h-64 border bg-black p-4">
|
||||
<Preloader />
|
||||
</section>
|
||||
{/if}
|
||||
<header class="text-primary p-4 text-lg">
|
||||
Top Workshop Results ({workshops.length})
|
||||
</header>
|
||||
{#if workshops.length}
|
||||
<Posts posts={workshops} linkTarget="_blank" />
|
||||
{:else if loading}
|
||||
<section class="border-gray h-64 border bg-black p-4">
|
||||
<Preloader />
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
section {
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
left: 0px;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
opacity: 0%;
|
||||
overflow: hidden;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
section.show {
|
||||
padding: 36px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
section > figure {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
section.show {
|
||||
opacity: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
section > div {
|
||||
position: relative;
|
||||
height: 0%;
|
||||
transition: height 0.6s ease-in-out;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
section.show > div {
|
||||
height: 90%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,63 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
import type { GUIPackage } from '$libs/types';
|
||||
import type { Package } from '@tea/ui/types';
|
||||
import { PackageStates } from '$libs/types';
|
||||
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
|
||||
import PackageCard from '@tea/ui/PackageCard/PackageCard.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { installPackage } from '@api';
|
||||
import { packagesStore } from '$libs/stores';
|
||||
|
||||
export let pkg: Package;
|
||||
|
||||
let packages: GUIPackage[] = [];
|
||||
|
||||
const getCTALabel = (state: PackageStates): string => {
|
||||
return {
|
||||
[PackageStates.AVAILABLE]: 'INSTALL',
|
||||
[PackageStates.INSTALLED]: 'INSTALLED',
|
||||
[PackageStates.INSTALLING]: 'INSTALLING',
|
||||
[PackageStates.UNINSTALLED]: 'RE-INSTALL'
|
||||
}[state];
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if (!packages.length) {
|
||||
const matches = await packagesStore.search(pkg.desc, 4);
|
||||
packages = matches.filter((mp) => mp.full_name !== pkg.full_name).slice(0, 3);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<header class="border-gray text-primary flex items-center justify-between border bg-black p-4">
|
||||
<span>MORE LIKE THIS</span>
|
||||
</header>
|
||||
<ul class="grid grid-cols-3 bg-black">
|
||||
{#if packages.length > 0}
|
||||
{#each packages as pkg}
|
||||
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
|
||||
<PackageCard
|
||||
{pkg}
|
||||
link={`/packages/${pkg.slug}`}
|
||||
ctaLabel={getCTALabel(pkg.state)}
|
||||
onClickCTA={async () => {
|
||||
try {
|
||||
pkg.state = PackageStates.INSTALLING;
|
||||
await installPackage(pkg.full_name);
|
||||
pkg.state = PackageStates.INSTALLED;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each Array(9) as _}
|
||||
<section class="h-50 border-gray border p-4">
|
||||
<Preloader />
|
||||
</section>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
|
@ -0,0 +1,38 @@
|
|||
<script lang="ts">
|
||||
import { authStore } from '$libs/stores';
|
||||
import type { Developer } from '@tea/ui/types';
|
||||
import { baseUrl } from '$libs/v1Client';
|
||||
|
||||
const { shell } = window.require('electron');
|
||||
|
||||
let user: Developer | null = null;
|
||||
const deviceId = authStore.deviceIdStore;
|
||||
|
||||
const openGithub = () => {
|
||||
shell.openExternal(`${baseUrl}/auth/user?device_id=${$deviceId}`)
|
||||
try {
|
||||
authStore.pollSession();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
authStore.subscribe((u) => (user = u));
|
||||
</script>
|
||||
|
||||
{#if user}
|
||||
<a href="/profile">
|
||||
<section class="flex">
|
||||
<img width="40" height="40" src={user.avatar_url || '/images/bored-ape.png'} alt="profile" />
|
||||
<div class="text-gray p-2">@{user.login}</div>
|
||||
</section>
|
||||
</a>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<section class="flex" on:click={openGithub}>
|
||||
<figure class="p-2">
|
||||
<img width="28" height="28" src="/images/github.png" alt="profile" />
|
||||
</figure>
|
||||
<div class="text-gray p-2">Login</div>
|
||||
</section>
|
||||
{/if}
|
75
modules/desktop/src/components/TopBar/TopBar.svelte
Normal file
|
@ -0,0 +1,75 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { beforeUpdate } from 'svelte';
|
||||
import { searchStore } from '$libs/stores';
|
||||
import SearchInput from '@tea/ui/SearchInput/SearchInput.svelte';
|
||||
import { navStore } from '$libs/stores';
|
||||
|
||||
import ProfileNavButton from './ProfileNavButton.svelte';
|
||||
|
||||
let { nextPath, prevPath } = navStore;
|
||||
|
||||
const onSearch = (term: string) => {
|
||||
searchStore.search(term);
|
||||
};
|
||||
|
||||
let currentPath: string;
|
||||
beforeUpdate(async () => {
|
||||
currentPath = $page.url.pathname;
|
||||
});
|
||||
</script>
|
||||
|
||||
<header class="border-gray flex w-full border border-l-0 border-r-0">
|
||||
<a href="/">
|
||||
<img width="40" height="40" src="/images/tea-icon.png" alt="tea" />
|
||||
</a>
|
||||
<ul class="text-gray flex h-10 gap-4 pl-4 align-middle leading-10">
|
||||
<button on:click={navStore.back} class={$prevPath ? 'active' : ''}>←</button>
|
||||
<button on:click={navStore.next} class={$nextPath ? 'active' : ''}>→</button>
|
||||
</ul>
|
||||
<SearchInput
|
||||
class="flex-grow border border-none py-4"
|
||||
size="small"
|
||||
placeholder="search the tea store"
|
||||
{onSearch}
|
||||
/>
|
||||
<ul class="text-gray flex gap-4 pr-4 pt-2 align-middle">
|
||||
<button class="icon-filter hover:text-white" />
|
||||
<button class="icon-share hover:text-white" />
|
||||
<button class="icon-star-empty hover:text-white" />
|
||||
</ul>
|
||||
<ProfileNavButton />
|
||||
</header>
|
||||
<menu
|
||||
class="border-gray text-gray flex h-10 gap-4 border border-l-0 border-r-0 border-t-0 pl-4 align-middle leading-10"
|
||||
>
|
||||
<a href="/cli" class={currentPath === '/cli' ? 'active' : ''}>install teaCli</a>
|
||||
<a href="/documentation" class={currentPath === '/documentation' ? 'active' : ''}>documentation</a
|
||||
>
|
||||
<a href="/packages" class={currentPath === '/packages' ? 'active' : ''}>packages</a>
|
||||
<a href="https://github.com/teaxyz" target="_blank" rel="noreferrer">
|
||||
<i class="icon-star-empty" /> Github (5.2k)
|
||||
</a>
|
||||
</menu>
|
||||
|
||||
<style>
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
menu > a:hover {
|
||||
color: white;
|
||||
}
|
||||
menu a.active {
|
||||
color: white;
|
||||
}
|
||||
|
||||
ul button {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
ul button.active {
|
||||
color: white;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
49
modules/desktop/src/libs/__tests__/teaDir.test.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { getPkgBottles } from '../teaDir';
|
||||
|
||||
describe('teaDir module', () => {
|
||||
it('should getPkgBottles from nested Dir object/s', () => {
|
||||
const results = getPkgBottles({
|
||||
name: 'kkos',
|
||||
path: '/Users/x/.tea/github.com/kkos',
|
||||
children: [
|
||||
{ name: '.DS_Store', path: '/Users/x/.tea/github.com/kkos/.DS_Store' },
|
||||
{
|
||||
name: 'oniguruma',
|
||||
path: '/Users/x/.tea/github.com/kkos/oniguruma',
|
||||
children: [
|
||||
{ name: '.DS_Store', path: '/Users/x/.tea/github.com/kkos/oniguruma/.DS_Store' },
|
||||
{
|
||||
path: '/Users/x/.tea/github.com/kkos/oniguruma/v6',
|
||||
name: 'v6',
|
||||
children: [
|
||||
{ name: '.DS_Store', path: '/Users/x/.tea/github.com/kkos/oniguruma/v6/.DS_Store' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'v*',
|
||||
path: '/Users/x/.tea/github.com/kkos/oniguruma/v*',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
name: 'v6.9.8',
|
||||
path: '/Users/x/.tea/github.com/kkos/oniguruma/v6.9.8',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
name: 'v6.9',
|
||||
path: '/Users/x/.tea/github.com/kkos/oniguruma/v6.9',
|
||||
children: []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
expect(results).toEqual([
|
||||
'github.com/kkos/oniguruma/v*',
|
||||
'github.com/kkos/oniguruma/v6',
|
||||
'github.com/kkos/oniguruma/v6.9',
|
||||
'github.com/kkos/oniguruma/v6.9.8'
|
||||
]);
|
||||
});
|
||||
});
|
105
modules/desktop/src/libs/api/electron.ts
Normal file
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* this is the main api integration, anything added here
|
||||
* should be mock replicated in ./mock.ts
|
||||
* why? to make it easier to verify features without us always
|
||||
* going through
|
||||
* the build->download->install->test loop
|
||||
* thus saving us so much time
|
||||
*
|
||||
* primary concerns here are any method that does the following:
|
||||
* - connect to remote api(api.tea.xyz) and returns a data
|
||||
* - connect to a local platform api and returns a data
|
||||
*/
|
||||
import axios from 'axios';
|
||||
|
||||
import type { Package, Review, AirtablePost, Bottle } from '@tea/ui/types';
|
||||
import type { GUIPackage, Course, Category, DeviceAuth } from '../types';
|
||||
|
||||
import * as mock from './mock';
|
||||
import { PackageStates } from '../types';
|
||||
import { getInstalledPackages } from '$libs/teaDir';
|
||||
import { installPackageCommand } from '$libs/cli';
|
||||
|
||||
import { get as apiGet } from '$libs/v1Client';
|
||||
|
||||
export async function getPackages(): Promise<GUIPackage[]> {
|
||||
const [packages, installedPackages] = await Promise.all([
|
||||
apiGet<Package[]>('packages'),
|
||||
getInstalledPackages()
|
||||
]);
|
||||
|
||||
return (packages || []).map((pkg) => {
|
||||
const found = installedPackages.find((p) => p.full_name === pkg.full_name);
|
||||
return {
|
||||
...pkg,
|
||||
state: found ? PackageStates.INSTALLED : PackageStates.AVAILABLE,
|
||||
installed_version: found ? found.version : ''
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function getFeaturedPackages(): Promise<Package[]> {
|
||||
const packages = await mock.getFeaturedPackages();
|
||||
return packages;
|
||||
}
|
||||
|
||||
export async function getPackageReviews(full_name: string): Promise<Review[]> {
|
||||
console.log(`getting reviews for ${full_name}`);
|
||||
const reviews: Review[] = await apiGet<Review[]>(
|
||||
`packages/${full_name.replaceAll('/', ':')}/reviews`
|
||||
);
|
||||
|
||||
return reviews;
|
||||
}
|
||||
|
||||
export async function installPackage(full_name: string) {
|
||||
try {
|
||||
await installPackageCommand(full_name);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFeaturedCourses(): Promise<Course[]> {
|
||||
const posts = await apiGet<AirtablePost[]>('posts', { tag: 'featured_course' });
|
||||
return posts.map((post) => {
|
||||
return {
|
||||
title: post.title,
|
||||
sub_title: post.sub_title,
|
||||
banner_image_url: post.thumb_image_url,
|
||||
link: post.link
|
||||
} as Course;
|
||||
});
|
||||
}
|
||||
|
||||
export async function getTopPackages(): Promise<GUIPackage[]> {
|
||||
const packages = await mock.getTopPackages();
|
||||
return packages;
|
||||
}
|
||||
|
||||
export async function getAllPosts(tag?: string): Promise<AirtablePost[]> {
|
||||
// add filter here someday: tag = news | course
|
||||
const posts = await apiGet<AirtablePost[]>('posts', tag ? { tag } : {});
|
||||
return posts;
|
||||
}
|
||||
|
||||
export async function getCategorizedPackages(): Promise<Category[]> {
|
||||
const categories = await apiGet<Category[]>('/packages/categorized');
|
||||
return categories;
|
||||
}
|
||||
|
||||
export async function getDeviceAuth(deviceId: string): Promise<DeviceAuth> {
|
||||
const data = await apiGet<DeviceAuth>(`/auth/device/${deviceId}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getPackageBottles(packageName: string): Promise<Bottle[]> {
|
||||
console.log('getting bottles for ', packageName);
|
||||
const req = await axios.get(`https://app.tea.xyz/api/bottles/${packageName}`);
|
||||
return req.data as Bottle[];
|
||||
}
|
||||
|
||||
export async function registerDevice(): Promise<string> {
|
||||
const { deviceId } = await apiGet<{ deviceId: string }>('/auth/registerDevice');
|
||||
return deviceId;
|
||||
}
|
348
modules/desktop/src/libs/api/mock.ts
Normal file
|
@ -0,0 +1,348 @@
|
|||
/**
|
||||
* primarily used to make this desktop app work in the website preview setting in the CI/CD
|
||||
* may contain fake/mock data
|
||||
*
|
||||
* TODO:
|
||||
* * make cors work with api.tea.xyz/v1
|
||||
*/
|
||||
import type { Package, Review, AirtablePost, Bottle } from '@tea/ui/types';
|
||||
import type { GUIPackage, Course, Category } from '../types';
|
||||
import { PackageStates } from '../types';
|
||||
import { loremIpsum } from 'lorem-ipsum';
|
||||
import _ from 'lodash';
|
||||
// import { getInstalledPackages } from '$libs/teaDir';
|
||||
// import { getSession } from '$libs/stores/auth';
|
||||
import * as v1Client from '$libs/v1Client';
|
||||
|
||||
const packages: Package[] = [
|
||||
{
|
||||
slug: 'mesonbuild_com',
|
||||
homepage: 'https://mesonbuild.com',
|
||||
name: 'mesonbuild.com',
|
||||
version: '0.63.3',
|
||||
last_modified: '2022-10-06T15:45:08.000Z',
|
||||
full_name: 'mesonbuild.com',
|
||||
dl_count: 270745,
|
||||
thumb_image_name: 'mesonbuild_com_option 1.jpg ',
|
||||
maintainer: '',
|
||||
desc: 'Fast and user friendly build system',
|
||||
thumb_image_url: 'https://tea.xyz/Images/packages/mesonbuild_com.jpg',
|
||||
installs: 0
|
||||
},
|
||||
{
|
||||
slug: 'pixman_org',
|
||||
homepage: 'http://www.pixman.org/',
|
||||
maintainer: 'freedesktop',
|
||||
name: 'pixman.org',
|
||||
version: '0.40.0',
|
||||
last_modified: '2022-09-26T19:37:47.000Z',
|
||||
full_name: 'pixman.org',
|
||||
dl_count: 0,
|
||||
thumb_image_name: 'pixman_org_option 1.jpg ',
|
||||
desc: 'Pixman is a library that provides low-level pixel manipulation features such as image compositing and trapezoid rasterization.',
|
||||
thumb_image_url: 'https://tea.xyz/Images/packages/pixman_org.jpg',
|
||||
installs: 0
|
||||
},
|
||||
{
|
||||
slug: 'freedesktop_org_pkg_config',
|
||||
homepage: 'https://freedesktop.org',
|
||||
maintainer: 'freedesktop.org',
|
||||
name: 'pkg-config',
|
||||
version: '0.29.2',
|
||||
last_modified: '2022-10-20T01:32:15.000Z',
|
||||
full_name: 'freedesktop.org/pkg-config',
|
||||
dl_count: 2661501,
|
||||
thumb_image_name: 'freedecktop_org_pkg_config option 1.jpg ',
|
||||
desc: 'Manage compile and link flags for libraries',
|
||||
thumb_image_url: 'https://tea.xyz/Images/packages/freedesktop_org_pkg_config.jpg',
|
||||
installs: 0
|
||||
},
|
||||
{
|
||||
slug: 'gnu_org_gettext',
|
||||
homepage: 'https://gnu.org',
|
||||
maintainer: 'gnu.org',
|
||||
name: 'gettext',
|
||||
version: '0.21.1',
|
||||
last_modified: '2022-10-20T01:23:46.000Z',
|
||||
full_name: 'gnu.org/gettext',
|
||||
dl_count: 3715970,
|
||||
thumb_image_name: 'gnu_org_gettext_option 1.jpg ',
|
||||
desc: 'GNU internationalization (i18n) and localization (l10n) library',
|
||||
thumb_image_url: 'https://tea.xyz/Images/packages/gnu_org_gettext.jpg',
|
||||
installs: 0
|
||||
},
|
||||
{
|
||||
slug: 'ipfs_tech',
|
||||
homepage: 'https://ipfs.tech',
|
||||
name: 'ipfs.tech',
|
||||
version: '0.16.0',
|
||||
last_modified: '2022-10-19T21:36:52.000Z',
|
||||
full_name: 'ipfs.tech',
|
||||
dl_count: 14457,
|
||||
thumb_image_name: 'ipfs_tech_option 2.jpg ',
|
||||
maintainer: '',
|
||||
desc: 'Peer-to-peer hypermedia protocol',
|
||||
thumb_image_url: 'https://tea.xyz/Images/packages/ipfs_tech.jpg',
|
||||
installs: 0
|
||||
},
|
||||
{
|
||||
slug: 'nixos_org_patchelf',
|
||||
homepage: 'https://nixos.org',
|
||||
maintainer: 'nixos.org',
|
||||
name: 'patchelf',
|
||||
version: '0.15.0',
|
||||
last_modified: '2022-09-27T04:50:44.000Z',
|
||||
full_name: 'nixos.org/patchelf',
|
||||
dl_count: 0,
|
||||
thumb_image_name: 'nixos_org_patchelf_option 1.jpg ',
|
||||
desc: 'PatchELF is a simple utility for modifying existing ELF executables and libraries.',
|
||||
thumb_image_url: 'https://tea.xyz/Images/packages/nixos_org_patchelf.jpg',
|
||||
installs: 0
|
||||
},
|
||||
{
|
||||
slug: 'tea_xyz',
|
||||
homepage: 'https://tea.xyz',
|
||||
maintainer: 'tea.xyz',
|
||||
name: 'tea.xyz',
|
||||
version: '0.8.6',
|
||||
last_modified: '2022-10-19T19:13:51.000Z',
|
||||
full_name: 'tea.xyz',
|
||||
dl_count: 0,
|
||||
thumb_image_name: 'tea_xyz_option 2.jpg ',
|
||||
desc: 'Website of tea.xyz',
|
||||
thumb_image_url: 'https://tea.xyz/Images/packages/tea_xyz.jpg',
|
||||
installs: 0
|
||||
},
|
||||
{
|
||||
slug: 'charm_sh_gum',
|
||||
homepage: 'https://charm.sh',
|
||||
maintainer: 'charm.sh',
|
||||
name: 'gum',
|
||||
version: '0.8.0',
|
||||
last_modified: '2022-10-21T02:15:16.000Z',
|
||||
full_name: 'charm.sh/gum',
|
||||
dl_count: 0,
|
||||
thumb_image_name: 'charm_sh_gum.jpg ',
|
||||
desc: '',
|
||||
thumb_image_url: 'https://tea.xyz/Images/packages/charm_sh_gum.jpg',
|
||||
installs: 0
|
||||
},
|
||||
{
|
||||
slug: 'pyyaml_org',
|
||||
homepage: 'https://pyyaml.org',
|
||||
name: 'pyyaml.org',
|
||||
version: '0.2.5',
|
||||
last_modified: '2022-10-03T15:35:14.000Z',
|
||||
full_name: 'pyyaml.org',
|
||||
dl_count: 107505,
|
||||
thumb_image_name: 'pyyaml_org_option 1.jpg ',
|
||||
maintainer: '',
|
||||
desc: 'YAML framework for Python',
|
||||
thumb_image_url: 'https://tea.xyz/Images/packages/pyyaml_org.jpg',
|
||||
installs: 0
|
||||
},
|
||||
{
|
||||
slug: 'tea_xyz_gx_cc',
|
||||
homepage: 'https://tea.xyz',
|
||||
maintainer: 'tea.xyz',
|
||||
name: 'cc',
|
||||
version: '0.1.0',
|
||||
last_modified: '2022-10-19T16:47:44.000Z',
|
||||
full_name: 'tea.xyz/gx/cc',
|
||||
dl_count: 0,
|
||||
thumb_image_name: 'tea_xyz_gx.jpg ',
|
||||
desc: '',
|
||||
thumb_image_url: 'https://tea.xyz/Images/packages/tea_xyz_gx_cc.jpg',
|
||||
installs: 0
|
||||
}
|
||||
];
|
||||
|
||||
export async function getPackages(): Promise<GUIPackage[]> {
|
||||
return packages.map((pkg) => {
|
||||
return {
|
||||
...pkg,
|
||||
state: PackageStates.AVAILABLE
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function getFeaturedPackages(): Promise<Package[]> {
|
||||
await delay(2000);
|
||||
return packages.slice(0, 4);
|
||||
}
|
||||
|
||||
export async function getPackageReviews(full_name: string): Promise<Review[]> {
|
||||
console.log(`generating reviews for ${full_name}`);
|
||||
|
||||
const reviewCount = _.random(9, 21);
|
||||
const reviews: Review[] = [];
|
||||
|
||||
for (let i = 0; i < reviewCount; i++) {
|
||||
const title = loremIpsum({
|
||||
count: _.random(2, 5),
|
||||
format: 'plain',
|
||||
paragraphLowerBound: 3,
|
||||
paragraphUpperBound: 7,
|
||||
random: Math.random,
|
||||
sentenceLowerBound: 5,
|
||||
sentenceUpperBound: 15,
|
||||
units: 'words'
|
||||
});
|
||||
const comment = loremIpsum({
|
||||
count: 2,
|
||||
format: 'plain',
|
||||
paragraphLowerBound: 3,
|
||||
paragraphUpperBound: 7,
|
||||
random: Math.random,
|
||||
sentenceLowerBound: 5,
|
||||
sentenceUpperBound: 15,
|
||||
units: 'sentences'
|
||||
});
|
||||
const rating = _.random(0, 5);
|
||||
reviews.push({
|
||||
title,
|
||||
comment,
|
||||
rating
|
||||
});
|
||||
}
|
||||
|
||||
await delay(2000);
|
||||
return reviews;
|
||||
}
|
||||
|
||||
export async function installPackage(full_name: string) {
|
||||
console.log('installing: ', full_name);
|
||||
await delay(10000);
|
||||
}
|
||||
|
||||
function delay(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export async function getFeaturedCourses(): Promise<Course[]> {
|
||||
const mockCourses: Course[] = [
|
||||
{
|
||||
title: 'Developing With Tea',
|
||||
sub_title: 'by Mxcl',
|
||||
link: '#',
|
||||
banner_image_url: 'https://tea.xyz/Images/packages/mesonbuild_com.jpg'
|
||||
},
|
||||
{
|
||||
title: 'Brewing Tea',
|
||||
sub_title: 'by Mxcl',
|
||||
link: '#',
|
||||
banner_image_url: 'https://tea.xyz/Images/packages/tea_xyz_gx_cc.jpg'
|
||||
},
|
||||
{
|
||||
title: 'Harvesting Tea',
|
||||
sub_title: 'by Mxcl',
|
||||
link: '#',
|
||||
banner_image_url: 'https://tea.xyz/Images/packages/ipfs_tech.jpg'
|
||||
}
|
||||
];
|
||||
|
||||
return mockCourses;
|
||||
}
|
||||
|
||||
export async function getTopPackages(): Promise<GUIPackage[]> {
|
||||
await delay(500);
|
||||
return packages.slice(0, 9).map((pkg) => {
|
||||
return {
|
||||
...pkg,
|
||||
state: PackageStates.AVAILABLE
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAllPosts(type: string): Promise<AirtablePost[]> {
|
||||
console.log('filter by type:', type);
|
||||
const posts: AirtablePost[] = [
|
||||
{
|
||||
airtable_record_id: 'a',
|
||||
link: 'https://google.com',
|
||||
title: 'Tea Inc releases game changing api!',
|
||||
sub_title: 'lorem ipsum dolor sit amet',
|
||||
short_description: 'lorem ipsum dolor sit amet',
|
||||
thumb_image_url: '/images/bored-ape.png',
|
||||
thumb_image_name: 'borred-api.png',
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
published_at: new Date(),
|
||||
tags: ['news']
|
||||
},
|
||||
{
|
||||
airtable_record_id: 'b',
|
||||
link: 'https://google.com',
|
||||
title: 'Bored Ape not bored anymore',
|
||||
sub_title: 'lorem ipsum dolor sit amet',
|
||||
short_description: 'lorem ipsum dolor sit amet',
|
||||
thumb_image_url: '/images/bored-ape.png',
|
||||
thumb_image_name: 'borred-api.png',
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
published_at: new Date(),
|
||||
tags: ['news']
|
||||
},
|
||||
{
|
||||
airtable_record_id: 'c',
|
||||
link: 'https://google.com',
|
||||
title: 'Markdown can be executed! hoohah!',
|
||||
sub_title: 'lorem ipsum dolor sit amet',
|
||||
short_description: 'lorem ipsum dolor sit amet',
|
||||
thumb_image_url: '/images/bored-ape.png',
|
||||
thumb_image_name: 'borred-api.png',
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
published_at: new Date(),
|
||||
tags: ['news']
|
||||
}
|
||||
];
|
||||
|
||||
return posts;
|
||||
}
|
||||
|
||||
export async function getCategorizedPackages(): Promise<Category[]> {
|
||||
const mockPackages = packages.slice(0, 9).map((pkg) => ({
|
||||
...pkg,
|
||||
state: PackageStates.AVAILABLE
|
||||
}));
|
||||
return [
|
||||
{
|
||||
label: 'framework essentials',
|
||||
cta_label: 'View all essentials >',
|
||||
packages: mockPackages
|
||||
},
|
||||
{
|
||||
label: 'star-struck heavyweights',
|
||||
cta_label: 'View all star-strucks >',
|
||||
packages: mockPackages
|
||||
},
|
||||
{
|
||||
label: 'simply delightful',
|
||||
cta_label: 'View all delightful packages >',
|
||||
packages: mockPackages
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
export async function getDeviceAuth(deviceId: string): Promise<any> {
|
||||
const data = await v1Client.get<any>(`/auth/device/${deviceId}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getPackageBottles(name: string): Promise<Bottle[]> {
|
||||
return [
|
||||
{ name, platform: 'darwin', arch: 'aarch64', version: '3.39.4' },
|
||||
{ name, platform: 'darwin', arch: 'aarch64', version: '3.40.0' },
|
||||
{ name, platform: 'darwin', arch: 'x86-64', version: '3.39.4' },
|
||||
{ name, platform: 'darwin', arch: 'x86-64', version: '3.40.0' },
|
||||
{ name, platform: 'linux', arch: 'aarch64', version: '3.39.4' },
|
||||
{ name, platform: 'linux', arch: 'aarch64', version: '3.40.0' },
|
||||
{ name, platform: 'linux', arch: 'x86-64', version: '3.39.4' },
|
||||
{ name, platform: 'linux', arch: 'x86-64', version: '3.40.0' }
|
||||
];
|
||||
}
|
||||
|
||||
export async function registerDevice(): Promise<string> {
|
||||
return 'uuid1234';
|
||||
}
|
6
modules/desktop/src/libs/cli.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
const { ipcRenderer } = window.require('electron');
|
||||
|
||||
export async function installPackageCommand(full_name: string) {
|
||||
const res = await ipcRenderer.invoke('install-package', { full_name });
|
||||
console.log('install:', res);
|
||||
}
|
144
modules/desktop/src/libs/stores.ts
Normal file
|
@ -0,0 +1,144 @@
|
|||
import { writable } from 'svelte/store';
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
import type { Package, Review, AirtablePost } from '@tea/ui/types';
|
||||
import type { GUIPackage } from '$libs/types';
|
||||
|
||||
import { getFeaturedPackages, getPackageReviews, getAllPosts } from '@api';
|
||||
import initAuthStore from './stores/auth';
|
||||
import initNavStore from './stores/nav';
|
||||
import initPackagesStore from './stores/pkgs';
|
||||
|
||||
export const featuredPackages = writable<Package[]>([]);
|
||||
|
||||
export const packagesStore = initPackagesStore();
|
||||
|
||||
export const initializeFeaturedPackages = async () => {
|
||||
console.log('intialize featured packages');
|
||||
const packages = await getFeaturedPackages();
|
||||
featuredPackages.set(packages);
|
||||
};
|
||||
|
||||
interface PackagesReview {
|
||||
[full_name: string]: Review[];
|
||||
}
|
||||
|
||||
function initPackagesReviewStore() {
|
||||
const { update, subscribe } = writable<PackagesReview>({});
|
||||
|
||||
let packagesReviews: PackagesReview = {};
|
||||
|
||||
subscribe((v) => (packagesReviews = v));
|
||||
|
||||
const getSetPackageReviews = async (full_name: string) => {
|
||||
if (full_name && !packagesReviews[full_name]) {
|
||||
packagesReviews[full_name] = [];
|
||||
const reviews = await getPackageReviews(full_name);
|
||||
update((v) => {
|
||||
return {
|
||||
...v,
|
||||
[full_name]: reviews
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
subscribe: (full_name: string, reset: (reviews: Review[]) => void) => {
|
||||
getSetPackageReviews(full_name);
|
||||
return subscribe((value) => {
|
||||
if (value[full_name]) {
|
||||
reset(value[full_name]);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const packagesReviewStore = initPackagesReviewStore();
|
||||
|
||||
function initPosts() {
|
||||
let initialized = false;
|
||||
const { subscribe, set } = writable<AirtablePost[]>([]);
|
||||
const posts: AirtablePost[] = [];
|
||||
let postsIndex: Fuse<AirtablePost>;
|
||||
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
getAllPosts().then(set);
|
||||
}
|
||||
|
||||
subscribe((v) => {
|
||||
posts.push(...v);
|
||||
postsIndex = new Fuse(posts, {
|
||||
keys: ['title', 'sub_title', 'short_description', 'tags']
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
search: async (term: string, limit = 10) => {
|
||||
const res = postsIndex.search(term, { limit });
|
||||
const matchingPosts: AirtablePost[] = res.map((v) => v.item);
|
||||
return matchingPosts;
|
||||
},
|
||||
subscribeByTag: (tag: string, cb: (posts: AirtablePost[]) => void) => {
|
||||
subscribe((newPosts: AirtablePost[]) => {
|
||||
const filteredPosts = newPosts.filter((post) => post.tags.includes(tag));
|
||||
cb(filteredPosts);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
export const postsStore = initPosts();
|
||||
|
||||
function initSearchStore() {
|
||||
const searching = writable<boolean>(false);
|
||||
const { subscribe, set } = writable<string>('');
|
||||
const packagesSearch = writable<GUIPackage[]>([]);
|
||||
const postsSearch = writable<AirtablePost[]>([]);
|
||||
|
||||
// TODO:
|
||||
// should use algolia if user is somehow online
|
||||
|
||||
const packagesFound: GUIPackage[] = [];
|
||||
|
||||
let term = '';
|
||||
|
||||
subscribe((v) => (term = v));
|
||||
packagesSearch.subscribe((v) => packagesFound.push(...v));
|
||||
|
||||
return {
|
||||
term,
|
||||
searching,
|
||||
packagesSearch,
|
||||
postsSearch,
|
||||
packagesFound,
|
||||
subscribe,
|
||||
search: async (term: string) => {
|
||||
searching.set(true);
|
||||
try {
|
||||
if (term) {
|
||||
const [resultPkgs, resultPosts] = await Promise.all([
|
||||
packagesStore.search(term, 5),
|
||||
postsStore.search(term, 10)
|
||||
]);
|
||||
packagesSearch.set(resultPkgs);
|
||||
postsSearch.set(resultPosts);
|
||||
} else {
|
||||
packagesSearch.set([]);
|
||||
postsSearch.set([]);
|
||||
}
|
||||
set(term);
|
||||
} finally {
|
||||
searching.set(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const searchStore = initSearchStore();
|
||||
|
||||
export const authStore = initAuthStore();
|
||||
|
||||
export const navStore = initNavStore();
|
82
modules/desktop/src/libs/stores/auth.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { writable } from 'svelte/store';
|
||||
|
||||
import { getDeviceAuth } from '@api';
|
||||
import type { Developer } from '@tea/ui/types';
|
||||
import type { Session } from '$libs/types';
|
||||
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
|
||||
const basePath = '.tea/tea.xyz/gui';
|
||||
|
||||
export let session: Session | null = null;
|
||||
export const getSession = async (): Promise<Session | null> => {
|
||||
session = await ipcRenderer.invoke('get-session');
|
||||
return session;
|
||||
};
|
||||
|
||||
export default function initAuthStore() {
|
||||
const sessionStore = writable<Session>({});
|
||||
let pollLoop = 0;
|
||||
|
||||
const deviceIdStore = writable<string>('');
|
||||
let deviceId = '';
|
||||
|
||||
getSession().then((sess) => {
|
||||
if (sess) {
|
||||
session = sess;
|
||||
sessionStore.set(sess);
|
||||
deviceIdStore.set(sess.device_id!);
|
||||
deviceId = sess.device_id!;
|
||||
}
|
||||
});
|
||||
|
||||
let timer: NodeJS.Timer | null;
|
||||
|
||||
async function updateSession(data: Session) {
|
||||
const localSession = {
|
||||
device_id: deviceId,
|
||||
key: data.key,
|
||||
user: data.user
|
||||
};
|
||||
console.log('localSession:', localSession);
|
||||
await ipcRenderer.invoke('update-session', localSession);
|
||||
sessionStore.set(localSession);
|
||||
}
|
||||
|
||||
async function pollSession() {
|
||||
if (!timer) {
|
||||
timer = setInterval(async () => {
|
||||
pollLoop++;
|
||||
try {
|
||||
const data = await getDeviceAuth(deviceId);
|
||||
console.log('dd', deviceId, data);
|
||||
if (data.status === 'SUCCESS') {
|
||||
updateSession({
|
||||
key: data.key,
|
||||
user: data.user
|
||||
});
|
||||
timer && clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
if (pollLoop > 20 && timer) {
|
||||
clearInterval(timer);
|
||||
pollLoop = 0;
|
||||
timer = null;
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
deviceId,
|
||||
deviceIdStore,
|
||||
subscribe: (cb: (u: Developer) => void) => {
|
||||
return sessionStore.subscribe((v) => v?.user && cb(v.user));
|
||||
},
|
||||
pollSession
|
||||
};
|
||||
}
|
55
modules/desktop/src/libs/stores/nav.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { writable } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export default function initNavStore() {
|
||||
const historyStore = writable<string[]>(['/']);
|
||||
let history = ['/'];
|
||||
|
||||
historyStore.subscribe((v) => (history = v));
|
||||
|
||||
const prevPathStore = writable<string>('');
|
||||
const nextPathStore = writable<string>('');
|
||||
|
||||
let currentIndex = 0; // if non next/back click
|
||||
|
||||
let isMovingNext = false;
|
||||
let isMovingBack = false;
|
||||
|
||||
return {
|
||||
historyStore,
|
||||
prevPath: prevPathStore,
|
||||
nextPath: nextPathStore,
|
||||
next: () => {
|
||||
if (currentIndex < history.length - 1) {
|
||||
isMovingNext = true;
|
||||
goto(history[currentIndex + 1]);
|
||||
prevPathStore.set(history[currentIndex]);
|
||||
currentIndex++;
|
||||
if (currentIndex >= history.length - 1) nextPathStore.set('');
|
||||
}
|
||||
},
|
||||
back: () => {
|
||||
if (currentIndex > 0) {
|
||||
isMovingBack = true;
|
||||
goto(history[currentIndex - 1]);
|
||||
nextPathStore.set(history[currentIndex]);
|
||||
currentIndex--;
|
||||
if (currentIndex === 0) prevPathStore.set('');
|
||||
}
|
||||
},
|
||||
setNewPath: (newNextPath: string, newPrevPath: string) => {
|
||||
const oldCurrentPath = history[currentIndex];
|
||||
const isNavArrows = isMovingBack || isMovingNext;
|
||||
if (!isNavArrows && newNextPath !== oldCurrentPath) {
|
||||
historyStore.update((history) => {
|
||||
const cleanHistory = history.filter((_v, i) => i <= currentIndex);
|
||||
currentIndex = cleanHistory.length;
|
||||
prevPathStore.set(cleanHistory[currentIndex - 1]);
|
||||
return [...cleanHistory, newNextPath];
|
||||
});
|
||||
}
|
||||
isMovingNext = false;
|
||||
isMovingBack = false;
|
||||
}
|
||||
};
|
||||
}
|
36
modules/desktop/src/libs/stores/pkgs.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { writable } from 'svelte/store';
|
||||
import type { GUIPackage } from '../types';
|
||||
import { getPackages } from '@api';
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
export default function initPackagesStore() {
|
||||
let initialized = false;
|
||||
const { subscribe, set } = writable<GUIPackage[]>([]);
|
||||
const packages: GUIPackage[] = [];
|
||||
let packagesIndex: Fuse<GUIPackage>;
|
||||
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
getPackages().then((pkgs) => {
|
||||
set(pkgs);
|
||||
packagesIndex = new Fuse(pkgs, {
|
||||
keys: ['name', 'full_name', 'desc']
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
subscribe((v) => packages.push(...v));
|
||||
|
||||
return {
|
||||
packages,
|
||||
subscribe,
|
||||
search: async (term: string, limit = 5): Promise<GUIPackage[]> => {
|
||||
if (!term || !packagesIndex) return [];
|
||||
// TODO: if online, use algolia else use Fuse
|
||||
|
||||
const res = packagesIndex.search(term, { limit });
|
||||
const matchingPackages: GUIPackage[] = res.map((v) => v.item);
|
||||
return matchingPackages;
|
||||
}
|
||||
};
|
||||
}
|
38
modules/desktop/src/libs/teaDir.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
// import { app } from 'electron';
|
||||
// import fs from 'fs';
|
||||
// import { join } from 'upath';
|
||||
|
||||
type Dir = {
|
||||
name: string;
|
||||
path: string;
|
||||
children?: Dir[];
|
||||
};
|
||||
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
export async function getInstalledPackages() {
|
||||
const pkgs = await ipcRenderer.invoke('get-installed-packages');
|
||||
return pkgs as { version: string; full_name: string }[];
|
||||
}
|
||||
|
||||
const semverTest =
|
||||
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/g;
|
||||
|
||||
export const getPkgBottles = (packageDir: Dir): string[] => {
|
||||
const bottles: string[] = [];
|
||||
|
||||
const pkg = packageDir.path.split('.tea/')[1];
|
||||
const version = pkg.split('/v')[1];
|
||||
|
||||
const isVersion = semverTest.test(version) || !isNaN(+version) || version === '*';
|
||||
|
||||
if (version && isVersion) {
|
||||
bottles.push(pkg);
|
||||
} else if (packageDir?.children?.length) {
|
||||
const childBottles = packageDir.children
|
||||
.map(getPkgBottles)
|
||||
.reduce((arr, bottles) => [...arr, ...bottles], []);
|
||||
bottles.push(...childBottles);
|
||||
}
|
||||
|
||||
return bottles.filter((b) => b !== undefined).sort(); // ie: ["gohugo.io/v*", "gohugo.io/v0", "gohugo.io/v0.108", "gohugo.io/v0.108.0"]
|
||||
};
|
49
modules/desktop/src/libs/types.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
// as much possible add types here that are unique to @tea/gui use only
|
||||
// else
|
||||
// please use the package @tea/ui/src/types.ts
|
||||
// things that go there are shared types/shapes like ie: Package
|
||||
|
||||
import type { Package, Developer } from '@tea/ui/types';
|
||||
|
||||
export enum PackageStates {
|
||||
AVAILABLE,
|
||||
INSTALLED,
|
||||
INSTALLING,
|
||||
UNINSTALLED
|
||||
}
|
||||
|
||||
export type GUIPackage = Package & {
|
||||
state: PackageStates;
|
||||
installed_version?: string;
|
||||
};
|
||||
|
||||
export type Course = {
|
||||
title: string;
|
||||
sub_title: string;
|
||||
banner_image_url: string;
|
||||
link: string;
|
||||
};
|
||||
|
||||
export type Category = {
|
||||
label: string;
|
||||
cta_label: string;
|
||||
packages: GUIPackage[];
|
||||
};
|
||||
|
||||
export enum AuthStatus {
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
PENDING = 'PENDING',
|
||||
SUCCESS = 'SUCCESS',
|
||||
FAILED = 'FAILED'
|
||||
}
|
||||
|
||||
export type DeviceAuth = {
|
||||
status: AuthStatus;
|
||||
user: Developer;
|
||||
key: string;
|
||||
};
|
||||
export interface Session {
|
||||
device_id?: string;
|
||||
key?: string;
|
||||
user?: Developer;
|
||||
}
|
43
modules/desktop/src/libs/v1Client.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import axios from 'axios';
|
||||
import type { Session } from '$libs/types';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { getSession } from '$libs/stores/auth';
|
||||
|
||||
export const baseUrl = 'https://api.tea.xyz/v1';
|
||||
|
||||
export async function get<T>(urlPath: string, params?: { [key: string]: string }) {
|
||||
console.log(`GET /v1/${urlPath}`);
|
||||
|
||||
const [session] = await Promise.all([getSession()]);
|
||||
|
||||
const headers =
|
||||
session?.device_id && session?.user
|
||||
? await getHeaders(`GET/${urlPath}`, session)
|
||||
: { Authorization: 'public ' };
|
||||
|
||||
const req = await axios.request({
|
||||
method: 'GET',
|
||||
baseURL: 'https://api.tea.xyz',
|
||||
url: ['v1', ...urlPath.split('/')].filter((p) => p).join('/'),
|
||||
headers,
|
||||
params
|
||||
});
|
||||
|
||||
return req.data as T;
|
||||
}
|
||||
|
||||
async function getHeaders(path: string, session: Session) {
|
||||
const unixMs = new Date().getTime();
|
||||
const unixHexSecs = Math.round(unixMs / 1000).toString(16); // hex
|
||||
const deviceId = session.device_id?.split('-')[0];
|
||||
const preHash = [unixHexSecs, session.key, deviceId, path].join('');
|
||||
|
||||
const Authorization = bcrypt.hashSync(preHash, 10);
|
||||
|
||||
return {
|
||||
Authorization,
|
||||
['tea-ts']: unixMs.toString(),
|
||||
['tea-uid']: session.user?.developer_id,
|
||||
['tea-gui_id']: session.device_id
|
||||
};
|
||||
}
|
87
modules/desktop/src/routes/+layout.svelte
Normal file
|
@ -0,0 +1,87 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
// import { navigating } from '$app/stores';
|
||||
// import { afterNavigate } from '$app/navigation';
|
||||
import TopBar from '$components/TopBar/TopBar.svelte';
|
||||
import FooterLinks from '$components/FooterLinks/FooterLinks.svelte';
|
||||
import { navStore } from '$libs/stores';
|
||||
|
||||
import SearchPopupResults from '$components/SearchPopupResults/SearchPopupResults.svelte';
|
||||
|
||||
let view: HTMLElement;
|
||||
|
||||
// $: if ($navigating) view.scrollTop = 0;
|
||||
|
||||
// afterNavigate(({ from, to }) => {
|
||||
// if (to && to?.route.id && from && from?.url) {
|
||||
// const nextPath = to.url.href.replace(to.url.origin, '');
|
||||
// const fromPath = from?.url.href.replace(from.url.origin, '');
|
||||
// navStore.setNewPath(nextPath, fromPath || '/');
|
||||
// }
|
||||
// });
|
||||
</script>
|
||||
|
||||
<div id="main-layout" class="w-full">
|
||||
<TopBar />
|
||||
<section class="relative pt-24" bind:this={view}>
|
||||
<figure />
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
<footer class="border-gray mt-8 w-full border border-r-0 bg-black">
|
||||
<FooterLinks />
|
||||
</footer>
|
||||
<!-- <SearchPopupResults /> -->
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#main-layout {
|
||||
height: 100vh;
|
||||
}
|
||||
section {
|
||||
height: calc(100vh - 82px);
|
||||
overflow-y: scroll;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
figure {
|
||||
z-index: 0;
|
||||
position: fixed;
|
||||
top: 220px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
background-image: url('/images/gui-background-grid.svg');
|
||||
}
|
||||
@media screen and (min-width: 1440px) {
|
||||
figure {
|
||||
background-size: cover;
|
||||
background-repeat: repeat-y;
|
||||
}
|
||||
.content {
|
||||
padding: 0vw 3.6vw !important;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 1440px) {
|
||||
figure {
|
||||
background-size: contain;
|
||||
background-repeat: repeat;
|
||||
}
|
||||
.content {
|
||||
padding: 0vw 3.33vw;
|
||||
}
|
||||
}
|
||||
|
||||
slot {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
div {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
footer {
|
||||
height: 100px;
|
||||
}
|
||||
</style>
|
2
modules/desktop/src/routes/+layout.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const ssr = false;
|
||||
export const prerender = false;
|
51
modules/desktop/src/routes/+page.svelte
Normal file
|
@ -0,0 +1,51 @@
|
|||
<!-- home / discover / welcome page -->
|
||||
<script lang="ts">
|
||||
import '$appcss';
|
||||
import PageHeader from '$components/PageHeader/PageHeader.svelte';
|
||||
import EssentialWorkshops from '$components/EssentialWorkshops/EssentialWorkshops.svelte';
|
||||
import Packages from '$components/Packages/Packages.svelte';
|
||||
import News from '$components/News/News.svelte';
|
||||
|
||||
import HeaderCard from '@tea/ui/HeaderCard/HeaderCard.svelte';
|
||||
import ListAction from '@tea/ui/ListAction/ListAction.svelte';
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<PageHeader coverUrl="/images/headers/header_bg_1.png">Discover</PageHeader>
|
||||
<section class="mt-8 mb-8">
|
||||
<Packages title="FOUNDATION ESSENTIALS" />
|
||||
</section>
|
||||
<PageHeader coverUrl="/images/headers/header_bg_1.png">ASSET TYPE</PageHeader>
|
||||
<section class="mt-8 mb-8 flex gap-4">
|
||||
<div>
|
||||
<HeaderCard
|
||||
title="Browser Packages"
|
||||
imgUrl="/images/bored-ape.png"
|
||||
ctaUrl="/packages"
|
||||
ctaLabel="Browse packages >"
|
||||
articleTitle="What are packages?"
|
||||
description="Collections of files aggregated to form larger frameworks & functions. Think Python or Node.js."
|
||||
/>
|
||||
<ListAction title="Top packages this week" mainCtaTitle="VIEW ALL PACKAGES" />
|
||||
</div>
|
||||
<div>
|
||||
<HeaderCard
|
||||
title="Browse Scripts"
|
||||
imgUrl="/images/bored-ape.png"
|
||||
ctaUrl="/packages"
|
||||
ctaLabel="Browse scripts >"
|
||||
articleTitle="What are scripts?"
|
||||
description="Invisible applications that chain packages together in order to perform cool actions on your computer."
|
||||
/>
|
||||
<ListAction title="Top scripts this week" mainCtaTitle="VIEW ALL SCRIPTS" />
|
||||
</div>
|
||||
</section>
|
||||
<PageHeader coverUrl="/images/headers/header_bg_1.png">TUTORIALS</PageHeader>
|
||||
<section class="mt-8 mb-8">
|
||||
<EssentialWorkshops title="WORKSHOPS TO GET STARTED" ctaLabel="Read more articles >" />
|
||||
</section>
|
||||
<PageHeader coverUrl="/images/headers/header_bg_1.png">OPEN-SOURCE NEWS</PageHeader>
|
||||
<section class="mt-8">
|
||||
<News />
|
||||
</section>
|
||||
</div>
|
18
modules/desktop/src/routes/cli/+page.svelte
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script>
|
||||
import '$appcss';
|
||||
import PageHeader from '$components/PageHeader/PageHeader.svelte';
|
||||
import CliBanner from '$components/CliBanner/CliBanner.svelte';
|
||||
import BigBlackSpace from '$components/BigBlackSpace/BigBlackSpace.svelte';
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<PageHeader>INSTALL TEA</PageHeader>
|
||||
|
||||
<section>
|
||||
<CliBanner />
|
||||
</section>
|
||||
|
||||
<section class="mt-8">
|
||||
<BigBlackSpace />
|
||||
</section>
|
||||
</div>
|
18
modules/desktop/src/routes/documentation/+page.svelte
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script>
|
||||
import '$appcss';
|
||||
import PageHeader from '$components/PageHeader/PageHeader.svelte';
|
||||
import FeaturedCourses from '$components/FeaturedCourses/FeaturedCourses.svelte';
|
||||
import EssentialWorkshops from '$components/EssentialWorkshops/EssentialWorkshops.svelte';
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<PageHeader>Documentation</PageHeader>
|
||||
|
||||
<section>
|
||||
<FeaturedCourses />
|
||||
</section>
|
||||
|
||||
<section class="mt-8">
|
||||
<EssentialWorkshops />
|
||||
</section>
|
||||
</div>
|
10
modules/desktop/src/routes/packages/+page.svelte
Normal file
|
@ -0,0 +1,10 @@
|
|||
<script>
|
||||
import '$appcss';
|
||||
import PageHeader from '$components/PageHeader/PageHeader.svelte';
|
||||
import SearchPackages from '$components/SearchPackages/SearchPackages.svelte';
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<PageHeader>Packages</PageHeader>
|
||||
<SearchPackages />
|
||||
</div>
|
105
modules/desktop/src/routes/packages/[slug]/+page.svelte
Normal file
|
@ -0,0 +1,105 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
import { onMount } from 'svelte';
|
||||
import PageHeader from '$components/PageHeader/PageHeader.svelte';
|
||||
import { packagesReviewStore } from '$libs/stores';
|
||||
import PackageBanner from '$components/PackageBanner/PackageBanner.svelte';
|
||||
import type { Review, Bottle } from '@tea/ui/types';
|
||||
import SuggestedPackages from '$components/SuggestedPackages/SuggestedPackages.svelte';
|
||||
import Tabs from '@tea/ui/Tabs/Tabs.svelte';
|
||||
import type { Tab } from '@tea/ui/types';
|
||||
import Bottles from '@tea/ui/Bottles/Bottles.svelte';
|
||||
import { getPackageBottles } from '@api';
|
||||
import PackageMetas from '@tea/ui/PackageMetas/PackageMetas.svelte';
|
||||
import Markdown from '@tea/ui/Markdown/Markdown.svelte';
|
||||
import PackageSnippets from '@tea/ui/PackageSnippets/PackageSnippets.svelte';
|
||||
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data;
|
||||
|
||||
import { packagesStore, featuredPackages } from '$libs/stores';
|
||||
|
||||
import type { Package } from '@tea/ui/types';
|
||||
|
||||
let pkg: Package;
|
||||
|
||||
let reviews: Review[];
|
||||
let bottles: Bottle[] = [];
|
||||
let versions: string[] = [];
|
||||
|
||||
let tabs: Tab[] = [];
|
||||
|
||||
const setPkg = (pkgs: Package[]) => {
|
||||
const foundPackage = pkgs.find(({ slug }) => slug === data?.slug) as Package;
|
||||
if (!pkg && foundPackage) {
|
||||
pkg = foundPackage;
|
||||
tabs.push({
|
||||
label: 'details',
|
||||
component: Markdown,
|
||||
props: { pkg }
|
||||
});
|
||||
}
|
||||
|
||||
if (!reviews && pkg) {
|
||||
packagesReviewStore.subscribe(pkg.full_name, (updatedReviews) => {
|
||||
reviews = updatedReviews;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
packagesStore.subscribe(setPkg);
|
||||
featuredPackages.subscribe(setPkg);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const newBottles = await getPackageBottles(pkg.full_name);
|
||||
const newVersion = newBottles.map((b) => b.version);
|
||||
versions = [...new Set(newVersion)];
|
||||
|
||||
bottles.push(...newBottles);
|
||||
tabs = [
|
||||
...tabs,
|
||||
{
|
||||
label: `versions (${versions.length || 0})`,
|
||||
component: Bottles,
|
||||
props: {
|
||||
bottles
|
||||
}
|
||||
}
|
||||
];
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<PageHeader coverUrl={pkg.thumb_image_url}>{pkg.full_name}</PageHeader>
|
||||
<section>
|
||||
<PackageBanner {pkg} />
|
||||
</section>
|
||||
|
||||
<section class="mt-8 flex gap-8">
|
||||
<div class="w-2/3">
|
||||
<Tabs class="bg-black" {tabs} />
|
||||
</div>
|
||||
<div class="w-1/3">
|
||||
<PackageMetas />
|
||||
</div>
|
||||
</section>
|
||||
<PageHeader class="mt-8" coverUrl="/images/headers/header_bg_1.png">SNIPPETS</PageHeader>
|
||||
<section class="mt-8">
|
||||
<PackageSnippets />
|
||||
</section>
|
||||
<!-- <section class="mt-8">
|
||||
<PackageReviews reviews={reviews || []} />
|
||||
</section> -->
|
||||
{#if pkg}
|
||||
<PageHeader class="mt-8" coverUrl="/images/headers/header_bg_1.png"
|
||||
>YOU MAY ALSO LIKE...</PageHeader
|
||||
>
|
||||
<section class="mt-8">
|
||||
<SuggestedPackages {pkg} />
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
11
modules/desktop/src/routes/packages/[slug]/+page.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import type { LoadEvent } from '@sveltejs/kit';
|
||||
|
||||
/** @type {import('./$types').PageLoad} */
|
||||
export function load({ params }: LoadEvent) {
|
||||
// TODO: search package details here
|
||||
return {
|
||||
title: `${params.slug}`,
|
||||
content: '',
|
||||
slug: params.slug
|
||||
};
|
||||
}
|
28
modules/desktop/src/routes/profile/+page.svelte
Normal file
|
@ -0,0 +1,28 @@
|
|||
<script>
|
||||
import '$appcss';
|
||||
import PageHeader from '$components/PageHeader/PageHeader.svelte';
|
||||
import ProfileBanner from '$components/ProfileBanner/ProfileBanner.svelte';
|
||||
import Preflight from '$components/Preflight/Preflight.svelte';
|
||||
import Badges from '$components/Badges/Badges.svelte';
|
||||
import InstalledPackages from '$components/InstalledPackages/InstalledPackages.svelte';
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<PageHeader>PROFILE</PageHeader>
|
||||
<section>
|
||||
<ProfileBanner />
|
||||
</section>
|
||||
|
||||
<section class="mt-8 grid grid-cols-2 gap-8">
|
||||
<div>
|
||||
<Preflight />
|
||||
</div>
|
||||
<div>
|
||||
<Badges />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-8">
|
||||
<InstalledPackages />
|
||||
</section>
|
||||
</div>
|
BIN
modules/desktop/static/favicon.ico
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
modules/desktop/static/favicon.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
modules/desktop/static/fonts/PPNeueMachina-InktrapLight.woff
Normal file
BIN
modules/desktop/static/fonts/Sono-Light.woff2
Normal file
BIN
modules/desktop/static/images/bored-ape.png
Normal file
After Width: | Height: | Size: 764 KiB |
1
modules/desktop/static/images/close.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12L19 6.41Z"/></svg>
|
After Width: | Height: | Size: 232 B |
1
modules/desktop/static/images/expand.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M12 19H5l.007-7H7v5h5v2Zm6.992-7H17V7h-5V5h7l-.008 7Z"/></svg>
|
After Width: | Height: | Size: 176 B |
28
modules/desktop/static/images/footer-grid-element.svg
Normal file
|
@ -0,0 +1,28 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="961.504" height="157.742" viewBox="0 0 961.504 157.742">
|
||||
<g id="Group_1224" data-name="Group 1224" transform="translate(-159.75 -8720.324)">
|
||||
<path id="Rectangle_616" data-name="Rectangle 616" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(160 8720.574)" fill="#949494"/>
|
||||
<path id="Rectangle_617" data-name="Rectangle 617" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(240.084 8720.574)" fill="#949494"/>
|
||||
<path id="Rectangle_618" data-name="Rectangle 618" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(320.168 8720.574)" fill="#949494"/>
|
||||
<path id="Rectangle_619" data-name="Rectangle 619" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(400.251 8720.574)" fill="#949494"/>
|
||||
<path id="Rectangle_620" data-name="Rectangle 620" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(480.334 8720.574)" fill="#949494"/>
|
||||
<path id="Rectangle_621" data-name="Rectangle 621" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(560.418 8720.574)" fill="#949494"/>
|
||||
<path id="Rectangle_622" data-name="Rectangle 622" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(640.502 8720.574)" fill="#949494"/>
|
||||
<path id="Rectangle_2829" data-name="Rectangle 2829" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(880.754 8720.574)" fill="#949494"/>
|
||||
<path id="Rectangle_623" data-name="Rectangle 623" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(720.586 8720.574)" fill="#949494"/>
|
||||
<path id="Rectangle_2827" data-name="Rectangle 2827" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(960.838 8720.574)" fill="#949494"/>
|
||||
<path id="Rectangle_624" data-name="Rectangle 624" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(800.669 8720.574)" fill="#949494"/>
|
||||
<path id="Rectangle_2825" data-name="Rectangle 2825" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(1040.92 8720.574)" fill="#949494"/>
|
||||
<path id="Rectangle_616-2" data-name="Rectangle 616" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(160 8799.358)" fill="#949494"/>
|
||||
<path id="Rectangle_617-2" data-name="Rectangle 617" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(240.084 8799.358)" fill="#949494"/>
|
||||
<path id="Rectangle_618-2" data-name="Rectangle 618" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(320.168 8799.358)" fill="#949494"/>
|
||||
<path id="Rectangle_619-2" data-name="Rectangle 619" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(400.251 8799.358)" fill="#949494"/>
|
||||
<path id="Rectangle_620-2" data-name="Rectangle 620" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(480.334 8799.358)" fill="#949494"/>
|
||||
<path id="Rectangle_621-2" data-name="Rectangle 621" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(560.418 8799.358)" fill="#949494"/>
|
||||
<path id="Rectangle_622-2" data-name="Rectangle 622" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(640.502 8799.358)" fill="#949494"/>
|
||||
<path id="Rectangle_2830" data-name="Rectangle 2830" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(880.754 8799.358)" fill="#949494"/>
|
||||
<path id="Rectangle_623-2" data-name="Rectangle 623" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(720.586 8799.358)" fill="#949494"/>
|
||||
<path id="Rectangle_2828" data-name="Rectangle 2828" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(960.838 8799.358)" fill="#949494"/>
|
||||
<path id="Rectangle_624-2" data-name="Rectangle 624" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(800.669 8799.358)" fill="#949494"/>
|
||||
<path id="Rectangle_2826" data-name="Rectangle 2826" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(1040.92 8799.358)" fill="#949494"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.3 KiB |
BIN
modules/desktop/static/images/github.png
Normal file
After Width: | Height: | Size: 83 KiB |
1
modules/desktop/static/images/gui-background-grid.svg
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
modules/desktop/static/images/headers/header_bg_1.png
Normal file
After Width: | Height: | Size: 316 KiB |
1
modules/desktop/static/images/minimize.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M20 14H4v-4h16"/></svg>
|
After Width: | Height: | Size: 137 B |
BIN
modules/desktop/static/images/tea-icon.png
Normal file
After Width: | Height: | Size: 48 KiB |
3
modules/desktop/static/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
28
modules/desktop/svelte.config.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import adapter from '@sveltejs/adapter-static';
|
||||
import preprocess from 'svelte-preprocess';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://github.com/sveltejs/svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: [
|
||||
preprocess({
|
||||
postcss: true
|
||||
})
|
||||
],
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
pages: 'build',
|
||||
assets: 'build',
|
||||
fallback: 'app.html'
|
||||
}),
|
||||
alias: {
|
||||
'@tea/ui/*': '../ui/src/*'
|
||||
}
|
||||
// ssr: false,
|
||||
// hydrate the <div id="svelte"> element in src/app.html
|
||||
// target: '#svelte'
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -1,5 +1,4 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
const { theme, plugins } = require('@tea/ui/tailwind.config.cjs');
|
||||
import { theme, plugins } from '@tea/ui/tailwind.config.cjs';
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{html,svelte,ts,js}', '../ui/src/**/*.{html,svelte,ts,js}'],
|
||||
theme,
|
26
modules/desktop/tsconfig.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom"],
|
||||
"paths": {
|
||||
"$appcss": ["src/app.css"],
|
||||
"$libs/*": ["src/libs/*"],
|
||||
"@api": ["src/libs/api/electron.ts"],
|
||||
"$components/*": ["src/components/*"],
|
||||
"@tea/ui/*": ["../ui/src/*"]
|
||||
}
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
40
modules/desktop/vite.config.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import type { UserConfig } from 'vite';
|
||||
import path from 'path';
|
||||
|
||||
// const isMock = process.env.BUILD_FOR === 'preview';
|
||||
|
||||
const config: UserConfig = {
|
||||
plugins: [sveltekit()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@tea/ui/*': path.resolve('../ui/src/*'),
|
||||
// this dynamic-ish static importing is followed by the svelte build
|
||||
// but for vscode editing intellisense tsconfig.json is being used
|
||||
// TODO: replace it with correct api
|
||||
'@api': path.resolve('src/libs/api/electron.ts'),
|
||||
$components: path.resolve('./src/components'),
|
||||
$libs: path.resolve('./src/libs'),
|
||||
$appcss: path.resolve('./src/app.css')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
fs: {
|
||||
allow: ['..']
|
||||
}
|
||||
},
|
||||
test: {
|
||||
// Jest like globals
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
include: ['src/**/*.{test,spec}.ts'],
|
||||
// Extend jest-dom matchers
|
||||
setupFiles: ['./setupTest.js'],
|
||||
coverage: {
|
||||
provider: 'c8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -2,4 +2,4 @@
|
|||
import '$appcss';
|
||||
</script>
|
||||
|
||||
<section class="h-56 border border-gray bg-black" />
|
||||
<section class="border-gray h-56 border bg-black" />
|
||||
|
|
|
@ -14,9 +14,9 @@
|
|||
|
||||
{#each categories as category}
|
||||
<PanelHeader ctaLabel={category.cta_label} ctaLink={'#'} title={category.label} />
|
||||
<ul class="grid grid-cols-3 border border-r-0 border-gray bg-black">
|
||||
<ul class="border-gray grid grid-cols-3 border border-r-0 bg-black">
|
||||
{#each category.packages as pkg}
|
||||
<div class="border border-t-0 border-l-0 border-gray p-4">
|
||||
<div class="border-gray border border-t-0 border-l-0 p-4">
|
||||
<MiniPackageCard {pkg} ctaLabel="DETAILS" link={`/packages/${pkg.slug}`} />
|
||||
</div>
|
||||
{/each}
|
||||
|
|
|
@ -10,14 +10,14 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
<section class="mt-4 border border-gray bg-black">
|
||||
<section class="border-gray mt-4 border bg-black">
|
||||
<header class="flex flex-col items-center py-8">
|
||||
<figure>
|
||||
<img alt="tea" src="/images/tea-icon.png" class="rounded-md" />
|
||||
</figure>
|
||||
<p class="text-primary">tea.cli ver. 0.6.0</p>
|
||||
</header>
|
||||
<footer class="flex h-20 border-t border-gray text-white">
|
||||
<footer class="border-gray flex h-20 border-t text-white">
|
||||
<input class="flex-grow bg-black pl-4" disabled value="sh <(curl tea.xyz)>" />
|
||||
<Button class="w-16 border-0 border-l-2 text-sm" onClick={onCopy}>{copyButtonText}</Button>
|
||||
<Button class="w-56 border-0 border-l-2 text-sm" onClick={() => console.log('cli')}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
{#if courses.length}
|
||||
<Posts posts={courses} linkTarget="_blank" />
|
||||
{:else}
|
||||
<section class="h-64 border border-gray bg-black p-4">
|
||||
<section class="border-gray h-64 border bg-black p-4">
|
||||
<Preloader />
|
||||
</section>
|
||||
{/if}
|
||||
|
|
|
@ -2,34 +2,34 @@
|
|||
import Button from '@tea/ui/Button/Button.svelte';
|
||||
</script>
|
||||
|
||||
<footer class="relative h-auto w-full bg-black font-machina">
|
||||
<footer class="font-machina relative h-auto w-full bg-black">
|
||||
<section class="p-4 px-16 py-16">
|
||||
<h3 class="mb-5 text-2xl text-primary">QUICK LINKS</h3>
|
||||
<h3 class="text-primary mb-5 text-2xl">QUICK LINKS</h3>
|
||||
<menu class="flex gap-4">
|
||||
<div class="flex-grow border border-l-0 border-r-0 border-gray">
|
||||
<div class="border-gray flex-grow border border-l-0 border-r-0">
|
||||
<a href="/">
|
||||
<Button>
|
||||
<div class="flex justify-between text-primary hover:text-black">
|
||||
<div class="text-primary flex justify-between hover:text-black">
|
||||
<div class="uppercase">About the tea store</div>
|
||||
<div>→</div>
|
||||
</div>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-grow border border-l-0 border-r-0 border-gray">
|
||||
<div class="border-gray flex-grow border border-l-0 border-r-0">
|
||||
<a href="/">
|
||||
<Button>
|
||||
<div class="flex justify-between text-primary hover:text-black">
|
||||
<div class="text-primary flex justify-between hover:text-black">
|
||||
<div class="uppercase">REPORT A PROBLEM</div>
|
||||
<div>→</div>
|
||||
</div>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-grow border border-l-0 border-r-0 border-gray">
|
||||
<div class="border-gray flex-grow border border-l-0 border-r-0">
|
||||
<a href="https://tea.xyz" target="_blank" rel="noreferrer">
|
||||
<Button>
|
||||
<div class="flex justify-between text-primary hover:text-black">
|
||||
<div class="text-primary flex justify-between hover:text-black">
|
||||
<div class="uppercase">VISIT TEA.XYZ</div>
|
||||
<div>→</div>
|
||||
</div>
|
||||
|
@ -39,8 +39,8 @@
|
|||
</menu>
|
||||
</section>
|
||||
|
||||
<section class="h-16 border border-r-0 border-gray p-4 px-16">
|
||||
<div class="flex gap-4 text-xs text-gray">
|
||||
<section class="border-gray h-16 border border-r-0 p-4 px-16">
|
||||
<div class="text-gray flex gap-4 text-xs">
|
||||
<a
|
||||
href="https://tea.xyz/terms-of-use/"
|
||||
target="_blank"
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
<header class="border border-gray bg-black p-4 text-primary">GETTING STARTED WITH TEA</header>
|
||||
<header class="border-gray text-primary border bg-black p-4">GETTING STARTED WITH TEA</header>
|
||||
<section class="grid grid-cols-3 bg-black">
|
||||
<div class="border border-gray p-4">
|
||||
<div class="border-gray border p-4">
|
||||
<ArticleCard
|
||||
content={{
|
||||
title: 'installing tea',
|
||||
|
@ -20,7 +20,7 @@
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="border border-gray p-4">
|
||||
<div class="border-gray border p-4">
|
||||
<ArticleCard
|
||||
content={{
|
||||
title: 'authenticating',
|
||||
|
@ -32,7 +32,7 @@
|
|||
onClick={doStuff}
|
||||
/>
|
||||
</div>
|
||||
<div class="border border-gray p-4">
|
||||
<div class="border-gray border p-4">
|
||||
<ArticleCard
|
||||
content={{
|
||||
title: 'give us a star',
|
||||
|
|
|
@ -15,10 +15,10 @@
|
|||
|
||||
<PanelHeader title="My installs" ctaLabel="Check for updates >" ctaLink="#" />
|
||||
|
||||
<ul class="grid grid-cols-3 border border-r-0 border-gray bg-black">
|
||||
<ul class="border-gray grid grid-cols-3 border border-r-0 bg-black">
|
||||
{#if packages.length > 0}
|
||||
{#each packages as pkg}
|
||||
<div class="border border-t-0 border-l-0 border-gray p-4">
|
||||
<div class="border-gray border border-t-0 border-l-0 p-4">
|
||||
<MiniPackageCard
|
||||
{pkg}
|
||||
ctaLabel="DETAILS"
|
||||
|
@ -30,7 +30,7 @@
|
|||
{/each}
|
||||
{:else}
|
||||
{#each Array(12) as _}
|
||||
<section class="h-50 border border-gray p-4">
|
||||
<section class="h-50 border-gray border p-4">
|
||||
<Preloader />
|
||||
</section>
|
||||
{/each}
|
||||
|
|