codify "platform" into resolution logic, server-fn/fullstack overhaul (#4596)
overhaul dioxus fullstack using a new implementation of server_fn This PR is a complete rewrite of dioxus fullstack designed to improve compatibility with axum and to fix a number of compilation errors based on feature flag unification plumbing through macros.
This commit is contained in:
1 parent
8421c3aef0
commit
f2b1c6d97f
424 files changed
+17284
-6624
No files matched your search
@@ -12,3 +12,6 @@ runs:
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /usr/share/swift
|
||||
sudo docker image prune --all --force || true
|
||||
|
||||
|
||||
+35
-154
@@ -45,7 +45,7 @@ jobs:
|
||||
check-msrv:
|
||||
if: github.event.pull_request.draft == false
|
||||
name: Check MSRV
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dtolnay/rust-toolchain@1.86.0
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
test:
|
||||
if: github.event.pull_request.draft == false
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Free Disk Space
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
release-test:
|
||||
if: github.event.pull_request.draft == false
|
||||
name: Test Suite with Optimizations
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Free Disk Space
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
fmt:
|
||||
if: github.event.pull_request.draft == false
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dtolnay/rust-toolchain@1.86.0
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
docs:
|
||||
if: github.event.pull_request.draft == false
|
||||
name: Docs
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
@@ -145,7 +145,7 @@ jobs:
|
||||
test-docs:
|
||||
if: github.event.pull_request.draft == false
|
||||
name: Test Docs
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
@@ -166,7 +166,7 @@ jobs:
|
||||
check:
|
||||
if: github.event.pull_request.draft == false
|
||||
name: Check
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
clippy:
|
||||
if: github.event.pull_request.draft == false
|
||||
name: Clippy
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-24.04, macos-latest]
|
||||
os: [blacksmith-4vcpu-ubuntu-2404, macos-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: nixbuild/nix-quick-install-action@master
|
||||
@@ -220,7 +220,7 @@ jobs:
|
||||
playwright:
|
||||
if: github.event.pull_request.draft == false
|
||||
name: Playwright Tests
|
||||
runs-on: macos-latest
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
@@ -229,6 +229,9 @@ jobs:
|
||||
steps:
|
||||
# Do our best to cache the toolchain and node install steps
|
||||
- uses: actions/checkout@v5
|
||||
- name: Free Disk Space
|
||||
if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404-arm' }}
|
||||
uses: ./.github/actions/free-disk-space
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
@@ -297,18 +300,9 @@ jobs:
|
||||
args: "--package dioxus-desktop",
|
||||
platform: "ios",
|
||||
}
|
||||
- {
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
os: ubuntu-24.04,
|
||||
toolchain: "1.86.0",
|
||||
cross: false,
|
||||
command: "build",
|
||||
args: "--all --tests",
|
||||
platform: "desktop",
|
||||
}
|
||||
- {
|
||||
target: aarch64-unknown-linux-gnu,
|
||||
os: ubuntu-24.04-arm,
|
||||
os: blacksmith-4vcpu-ubuntu-2404-arm,
|
||||
toolchain: "1.86.0",
|
||||
cross: false,
|
||||
command: "build",
|
||||
@@ -317,16 +311,36 @@ jobs:
|
||||
}
|
||||
- {
|
||||
target: aarch64-linux-android,
|
||||
os: ubuntu-24.04,
|
||||
os: blacksmith-4vcpu-ubuntu-2404,
|
||||
toolchain: "1.86.0",
|
||||
cross: true,
|
||||
command: "build",
|
||||
args: "--package dioxus-desktop",
|
||||
platform: "android",
|
||||
}
|
||||
# commented out because it's having issues with space on the device, but we already test it above
|
||||
# - {
|
||||
# target: x86_64-unknown-linux-gnu,
|
||||
# os: ubuntu-24.04,
|
||||
# toolchain: "1.86.0",
|
||||
# cross: false,
|
||||
# command: "build",
|
||||
# args: "--all --tests",
|
||||
# platform: "desktop",
|
||||
# }
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Free Disk Space
|
||||
if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404-arm' }}
|
||||
uses: ./.github/actions/free-disk-space
|
||||
|
||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404-arm' }}
|
||||
with:
|
||||
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
|
||||
version: ${{ matrix.platform.target }}-${{ matrix.platform.os }} # disambiguate since we're in a matrix and this caching action doesn't factor in these variables
|
||||
|
||||
- name: install stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
@@ -338,16 +352,6 @@ jobs:
|
||||
if: ${{ matrix.platform.target == 'x86_64-pc-windows-msvc' }}
|
||||
uses: ilammy/setup-nasm@v1
|
||||
|
||||
- name: Free Disk Space
|
||||
if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' }}
|
||||
uses: ./.github/actions/free-disk-space
|
||||
|
||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' }}
|
||||
with:
|
||||
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
|
||||
version: ${{ matrix.platform.target }}-${{ matrix.platform.os }} # disambiguate since we're in a matrix and this caching action doesn't factor in these variables
|
||||
|
||||
- name: Install cross
|
||||
if: ${{ matrix.platform.cross == true }}
|
||||
uses: taiki-e/install-action@cross
|
||||
@@ -386,126 +390,3 @@ jobs:
|
||||
working-directory: ${{ env.UV_WORKSPACE }}
|
||||
run: |
|
||||
cargo test --workspace --tests
|
||||
|
||||
# Only run semver checks if the PR is not a draft and does not have the breaking label
|
||||
# Breaking PRs don't need to follow semver since they are breaking changes
|
||||
# However, this means we won't attempt to backport them, so you should be careful about using this label, as it will
|
||||
# likely make future backporting difficult
|
||||
#
|
||||
# todo: fix this so even if breaking changes have been merged, the fix can be backported
|
||||
#
|
||||
# This will stop working once the first breaking change has been merged, so we should really try to just backport the fix
|
||||
# and *then* run the semver checks. Basically "would backporting this PR cause a breaking change on stable?"
|
||||
#
|
||||
# semver:
|
||||
# if: github.event.pull_request.draft == false && !contains(github.event.pull_request.labels.*.name, 'breaking')
|
||||
# name: Semver Check
|
||||
# runs-on: ubuntu-24.04
|
||||
# steps:
|
||||
# - uses: actions/checkout@v5
|
||||
# - uses: dtolnay/rust-toolchain@1.86.0
|
||||
# - uses: Swatinem/rust-cache@v2
|
||||
# with:
|
||||
# cache-all-crates: "true"
|
||||
# cache-on-failure: "true"
|
||||
# - name: Check semver
|
||||
# uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||
# with:
|
||||
# manifest-path: ./Cargo.toml
|
||||
# exclude: "dioxus-cli, dioxus-ext"
|
||||
|
||||
# Releases for the CLI on a per-branch basis
|
||||
# nightly-cli:
|
||||
# if: github.event.pull_request.draft == false
|
||||
# runs-on: ${{ matrix.platform.os }}
|
||||
# strategy:
|
||||
# matrix:
|
||||
# platform:
|
||||
# - {
|
||||
# target: x86_64-pc-windows-msvc,
|
||||
# os: windows-latest
|
||||
# }
|
||||
# - {
|
||||
# target: aarch64-pc-windows-msvc,
|
||||
# os: windows-latest
|
||||
# }
|
||||
# - {
|
||||
# target: x86_64-apple-darwin,
|
||||
# os: macos-13
|
||||
# }
|
||||
# - {
|
||||
# target: aarch64-apple-darwin,
|
||||
# os: macos-latest
|
||||
# }
|
||||
# - {
|
||||
# target: x86_64-unknown-linux-gnu,
|
||||
# os: ubuntu-24.04
|
||||
# }
|
||||
# - {
|
||||
# target: aarch64-unknown-linux-gnu,
|
||||
# os: ubuntu-24.04-arm
|
||||
# }
|
||||
# - {
|
||||
# target: x86_64-unknown-linux-musl,
|
||||
# os: ubuntu-24.04
|
||||
# }
|
||||
# - {
|
||||
# target: aarch64-unknown-linux-musl,
|
||||
# os: ubuntu-24.04-arm
|
||||
# }
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v5
|
||||
|
||||
# - name: Install openssl on macos
|
||||
# if: matrix.platform.os == 'macos-latest'
|
||||
# run: brew install openssl
|
||||
|
||||
# - name: Install nasm for windows (tls)
|
||||
# if: ${{ matrix.platform.target == 'x86_64-pc-windows-msvc' }}
|
||||
# uses: ilammy/setup-nasm@v1
|
||||
|
||||
# - name: Free Disk Space
|
||||
# if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' }}
|
||||
# uses: ./.github/actions/free-disk-space
|
||||
|
||||
# - uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
# if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' }}
|
||||
# with:
|
||||
# packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev musl-tools
|
||||
# version: 1.0
|
||||
|
||||
# - name: Install stable
|
||||
# uses: dtolnay/rust-toolchain@master
|
||||
# with:
|
||||
# toolchain: "1.86.0"
|
||||
# targets: ${{ matrix.platform.target }}
|
||||
|
||||
# - uses: Swatinem/rust-cache@v2
|
||||
# with:
|
||||
# cache-all-crates: "true"
|
||||
# save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
# - name: Free Disk Space
|
||||
# uses: jlumbroso/free-disk-space@v1.3.1
|
||||
# with: # speed things up a bit
|
||||
# large-packages: false
|
||||
# docker-images: false
|
||||
# swap-storage: false
|
||||
|
||||
# # Todo: we want `cargo install dx` to actually just use a prebuilt binary instead of building it
|
||||
# - name: Build CLI
|
||||
# run: |
|
||||
# cargo build --release --package dioxus-cli --target ${{ matrix.platform.target }}
|
||||
|
||||
# - name: Upload CLI binaries
|
||||
# uses: actions/upload-artifact@v4
|
||||
# if: always()
|
||||
# with:
|
||||
# name: dx-${{ matrix.platform.target }}-${{ github.event.number || github.ref_name }}
|
||||
# path: ./target/${{ matrix.platform.target }}/release/dx
|
||||
# retention-days: ${{ github.ref == 'refs/heads/main' && 90 || 30 }}
|
||||
# overwrite: true
|
||||
|
||||
# - name: Output artifact ID
|
||||
# run: echo 'Artifact ID is ${{ steps.artifact-upload-step.outputs.artifact-id }}'
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
# Attempt to backport a merged pull request to the latest stable release
|
||||
backport:
|
||||
name: Backport pull request
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
|
||||
# Don't run on closed unmerged pull requests, or pull requests with the "breaking" label
|
||||
if: github.event.pull_request.merged && !contains(github.event.pull_request.labels.*.name, 'breaking')
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
# Upload nightly docs to the website
|
||||
docs:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
|
||||
@@ -22,7 +22,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
promote:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Publish the next pre-release
|
||||
|
||||
@@ -58,9 +58,9 @@ jobs:
|
||||
- target: aarch64-apple-darwin
|
||||
os: macos-latest
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-24.04
|
||||
os: blacksmith-4vcpu-ubuntu-2404
|
||||
- target: aarch64-unknown-linux-gnu
|
||||
os: ubuntu-24.04-arm
|
||||
os: blacksmith-4vcpu-ubuntu-2404
|
||||
# - target: x86_64-unknown-linux-musl
|
||||
# os: ubuntu-24.04
|
||||
# - target: aarch64-unknown-linux-musl
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
typos:
|
||||
if: github.event.pull_request.draft == false
|
||||
name: Check for typos
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Check for typos
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
link-check:
|
||||
if: github.event.pull_request.draft == false
|
||||
name: Check For Invalid Links
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Restore lychee cache
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
/dist
|
||||
.DS_Store
|
||||
/examples/assets/test_video.mp4
|
||||
/examples/_assets/test_video.mp4
|
||||
static
|
||||
|
||||
# new recommendation to keep the lockfile in for CI and reproducible builds
|
||||
|
||||
Generated
+569
-212
File diff suppressed because it is too large.
Load diff
+447
-143
@@ -35,8 +35,9 @@ members = [
|
||||
"packages/document",
|
||||
"packages/extension",
|
||||
"packages/fullstack",
|
||||
"packages/fullstack-hooks",
|
||||
"packages/fullstack-protocol",
|
||||
"packages/fullstack-core",
|
||||
"packages/fullstack-macro",
|
||||
"packages/fullstack-server",
|
||||
"packages/generational-box",
|
||||
"packages/history",
|
||||
"packages/hooks",
|
||||
@@ -50,7 +51,6 @@ members = [
|
||||
"packages/rsx-hotreload",
|
||||
"packages/rsx-rosetta",
|
||||
"packages/rsx",
|
||||
"packages/server-macro",
|
||||
"packages/signals",
|
||||
"packages/stores",
|
||||
"packages/stores-macro",
|
||||
@@ -59,7 +59,6 @@ members = [
|
||||
"packages/cli-config",
|
||||
"packages/devtools",
|
||||
"packages/devtools-types",
|
||||
"packages/isrg",
|
||||
"packages/rsx-hotreload",
|
||||
"packages/const-serialize",
|
||||
"packages/const-serialize-macro",
|
||||
@@ -70,7 +69,9 @@ members = [
|
||||
"packages/native-dom",
|
||||
"packages/asset-resolver",
|
||||
"packages/depinfo",
|
||||
"packages/server",
|
||||
|
||||
# CLI harnesses, all included
|
||||
"packages/cli-harnesses/*",
|
||||
|
||||
# Playwright tests
|
||||
"packages/playwright-tests/liveview",
|
||||
@@ -98,25 +99,27 @@ members = [
|
||||
"packages/subsecond/subsecond-tests/cross-tls-test",
|
||||
|
||||
# Full project examples
|
||||
"examples/fullstack-hackernews",
|
||||
"examples/ecommerce-site",
|
||||
"examples/bluetooth-scanner",
|
||||
"examples/file-explorer",
|
||||
"examples/hotdog",
|
||||
"examples/01-app-demos/hackernews",
|
||||
"examples/01-app-demos/ecommerce-site",
|
||||
"examples/01-app-demos/bluetooth-scanner",
|
||||
"examples/01-app-demos/file-explorer",
|
||||
"examples/01-app-demos/hotdog",
|
||||
|
||||
# Simple examples that require a crate
|
||||
"examples/tailwind",
|
||||
"examples/pwa",
|
||||
"examples/fullstack-hello-world",
|
||||
"examples/fullstack-router",
|
||||
"examples/fullstack-streaming",
|
||||
"examples/fullstack-desktop",
|
||||
"examples/fullstack-auth",
|
||||
"examples/fullstack-websockets",
|
||||
"examples/wgpu-texture",
|
||||
"examples/native-headless",
|
||||
"examples/native-headless-in-bevy",
|
||||
"examples/bevy",
|
||||
# Fullstack examples
|
||||
"examples/07-fullstack/hello-world",
|
||||
"examples/07-fullstack/router",
|
||||
"examples/07-fullstack/desktop",
|
||||
"examples/07-fullstack/auth",
|
||||
"examples/07-fullstack/auth-with-fullstack",
|
||||
"examples/07-fullstack/ssr-only",
|
||||
|
||||
# Integrations
|
||||
"examples/10-integrations/tailwind",
|
||||
"examples/10-integrations/pwa",
|
||||
"examples/10-integrations/wgpu-texture",
|
||||
"examples/10-integrations/native-headless",
|
||||
"examples/10-integrations/native-headless-in-bevy",
|
||||
"examples/10-integrations/bevy",
|
||||
|
||||
# Playwright tests
|
||||
"packages/playwright-tests/liveview",
|
||||
@@ -141,7 +144,6 @@ version = "0.7.0-rc.0"
|
||||
# dependencies that are shared across packages
|
||||
[workspace.dependencies]
|
||||
dioxus = { path = "packages/dioxus", version = "0.7.0-rc.0" }
|
||||
dioxus-lib = { path = "packages/dioxus-lib", version = "0.7.0-rc.0" }
|
||||
dioxus-core = { path = "packages/core", version = "0.7.0-rc.0" }
|
||||
dioxus-core-types = { path = "packages/core-types", version = "0.7.0-rc.0" }
|
||||
dioxus-core-macro = { path = "packages/core-macro", version = "0.7.0-rc.0" }
|
||||
@@ -154,10 +156,8 @@ dioxus-html = { path = "packages/html", version = "0.7.0-rc.0", default-features
|
||||
dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.7.0-rc.0" }
|
||||
dioxus-hooks = { path = "packages/hooks", version = "0.7.0-rc.0" }
|
||||
dioxus-web = { path = "packages/web", version = "0.7.0-rc.0", default-features = false }
|
||||
dioxus-isrg = { path = "packages/isrg", version = "0.7.0-rc.0" }
|
||||
dioxus-ssr = { path = "packages/ssr", version = "0.7.0-rc.0", default-features = false }
|
||||
dioxus-desktop = { path = "packages/desktop", version = "0.7.0-rc.0", default-features = false }
|
||||
dioxus-mobile = { path = "packages/mobile", version = "0.7.0-rc.0" }
|
||||
dioxus-interpreter-js = { path = "packages/interpreter", version = "0.7.0-rc.0" }
|
||||
dioxus-liveview = { path = "packages/liveview", version = "0.7.0-rc.0" }
|
||||
dioxus-autofmt = { path = "packages/autofmt", version = "0.7.0-rc.0" }
|
||||
@@ -170,11 +170,10 @@ dioxus-stores = { path = "packages/stores", version = "0.7.0-rc.0" }
|
||||
dioxus-stores-macro = { path = "packages/stores-macro", version = "0.7.0-rc.0" }
|
||||
dioxus-devtools = { path = "packages/devtools", version = "0.7.0-rc.0" }
|
||||
dioxus-devtools-types = { path = "packages/devtools-types", version = "0.7.0-rc.0" }
|
||||
dioxus-server = { path = "packages/server", version = "0.7.0-rc.0" }
|
||||
dioxus-fullstack = { path = "packages/fullstack", version = "0.7.0-rc.0" }
|
||||
dioxus-fullstack-hooks = { path = "packages/fullstack-hooks", version = "0.7.0-rc.0" }
|
||||
dioxus-fullstack-protocol = { path = "packages/fullstack-protocol", version = "0.7.0-rc.0" }
|
||||
dioxus_server_macro = { path = "packages/server-macro", version = "0.7.0-rc.0", default-features = false }
|
||||
dioxus-fullstack = { path = "packages/fullstack", version = "0.7.0-rc.0", default-features = false}
|
||||
dioxus-fullstack-core = { path = "packages/fullstack-core", version = "0.7.0-rc.0", default-features = false}
|
||||
dioxus-fullstack-macro = { path = "packages/fullstack-macro", version = "0.7.0-rc.0", default-features = false }
|
||||
dioxus-server = { path = "packages/fullstack-server", version = "0.7.0-rc.0" }
|
||||
dioxus-dx-wire-format = { path = "packages/dx-wire-format", version = "0.7.0-rc.0" }
|
||||
dioxus-logger = { path = "packages/logger", version = "0.7.0-rc.0" }
|
||||
dioxus-native = { path = "packages/native", version = "0.7.0-rc.0" }
|
||||
@@ -207,7 +206,6 @@ manganis-macro = { path = "packages/manganis/manganis-macro", version = "0.7.0-r
|
||||
wasm-splitter = { path = "packages/wasm-split/wasm-split", version = "0.7.0-rc.0" }
|
||||
wasm-split-macro = { path = "packages/wasm-split/wasm-split-macro", version = "0.7.0-rc.0" }
|
||||
wasm-split-cli = { path = "packages/wasm-split/wasm-split-cli", version = "0.7.0-rc.0" }
|
||||
wasm-split-harness = { path = "packages/playwright-tests/wasm-split-harness", version = "0.7.0-rc.0" }
|
||||
wasm-used = { path = "packages/wasm-split/wasm-used", version = "0.7.0-rc.0" }
|
||||
|
||||
depinfo = { path = "packages/depinfo", version = "0.7.0-rc.0" }
|
||||
@@ -257,8 +255,7 @@ lru = "0.16.0"
|
||||
async-trait = "0.1.88"
|
||||
axum = { version = "0.8.4", default-features = false }
|
||||
axum-server = { version = "0.7.2", default-features = false }
|
||||
server_fn = { version = "=0.8.3", default-features = false }
|
||||
server_fn_macro = { version = "=0.8.3" }
|
||||
http-body = { version = "1.0"}
|
||||
tower = "0.5.2"
|
||||
http = "1.3.1"
|
||||
notify = { version = "8.1.0" }
|
||||
@@ -277,12 +274,13 @@ serde_json = "1.0.140"
|
||||
serde = "1.0.219"
|
||||
syn = "2.0"
|
||||
quote = "1.0"
|
||||
axum-core = "0.5"
|
||||
proc-macro2 = "1.0.101"
|
||||
axum_session = "0.16.0"
|
||||
axum_session_auth = "0.16.0"
|
||||
axum_session_sqlx = "0.5.0"
|
||||
axum-extra = "0.10.1"
|
||||
reqwest = "0.12.22"
|
||||
reqwest = { version = "0.12.23", default-features = false }
|
||||
owo-colors = "4.2.2"
|
||||
ciborium = "0.2.2"
|
||||
base64 = "0.22.1"
|
||||
@@ -291,13 +289,10 @@ convert_case = "0.8.0"
|
||||
tungstenite = { version = "0.27.0" }
|
||||
tokio-tungstenite = { version = "0.27.0" }
|
||||
gloo-timers = "0.3.0"
|
||||
fluent-uri = { version = "0.3.2", features = ["serde"] }
|
||||
internment = { version = "0.8.6" }
|
||||
proc-macro2-diagnostics = { version = "0.10", default-features = false }
|
||||
env_logger = "0.11.8"
|
||||
chrono = { version = "0.4.39" }
|
||||
gloo = { version = "0.11.0" }
|
||||
gloo-utils = { version = "0.2.0" }
|
||||
rustversion = "1.0.21"
|
||||
rand = "0.9"
|
||||
longest-increasing-subsequence = "0.1.0"
|
||||
@@ -320,7 +315,6 @@ rayon = "1.10.0"
|
||||
wasmparser = "0.235.0"
|
||||
itertools = "0.14.0"
|
||||
object = { version = "0.37.1" }
|
||||
bincode = "1.3.3"
|
||||
inventory = { version = "0.3" }
|
||||
macro-string = "0.1.4"
|
||||
walkdir = "2.5.0"
|
||||
@@ -334,6 +328,16 @@ libloading = "0.8.8"
|
||||
libc = "0.2.174"
|
||||
memmap2 = "0.9.5"
|
||||
memfd = "0.6.4"
|
||||
xxhash-rust = {version = "0.8.15", default-features = false}
|
||||
serde_qs = "0.15.0"
|
||||
multer = "3.1.0"
|
||||
const-str = "0.7.0"
|
||||
bytes = "1.10"
|
||||
send_wrapper = "0.6.0"
|
||||
pin-project = { version = "1.1.10" }
|
||||
postcard = { version = "1.1.3", default-features = false }
|
||||
serde_urlencoded = "0.7"
|
||||
form_urlencoded = "1.2.1"
|
||||
|
||||
# desktop
|
||||
wry = { version = "0.52.1", default-features = false }
|
||||
@@ -341,13 +345,7 @@ tao = { version = "0.34.0", features = ["rwh_05"] }
|
||||
infer = "0.19.0"
|
||||
dunce = "1.0.5"
|
||||
percent-encoding = "2.3.1"
|
||||
global-hotkey = "0.7.0"
|
||||
rfd = { version = "0.15.3", default-features = false }
|
||||
muda = "0.17.0"
|
||||
cocoa = "0.26"
|
||||
core-foundation = "0.10.1"
|
||||
objc = { version = "0.2.7", features = ["exception"] }
|
||||
objc_id = "0.1.1"
|
||||
tray-icon = "0.21.0"
|
||||
open = "5.3.2"
|
||||
webbrowser = "1.0"
|
||||
@@ -362,14 +360,11 @@ path-absolutize = "3.1"
|
||||
crossterm = { version = "0.29.0" }
|
||||
ratatui = { version = "0.29.0" }
|
||||
shell-words = "1.1.0"
|
||||
color-eyre = "0.6.5"
|
||||
|
||||
# native
|
||||
keyboard-types = { version = "0.7", default-features = false }
|
||||
winit = { version = "0.30.11", features = ["rwh_06"] }
|
||||
|
||||
[profile.dev]
|
||||
|
||||
# our release profile should be fast to compile and fast to run
|
||||
# when we ship our CI builds, we turn on LTO which improves perf leftover by turning on incremental
|
||||
[profile.release]
|
||||
@@ -419,28 +414,40 @@ inherits = "dev"
|
||||
[package]
|
||||
name = "dioxus-examples"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
description = "Top level crate for the Dioxus repository"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
documentation = "https://dioxuslabs.com"
|
||||
keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
rust-version = "1.80.0"
|
||||
rust-version = "1.85.0"
|
||||
publish = false
|
||||
version = "0.7.0-rc.0"
|
||||
|
||||
[dependencies]
|
||||
reqwest = { workspace = true, features = ["json"], optional = true }
|
||||
reqwest = { workspace = true, features = ["json"] }
|
||||
ciborium = { workspace = true, optional = true }
|
||||
base64 = { workspace = true, optional = true }
|
||||
http-range = { version = "0.1.5", optional = true }
|
||||
http-range = { version = "0.1.5" }
|
||||
wgpu = { workspace = true, optional = true }
|
||||
ouroboros = { version = "*", optional = true }
|
||||
wasm-splitter = { workspace = true, package = "wasm-split" }
|
||||
wasm-splitter = { workspace = true }
|
||||
|
||||
sqlx = { version = "0.8.6", features = [
|
||||
"macros",
|
||||
"migrate",
|
||||
"postgres",
|
||||
"sqlite",
|
||||
"_unstable-all-types",
|
||||
"tls-native-tls",
|
||||
"runtime-tokio",
|
||||
], optional = true }
|
||||
wasm-streams = "0.4.2"
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus = { workspace = true, features = ["router"] }
|
||||
dioxus = { workspace = true, features = ["router", "fullstack"] }
|
||||
dioxus-html = { workspace = true, features = ["serialize"] }
|
||||
dioxus-stores = { workspace = true }
|
||||
dioxus-ssr = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
@@ -451,10 +458,17 @@ rand = { workspace = true, features = ["small_rng"] }
|
||||
form_urlencoded = "1.2.1"
|
||||
async-std = "1.13.1"
|
||||
web-time = "1.1.0"
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
axum-core = { workspace = true }
|
||||
uuid = { workspace = true, features = ["v4", "serde"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
getrandom = { workspace = true, features = ["wasm_js"] }
|
||||
tokio = { version = "1.46", default-features = false, features = ["sync", "macros", "io-util", "rt", "time"] }
|
||||
uuid = { workspace = true, features = ["v4", "serde", "js"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
tokio = { version = "1.46", features = ["full"] }
|
||||
@@ -466,161 +480,451 @@ default = ["desktop"]
|
||||
desktop = ["dioxus/desktop"]
|
||||
native = ["dioxus/native"]
|
||||
liveview = ["dioxus/liveview"]
|
||||
fullstack = ["dioxus/fullstack"]
|
||||
server = ["dioxus/server"]
|
||||
mobile = ["dioxus/mobile"]
|
||||
web = ["dioxus/web"]
|
||||
http = ["dep:reqwest", "dep:http-range"]
|
||||
gpu = ["dep:ouroboros", "dep:wgpu"]
|
||||
|
||||
[[example]]
|
||||
name = "login_form"
|
||||
required-features = ["http"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "dog_app"
|
||||
required-features = ["http"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "video_stream"
|
||||
required-features = ["http", "desktop"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "suspense"
|
||||
required-features = ["http", "desktop"]
|
||||
name = "websocket_chat"
|
||||
path = "examples/01-app-demos/websocket_chat.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "weather_app"
|
||||
required-features = ["http"]
|
||||
path = "examples/01-app-demos/weather_app.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "crm"
|
||||
path = "examples/01-app-demos/crm.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "image_generator_openai"
|
||||
required-features = ["http"]
|
||||
path = "examples/01-app-demos/image_generator_openai.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "todomvc"
|
||||
path = "examples/01-app-demos/todomvc.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "calculator_mutable"
|
||||
path = "examples/01-app-demos/calculator_mutable.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
path = "examples/01-app-demos/hello_world.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "counters"
|
||||
path = "examples/01-app-demos/counters.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "todomvc_store"
|
||||
path = "examples/01-app-demos/todomvc_store.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "dog_app"
|
||||
path = "examples/01-app-demos/dog_app.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "calculator"
|
||||
path = "examples/01-app-demos/calculator.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "repo_readme"
|
||||
path = "examples/01-app-demos/repo_readme.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "nested_listeners"
|
||||
path = "examples/02-building-ui/nested_listeners.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "disabled"
|
||||
path = "examples/02-building-ui/disabled.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "svg"
|
||||
path = "examples/02-building-ui/svg.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "custom_assets"
|
||||
path = "examples/03-assets-styling/custom_assets.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "dynamic_assets"
|
||||
path = "examples/03-assets-styling/dynamic_assets.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "meta"
|
||||
path = "examples/03-assets-styling/meta.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "meta_elements"
|
||||
path = "examples/03-assets-styling/meta_elements.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "reducer"
|
||||
path = "examples/04-managing-state/reducer.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "memo_chain"
|
||||
path = "examples/04-managing-state/memo_chain.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "global"
|
||||
path = "examples/04-managing-state/global.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "context_api"
|
||||
path = "examples/04-managing-state/context_api.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "signals"
|
||||
path = "examples/04-managing-state/signals.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "errors"
|
||||
path = "examples/04-managing-state/error_handling.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "backgrounded_futures"
|
||||
path = "examples/05-using-async/backgrounded_futures.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "clock"
|
||||
path = "examples/05-using-async/clock.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "streams"
|
||||
path = "examples/05-using-async/streams.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "suspense"
|
||||
path = "examples/05-using-async/suspense.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "future"
|
||||
path = "examples/05-using-async/future.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
|
||||
[[example]]
|
||||
name = "simple_router"
|
||||
path = "examples/06-routing/simple_router.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "router_restore_scroll"
|
||||
path = "examples/06-routing/router_restore_scroll.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "link"
|
||||
path = "examples/06-routing/link.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "hash_fragment_state"
|
||||
required-features = ["ciborium", "base64"]
|
||||
path = "examples/06-routing/hash_fragment_state.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "backgrounded_futures"
|
||||
required-features = ["desktop"]
|
||||
name = "router"
|
||||
path = "examples/06-routing/router.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "calculator_mutable"
|
||||
required-features = ["desktop"]
|
||||
name = "router_resource"
|
||||
path = "examples/06-routing/router_resource.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "calculator"
|
||||
required-features = ["desktop"]
|
||||
name = "query_segment_search"
|
||||
path = "examples/06-routing/query_segment_search.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "clock"
|
||||
name = "flat_router"
|
||||
path = "examples/06-routing/flat_router.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "crm"
|
||||
name = "header_map"
|
||||
path = "examples/07-fullstack/header_map.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "login_form"
|
||||
path = "examples/07-fullstack/login_form.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "handling_errors"
|
||||
path = "examples/07-fullstack/handling_errors.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "dog_app_self_hosted"
|
||||
path = "examples/07-fullstack/dog_app_self_hosted.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "through_reqwest"
|
||||
path = "examples/07-fullstack/through_reqwest.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "streaming_file_upload"
|
||||
path = "examples/07-fullstack/streaming_file_upload.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "multipart_form"
|
||||
path = "examples/07-fullstack/multipart_form.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "full_request_access"
|
||||
path = "examples/07-fullstack/full_request_access.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "fullstack_hello_world"
|
||||
path = "examples/07-fullstack/fullstack_hello_world.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "server_sent_events"
|
||||
path = "examples/07-fullstack/server_sent_events.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "server_state"
|
||||
path = "examples/07-fullstack/server_state.rs"
|
||||
doc-scrape-examples = true
|
||||
required-features = ["sqlx"]
|
||||
|
||||
[[example]]
|
||||
name = "streaming"
|
||||
path = "examples/07-fullstack/streaming.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "custom_axum_serve"
|
||||
path = "examples/07-fullstack/custom_axum_serve.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "websocket"
|
||||
path = "examples/07-fullstack/websocket.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "control_focus"
|
||||
path = "examples/08-apis/control_focus.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "window_popup"
|
||||
required-features = ["desktop"]
|
||||
path = "examples/08-apis/window_popup.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "custom_html"
|
||||
required-features = ["desktop"]
|
||||
path = "examples/08-apis/custom_html.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "custom_menu"
|
||||
name = "multiwindow_with_tray_icon"
|
||||
required-features = ["desktop"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "dynamic_asset"
|
||||
required-features = ["desktop"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "errors"
|
||||
required-features = ["desktop"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "future"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "hydration"
|
||||
required-features = ["desktop"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "multiwindow"
|
||||
required-features = ["desktop"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "overlay"
|
||||
required-features = ["desktop"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "popup"
|
||||
required-features = ["desktop"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "read_size"
|
||||
required-features = ["desktop"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "shortcut"
|
||||
required-features = ["desktop"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "streams"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "visible"
|
||||
path = "examples/08-apis/multiwindow_with_tray_icon.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "window_event"
|
||||
required-features = ["desktop"]
|
||||
path = "examples/08-apis/window_event.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "read_size"
|
||||
path = "examples/08-apis/read_size.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "logging"
|
||||
path = "examples/08-apis/logging.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "overlay"
|
||||
required-features = ["desktop"]
|
||||
path = "examples/08-apis/overlay.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "ssr"
|
||||
path = "examples/08-apis/ssr.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "video_stream"
|
||||
required-features = ["desktop"]
|
||||
path = "examples/08-apis/video_stream.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "title"
|
||||
path = "examples/08-apis/title.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "file_upload"
|
||||
path = "examples/08-apis/file_upload.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "window_focus"
|
||||
required-features = ["desktop"]
|
||||
path = "examples/08-apis/window_focus.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "eval"
|
||||
path = "examples/08-apis/eval.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "shortcut"
|
||||
required-features = ["desktop"]
|
||||
path = "examples/08-apis/shortcut.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "scroll_to_offset"
|
||||
path = "examples/08-apis/scroll_to_offset.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "scroll_to_top"
|
||||
path = "examples/08-apis/scroll_to_top.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "wgpu_child_window"
|
||||
path = "examples/08-apis/wgpu_child_window.rs"
|
||||
required-features = ["gpu", "desktop"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "multiwindow"
|
||||
required-features = ["desktop"]
|
||||
path = "examples/08-apis/multiwindow.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "window_zoom"
|
||||
required-features = ["desktop"]
|
||||
path = "examples/08-apis/window_zoom.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "wgpu_child_window"
|
||||
required-features = ["desktop", "gpu"]
|
||||
|
||||
[[example]]
|
||||
name = "control_focus"
|
||||
name = "custom_menu"
|
||||
required-features = ["desktop"]
|
||||
path = "examples/08-apis/custom_menu.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "eval"
|
||||
name = "on_resize"
|
||||
path = "examples/08-apis/on_resize.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "logging"
|
||||
name = "form"
|
||||
path = "examples/08-apis/form.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "on_visible"
|
||||
path = "examples/08-apis/on_visible.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "all_events"
|
||||
path = "examples/09-reference/all_events.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "xss_safety"
|
||||
path = "examples/09-reference/xss_safety.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "web_component"
|
||||
path = "examples/09-reference/web_component.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "generic_component"
|
||||
path = "examples/09-reference/generic_component.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "shorthand"
|
||||
path = "examples/09-reference/shorthand.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "simple_list"
|
||||
path = "examples/09-reference/simple_list.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "optional_props"
|
||||
path = "examples/09-reference/optional_props.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "rsx_usage"
|
||||
path = "examples/09-reference/rsx_usage.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "spread"
|
||||
path = "examples/09-reference/spread.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "__scrape_example_list"
|
||||
path = "examples/scripts/scrape_examples.rs"
|
||||
File renamed without changes.
+1
-1
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "bluetooth-scanner"
|
||||
version = "0.1.1"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,90 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app)
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut scan = use_action(|| async {
|
||||
use btleplug::api::{Central, Manager as _, Peripheral, ScanFilter};
|
||||
|
||||
let manager = btleplug::platform::Manager::new().await?;
|
||||
|
||||
// get the first bluetooth adapter
|
||||
let adapters = manager.adapters().await?;
|
||||
let central = adapters
|
||||
.into_iter()
|
||||
.next()
|
||||
.context("No Bluetooth adapter found")?;
|
||||
|
||||
// start scanning for devices
|
||||
central.start_scan(ScanFilter::default()).await?;
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
|
||||
// Return the list of peripherals after scanning
|
||||
let mut devices = vec![];
|
||||
for p in central.peripherals().await? {
|
||||
if let Some(p) = p.properties().await? {
|
||||
devices.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort them by RSSI (signal strength)
|
||||
devices.sort_by_key(|p| p.rssi.unwrap_or(-100));
|
||||
|
||||
dioxus::Ok(devices)
|
||||
});
|
||||
|
||||
rsx! {
|
||||
Stylesheet { href: asset!("/assets/tailwind.css") }
|
||||
div {
|
||||
div { class: "py-8 px-6",
|
||||
div { class: "container px-4 mx-auto",
|
||||
h2 { class: "text-2xl font-bold", "Scan for Bluetooth Devices" }
|
||||
button {
|
||||
class: "inline-block w-full md:w-auto px-6 py-3 font-medium text-white bg-indigo-500 hover:bg-indigo-600 rounded transition duration-200",
|
||||
disabled: scan.is_pending(),
|
||||
onclick: move |_| {
|
||||
scan.call();
|
||||
},
|
||||
if scan.is_pending() { "Scanning" } else { "Scan" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section { class: "py-8",
|
||||
div { class: "container px-4 mx-auto",
|
||||
div { class: "p-4 mb-6 bg-white shadow rounded overflow-x-auto",
|
||||
table { class: "table-auto w-full",
|
||||
thead {
|
||||
tr { class: "text-xs text-gray-500 text-left",
|
||||
th { class: "pl-6 pb-3 font-medium", "Strength" }
|
||||
th { class: "pb-3 font-medium", "Network" }
|
||||
th { class: "pb-3 font-medium", "Channel" }
|
||||
th { class: "pb-3 px-2 font-medium", "Security" }
|
||||
}
|
||||
}
|
||||
match scan.result() {
|
||||
None if scan.is_pending() => rsx! { "Scanning..." },
|
||||
None => rsx! { "Press Scan to start scanning" },
|
||||
Some(Err(_err)) => rsx! { "Failed to scan" },
|
||||
Some(Ok(peripherals)) => rsx! {
|
||||
tbody {
|
||||
for peripheral in peripherals.read().iter().rev() {
|
||||
tr { class: "text-xs bg-gray-50",
|
||||
td { class: "py-5 px-6 font-medium", "{peripheral.rssi.unwrap_or(-100)}" }
|
||||
td { class: "flex py-3 font-medium", "{peripheral.local_name.clone().unwrap_or_default()}" }
|
||||
td { span { class: "inline-block py-1 px-2 text-white bg-green-500 rounded-full", "{peripheral.address}" } }
|
||||
td { span { class: "inline-block py-1 px-2 text-purple-500 bg-purple-50 rounded-full", "{peripheral.tx_power_level.unwrap_or_default()}" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -54,7 +54,7 @@ fn app() -> Element {
|
||||
};
|
||||
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
div { id: "wrapper",
|
||||
div { class: "app",
|
||||
div { class: "calculator", tabindex: "0", onkeydown: handle_key_down_event,
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
use dioxus::desktop::tao::dpi::LogicalSize;
|
||||
use dioxus::desktop::{Config, WindowBuilder};
|
||||
use dioxus::html::input_data::keyboard_types::Key;
|
||||
use dioxus::html::MouseEvent;
|
||||
use dioxus::html::input_data::keyboard_types::Key;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
@@ -29,10 +29,7 @@ fn app() -> Element {
|
||||
let mut state = use_signal(Calculator::new);
|
||||
|
||||
rsx! {
|
||||
document::Link {
|
||||
rel: "stylesheet",
|
||||
href: asset!("/examples/assets/calculator.css"),
|
||||
}
|
||||
Stylesheet { href: asset!("/examples/assets/calculator.css") }
|
||||
div { id: "wrapper",
|
||||
div { class: "app",
|
||||
div {
|
||||
@@ -16,7 +16,7 @@ fn app() -> Element {
|
||||
let sum = use_memo(move || counters.read().iter().copied().sum::<i32>());
|
||||
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
|
||||
div { id: "controls",
|
||||
button { onclick: move |_| counters.write().push(0), "Add counter" }
|
||||
@@ -20,20 +20,12 @@ fn main() {
|
||||
}))
|
||||
.launch(|| {
|
||||
rsx! {
|
||||
document::Link {
|
||||
rel: "stylesheet",
|
||||
Stylesheet {
|
||||
href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css",
|
||||
integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5",
|
||||
crossorigin: "anonymous",
|
||||
}
|
||||
document::Link {
|
||||
rel: "stylesheet",
|
||||
href: asset!("/examples/assets/crm.css"),
|
||||
}
|
||||
document::Link {
|
||||
rel: "stylesheet",
|
||||
href: asset!("/examples/assets/crm.css"),
|
||||
}
|
||||
Stylesheet { href: asset!("/examples/assets/crm.css") }
|
||||
h1 { "Dioxus CRM Example" }
|
||||
Router::<Route> {}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
//! This example demonstrates a simple app that fetches a list of dog breeds and displays a random dog.
|
||||
//!
|
||||
//! This app combines `use_loader` and `use_action` to fetch data from the Dog API.
|
||||
//! - `use_loader` automatically fetches the list of dog breeds when the component mounts.
|
||||
//! - `use_action` fetches a random dog image whenever the `.dispatch` method is called.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
// Fetch the list of breeds from the Dog API, using the `?` syntax to suspend or throw errors
|
||||
let breed_list = use_loader(move || async move {
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
|
||||
struct ListBreeds {
|
||||
message: HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
reqwest::get("https://dog.ceo/api/breeds/list/all")
|
||||
.await?
|
||||
.json::<ListBreeds>()
|
||||
.await
|
||||
})?;
|
||||
|
||||
// Whenever this action is called, it will re-run the future and return the result.
|
||||
let mut breed = use_action(move |breed| async move {
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
struct DogApi {
|
||||
message: String,
|
||||
}
|
||||
|
||||
reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random"))
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<DogApi>()
|
||||
.await
|
||||
});
|
||||
|
||||
rsx! {
|
||||
h1 { "Doggo selector" }
|
||||
div { width: "400px",
|
||||
for cur_breed in breed_list.read().message.keys().take(20).cloned() {
|
||||
button {
|
||||
onclick: move |_| {
|
||||
breed.call(cur_breed.clone());
|
||||
},
|
||||
"{cur_breed}"
|
||||
}
|
||||
}
|
||||
}
|
||||
div {
|
||||
match breed.result() {
|
||||
None => rsx! { div { "Click the button to fetch a dog!" } },
|
||||
Some(Err(_e)) => rsx! { div { "Failed to fetch a dog, please try again." } },
|
||||
Some(Ok(res)) => rsx! {
|
||||
img {
|
||||
max_width: "500px",
|
||||
max_height: "500px",
|
||||
src: "{res.read().message}"
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
File renamed without changes.
+1
-1
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "ecommerce-site"
|
||||
version = "0.1.1"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+25
-75
@@ -1,9 +1,27 @@
|
||||
#![allow(unused)]
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use dioxus::prelude::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
// Cache up to 100 requests, invalidating them after 60 seconds
|
||||
pub(crate) async fn fetch_product(product_id: usize) -> Result<Product> {
|
||||
Ok(
|
||||
reqwest::get(format!("https://fakestoreapi.com/products/{product_id}"))
|
||||
.await?
|
||||
.json()
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
// Cache up to 100 requests, invalidating them after 60 seconds
|
||||
pub(crate) async fn fetch_products(count: usize, sort: Sort) -> Result<Vec<Product>> {
|
||||
Ok(reqwest::get(format!(
|
||||
"https://fakestoreapi.com/products/?sort={sort}&limit={count}"
|
||||
))
|
||||
.await?
|
||||
.json()
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug, Default)]
|
||||
pub(crate) struct Product {
|
||||
@@ -54,48 +72,8 @@ impl Display for Sort {
|
||||
}
|
||||
}
|
||||
|
||||
// Cache up to 100 requests, invalidating them after 60 seconds
|
||||
pub(crate) async fn fetch_user_carts(user_id: usize) -> Result<Vec<Cart>, reqwest::Error> {
|
||||
reqwest::get(format!(
|
||||
"https://fakestoreapi.com/carts/user/{user_id}?startdate=2019-12-10&enddate=2023-01-01"
|
||||
))
|
||||
.await?
|
||||
.json()
|
||||
.await
|
||||
}
|
||||
|
||||
// Cache up to 100 requests, invalidating them after 60 seconds
|
||||
pub(crate) async fn fetch_user(user_id: usize) -> dioxus::Result<Product> {
|
||||
Ok(
|
||||
reqwest::get(format!("https://fakestoreapi.com/users/{user_id}"))
|
||||
.await?
|
||||
.json()
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
// Cache up to 100 requests, invalidating them after 60 seconds
|
||||
pub(crate) async fn fetch_product(product_id: usize) -> dioxus::Result<Product> {
|
||||
Ok(
|
||||
reqwest::get(format!("https://fakestoreapi.com/products/{product_id}"))
|
||||
.await?
|
||||
.json()
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
// Cache up to 100 requests, invalidating them after 60 seconds
|
||||
pub(crate) async fn fetch_products(count: usize, sort: Sort) -> dioxus::Result<Vec<Product>> {
|
||||
Ok(reqwest::get(format!(
|
||||
"https://fakestoreapi.com/products/?sort={sort}&limit={count}"
|
||||
))
|
||||
.await?
|
||||
.json()
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) struct User {
|
||||
struct User {
|
||||
id: usize,
|
||||
email: String,
|
||||
username: String,
|
||||
@@ -104,14 +82,6 @@ pub(crate) struct User {
|
||||
phone: String,
|
||||
}
|
||||
|
||||
impl User {
|
||||
async fn fetch_most_recent_cart(&self) -> Result<Option<Cart>, reqwest::Error> {
|
||||
let all_carts = fetch_user_carts(self.id).await?;
|
||||
|
||||
Ok(all_carts.into_iter().max_by_key(|cart| cart.date))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct FullName {
|
||||
firstname: String,
|
||||
@@ -119,7 +89,7 @@ struct FullName {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub(crate) struct Cart {
|
||||
struct Cart {
|
||||
id: usize,
|
||||
#[serde(rename = "userId")]
|
||||
user_id: usize,
|
||||
@@ -128,29 +98,9 @@ pub(crate) struct Cart {
|
||||
date: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Cart {
|
||||
async fn update_database(&mut self) -> Result<(), reqwest::Error> {
|
||||
let id = self.id;
|
||||
let client = reqwest::Client::new();
|
||||
*self = client
|
||||
.put(format!("https://fakestoreapi.com/carts/{id}"))
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub(crate) struct ProductInCart {
|
||||
struct ProductInCart {
|
||||
#[serde(rename = "productId")]
|
||||
product_id: usize,
|
||||
quantity: usize,
|
||||
}
|
||||
|
||||
impl ProductInCart {
|
||||
pub async fn fetch_product(&self) -> Result<Product, dioxus::CapturedError> {
|
||||
fetch_product(self.product_id).await
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,23 @@
|
||||
// The homepage is statically rendered, so we don't need to a persistent websocket connection.
|
||||
|
||||
use crate::{
|
||||
api::{fetch_products, Sort},
|
||||
components::nav::Nav,
|
||||
components::product_item::ProductItem,
|
||||
};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
pub(crate) fn Home() -> Element {
|
||||
let products = use_loader(|| fetch_products(10, Sort::Ascending))?;
|
||||
|
||||
rsx! {
|
||||
Nav {}
|
||||
section { class: "p-10",
|
||||
for product in products.iter() {
|
||||
ProductItem {
|
||||
product: product.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub(crate) fn ChildrenOrLoading(children: Element) -> Element {
|
||||
rsx! {
|
||||
Stylesheet { href: asset!("/public/loading.css") }
|
||||
SuspenseBoundary {
|
||||
fallback: |_| rsx! { div { class: "spinner", } },
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
pub fn nav() -> Element {
|
||||
#[component]
|
||||
pub fn Nav() -> Element {
|
||||
rsx! {
|
||||
section { class: "relative",
|
||||
nav { class: "flex justify-between border-b",
|
||||
+1
-1
@@ -3,7 +3,7 @@ use dioxus::prelude::*;
|
||||
use crate::api::Product;
|
||||
|
||||
#[component]
|
||||
pub(crate) fn product_item(product: Product) -> Element {
|
||||
pub(crate) fn ProductItem(product: Product) -> Element {
|
||||
let Product {
|
||||
id,
|
||||
title,
|
||||
+35
-36
@@ -3,44 +3,11 @@ use std::{fmt::Display, str::FromStr};
|
||||
use crate::api::{fetch_product, Product};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
enum Size {
|
||||
Small,
|
||||
#[default]
|
||||
Medium,
|
||||
Large,
|
||||
}
|
||||
|
||||
impl Display for Size {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Size::Small => "small".fmt(f),
|
||||
Size::Medium => "medium".fmt(f),
|
||||
Size::Large => "large".fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Size {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
use Size::*;
|
||||
match s.to_lowercase().as_str() {
|
||||
"small" => Ok(Small),
|
||||
"medium" => Ok(Medium),
|
||||
"large" => Ok(Large),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn product_page(product_id: ReadSignal<usize>) -> Element {
|
||||
pub fn ProductPage(product_id: ReadSignal<usize>) -> Element {
|
||||
let mut quantity = use_signal(|| 1);
|
||||
let mut size = use_signal(Size::default);
|
||||
|
||||
let product = use_server_future(move || fetch_product(product_id()))?;
|
||||
let product = use_loader(move || fetch_product(product_id()))?;
|
||||
|
||||
let Product {
|
||||
title,
|
||||
@@ -50,7 +17,7 @@ pub fn product_page(product_id: ReadSignal<usize>) -> Element {
|
||||
image,
|
||||
rating,
|
||||
..
|
||||
} = product().unwrap()?;
|
||||
} = product();
|
||||
|
||||
rsx! {
|
||||
section { class: "py-20",
|
||||
@@ -216,6 +183,38 @@ pub fn product_page(product_id: ReadSignal<usize>) -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum Size {
|
||||
Small,
|
||||
#[default]
|
||||
Medium,
|
||||
Large,
|
||||
}
|
||||
|
||||
impl Display for Size {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Size::Small => "small".fmt(f),
|
||||
Size::Medium => "medium".fmt(f),
|
||||
Size::Large => "large".fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Size {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
use Size::*;
|
||||
match s.to_lowercase().as_str() {
|
||||
"small" => Ok(Small),
|
||||
"medium" => Ok(Medium),
|
||||
"large" => Ok(Large),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod icons {
|
||||
use super::*;
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@ enum Route {
|
||||
fn Details(product_id: usize) -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
components::nav::nav {}
|
||||
components::product_page::product_page {
|
||||
components::nav::Nav {}
|
||||
components::product_page::ProductPage {
|
||||
product_id
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+3
-9
@@ -8,25 +8,19 @@
|
||||
use std::env::current_dir;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use dioxus::desktop::{Config, WindowBuilder};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::LaunchBuilder::desktop()
|
||||
.with_cfg(Config::new().with_window(WindowBuilder::new().with_resizable(true)))
|
||||
.launch(app)
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut files = use_signal(Files::new);
|
||||
|
||||
rsx! {
|
||||
document::Link {
|
||||
rel: "stylesheet",
|
||||
href: asset!("/assets/fileexplorer.css")
|
||||
}
|
||||
Stylesheet { href: asset!("/assets/fileexplorer.css") }
|
||||
Stylesheet { href: "https://fonts.googleapis.com/icon?family=Material+Icons" }
|
||||
div {
|
||||
document::Link { href: "https://fonts.googleapis.com/icon?family=Material+Icons", rel: "stylesheet" }
|
||||
header {
|
||||
i { class: "material-icons icon-menu", "menu" }
|
||||
h1 { "Files: " {files.read().current()} }
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+54
-109
@@ -13,45 +13,37 @@ use svg_attributes::to;
|
||||
fn main() {
|
||||
LaunchBuilder::new()
|
||||
.with_cfg(server_only! {
|
||||
dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
|
||||
dioxus::server::ServeConfig::builder().enable_out_of_order_streaming()
|
||||
})
|
||||
.launch(|| rsx! { Router::<Route> {} });
|
||||
.launch(|| {
|
||||
rsx! {
|
||||
Stylesheet { href: asset!("/assets/hackernews.css") }
|
||||
Router::<Route> {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Routable)]
|
||||
enum Route {
|
||||
#[redirect("/", || Route::Homepage { story: PreviewState { active_story: None } })]
|
||||
#[route("/:story")]
|
||||
Homepage { story: PreviewState },
|
||||
}
|
||||
|
||||
pub fn App() -> Element {
|
||||
rsx! {
|
||||
Router::<Route> {}
|
||||
}
|
||||
#[route("/story&:story")]
|
||||
StoryPreview { story: Option<i64> },
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Homepage(story: ReadSignal<PreviewState>) -> Element {
|
||||
fn StoryPreview(story: ReadSignal<Option<i64>>) -> Element {
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: asset!("/assets/hackernews.css") }
|
||||
div { display: "flex", flex_direction: "row", width: "100%",
|
||||
div {
|
||||
width: "50%",
|
||||
SuspenseBoundary {
|
||||
fallback: |context: SuspenseContext| rsx! {
|
||||
"Loading..."
|
||||
},
|
||||
div { width: "50%",
|
||||
SuspenseBoundary { fallback: |context| rsx! { "Loading..." },
|
||||
Stories {}
|
||||
}
|
||||
}
|
||||
div { width: "50%",
|
||||
SuspenseBoundary {
|
||||
fallback: |context: SuspenseContext| rsx! {
|
||||
"Loading preview..."
|
||||
},
|
||||
Preview {
|
||||
story
|
||||
SuspenseBoundary { fallback: |context| rsx! { "Loading preview..." },
|
||||
if let Some(story) = story() {
|
||||
Preview { story_id: story }
|
||||
} else {
|
||||
div { padding: "0.5rem", "Select a story to preview" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,32 +53,31 @@ fn Homepage(story: ReadSignal<PreviewState>) -> Element {
|
||||
|
||||
#[component]
|
||||
fn Stories() -> Element {
|
||||
let stories: Resource<dioxus::Result<Vec<i64>>> = use_server_future(move || async move {
|
||||
let url = format!("{}topstories.json", BASE_API_URL);
|
||||
let mut stories_ids = reqwest::get(&url).await?.json::<Vec<i64>>().await?;
|
||||
stories_ids.truncate(30);
|
||||
Ok(stories_ids)
|
||||
let stories = use_loader(move || async move {
|
||||
let stories_ids = reqwest::get(&format!("{}topstories.json", BASE_API_URL))
|
||||
.await?
|
||||
.json::<Vec<i64>>()
|
||||
.await?
|
||||
.into_iter()
|
||||
.take(30)
|
||||
.collect::<Vec<i64>>();
|
||||
dioxus::Ok(stories_ids)
|
||||
})?;
|
||||
|
||||
match stories().unwrap() {
|
||||
Ok(list) => rsx! {
|
||||
div {
|
||||
for story in list {
|
||||
ChildrenOrLoading {
|
||||
key: "{story}",
|
||||
StoryListing { story }
|
||||
}
|
||||
rsx! {
|
||||
div {
|
||||
for story in stories() {
|
||||
ChildrenOrLoading { key: "{story}",
|
||||
StoryListing { story }
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(err) => rsx! {"An error occurred while fetching stories {err}"},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn StoryListing(story: ReadSignal<i64>) -> Element {
|
||||
let story = use_server_future(move || get_story(story()))?;
|
||||
|
||||
let story = use_loader(move || get_story(story()))?;
|
||||
let StoryItem {
|
||||
title,
|
||||
url,
|
||||
@@ -96,7 +87,7 @@ fn StoryListing(story: ReadSignal<i64>) -> Element {
|
||||
kids,
|
||||
id,
|
||||
..
|
||||
} = story().unwrap()?.item;
|
||||
} = story().item;
|
||||
|
||||
let url = url.as_deref().unwrap_or_default();
|
||||
let hostname = url
|
||||
@@ -121,7 +112,7 @@ fn StoryListing(story: ReadSignal<i64>) -> Element {
|
||||
position: "relative",
|
||||
div { font_size: "1.5rem",
|
||||
Link {
|
||||
to: Route::Homepage { story: PreviewState { active_story: Some(id) } },
|
||||
to: Route::StoryPreview { story: Some(id) },
|
||||
"{title}"
|
||||
}
|
||||
a {
|
||||
@@ -141,44 +132,9 @@ fn StoryListing(story: ReadSignal<i64>) -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct PreviewState {
|
||||
active_story: Option<i64>,
|
||||
}
|
||||
|
||||
impl FromStr for PreviewState {
|
||||
type Err = ParseIntError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let state = i64::from_str(s)?;
|
||||
Ok(PreviewState {
|
||||
active_story: Some(state),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PreviewState {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
if let Some(id) = &self.active_story {
|
||||
write!(f, "{id}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Preview(story: ReadSignal<PreviewState>) -> Element {
|
||||
let PreviewState {
|
||||
active_story: Some(id),
|
||||
} = story()
|
||||
else {
|
||||
return rsx! {"Click on a story to preview it here"};
|
||||
};
|
||||
|
||||
let story = use_server_future(use_reactive!(|id| get_story(id)))?;
|
||||
|
||||
let story = story().unwrap()?;
|
||||
|
||||
fn Preview(story_id: ReadSignal<i64>) -> Element {
|
||||
let story = use_loader(move || get_story(story_id()))?.cloned();
|
||||
rsx! {
|
||||
div { padding: "0.5rem",
|
||||
div { font_size: "1.5rem", a { href: story.item.url, "{story.item.title}" } }
|
||||
@@ -194,13 +150,14 @@ fn Preview(story: ReadSignal<PreviewState>) -> Element {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Comment(comment: i64) -> Element {
|
||||
let comment: Resource<dioxus::Result<CommentData>> =
|
||||
use_server_future(use_reactive!(|comment| async move {
|
||||
let url = format!("{}{}{}.json", BASE_API_URL, ITEM_API, comment);
|
||||
let mut comment = reqwest::get(&url).await?.json::<CommentData>().await?;
|
||||
Ok(comment)
|
||||
}))?;
|
||||
fn Comment(comment: ReadSignal<i64>) -> Element {
|
||||
let comment = use_loader(move || async move {
|
||||
let mut comment = reqwest::get(&format!("{}{}{}.json", BASE_API_URL, ITEM_API, comment))
|
||||
.await?
|
||||
.json::<CommentData>()
|
||||
.await?;
|
||||
dioxus::Ok(comment)
|
||||
})?;
|
||||
|
||||
let CommentData {
|
||||
by,
|
||||
@@ -209,7 +166,7 @@ fn Comment(comment: i64) -> Element {
|
||||
id,
|
||||
kids,
|
||||
..
|
||||
} = comment().unwrap()?;
|
||||
} = comment();
|
||||
|
||||
rsx! {
|
||||
div { padding: "0.5rem",
|
||||
@@ -272,33 +229,21 @@ pub struct StoryItem {
|
||||
pub r#type: String,
|
||||
}
|
||||
|
||||
pub async fn get_story(id: i64) -> dioxus::Result<StoryPageData> {
|
||||
let url = format!("{}{}{}.json", BASE_API_URL, ITEM_API, id);
|
||||
Ok(reqwest::get(&url).await?.json::<StoryPageData>().await?)
|
||||
pub async fn get_story(id: i64) -> Result<StoryPageData> {
|
||||
Ok(
|
||||
reqwest::get(&format!("{}{}{}.json", BASE_API_URL, ITEM_API, id))
|
||||
.await?
|
||||
.json::<StoryPageData>()
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ChildrenOrLoading(children: Element) -> Element {
|
||||
rsx! {
|
||||
SuspenseBoundary {
|
||||
fallback: |context: SuspenseContext| {
|
||||
rsx! {
|
||||
if let Some(placeholder) = context.suspense_placeholder() {
|
||||
{placeholder}
|
||||
} else {
|
||||
LoadingIndicator {}
|
||||
}
|
||||
}
|
||||
},
|
||||
fallback: |_| rsx! { div { class: "spinner", } },
|
||||
children
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn LoadingIndicator() -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
class: "spinner",
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -11,9 +11,10 @@ reqwest = { workspace = true, features = ["json"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
rusqlite = { version = "0.32.0", optional = true }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["web"]
|
||||
default = ["web", "server"]
|
||||
web = ["dioxus/web"]
|
||||
desktop = ["dioxus/desktop"]
|
||||
mobile = ["dioxus/mobile"]
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,41 @@
|
||||
use anyhow::Result;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
thread_local! {
|
||||
static DB: std::sync::LazyLock<rusqlite::Connection> = std::sync::LazyLock::new(|| {
|
||||
let conn = rusqlite::Connection::open("hotdogdb/hotdog.db").expect("Failed to open database");
|
||||
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS dogs (
|
||||
id INTEGER PRIMARY KEY,
|
||||
url TEXT NOT NULL
|
||||
);",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
conn
|
||||
});
|
||||
}
|
||||
|
||||
#[get("/api/dogs")]
|
||||
pub async fn list_dogs() -> Result<Vec<(usize, String)>> {
|
||||
DB.with(|db| {
|
||||
Ok(db
|
||||
.prepare("SELECT id, url FROM dogs ORDER BY id DESC LIMIT 10")?
|
||||
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
|
||||
.collect::<Result<Vec<(usize, String)>, rusqlite::Error>>()?)
|
||||
})
|
||||
}
|
||||
|
||||
#[delete("/api/dogs/{id}")]
|
||||
pub async fn remove_dog(id: usize) -> Result<()> {
|
||||
DB.with(|db| db.execute("DELETE FROM dogs WHERE id = ?1", [id]))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[post("/api/dogs")]
|
||||
pub async fn save_dog(image: String) -> Result<()> {
|
||||
DB.with(|db| db.execute("INSERT INTO dogs (url) VALUES (?1)", [&image]))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::{
|
||||
backend::{list_dogs, remove_dog, save_dog},
|
||||
Route,
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn Favorites() -> Element {
|
||||
let mut favorites = use_loader(list_dogs)?;
|
||||
|
||||
rsx! {
|
||||
div { id: "favorites",
|
||||
for (id , url) in favorites.cloned() {
|
||||
div { class: "favorite-dog", key: "{id}",
|
||||
img { src: "{url}" }
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
_ = remove_dog(id).await;
|
||||
favorites.restart();
|
||||
},
|
||||
"❌"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn NavBar() -> Element {
|
||||
rsx! {
|
||||
div { id: "title",
|
||||
span {}
|
||||
Link { to: Route::DogView, h1 { "🌭 HotDog! " } }
|
||||
Link { to: Route::Favorites, id: "heart", "♥️" }
|
||||
}
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn DogView() -> Element {
|
||||
let mut img_src = use_loader(|| async move {
|
||||
anyhow::Ok(
|
||||
reqwest::get("https://dog.ceo/api/breeds/image/random")
|
||||
.await?
|
||||
.json::<serde_json::Value>()
|
||||
.await?["message"]
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
rsx! {
|
||||
div { id: "dogview",
|
||||
img { id: "dogimg", src: "{img_src}" }
|
||||
}
|
||||
div { id: "buttons",
|
||||
button {
|
||||
id: "skip",
|
||||
onclick: move |_| img_src.restart(),
|
||||
"skip"
|
||||
}
|
||||
button {
|
||||
id: "save",
|
||||
onclick: move |_| async move { _ = save_dog(img_src()).await },
|
||||
"save!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
mod backend;
|
||||
mod components;
|
||||
mod frontend;
|
||||
|
||||
use components::{DogView, Favorites, NavBar};
|
||||
use dioxus::prelude::*;
|
||||
use frontend::*;
|
||||
|
||||
#[derive(Routable, PartialEq, Clone)]
|
||||
enum Route {
|
||||
@@ -16,14 +16,14 @@ enum Route {
|
||||
|
||||
fn main() {
|
||||
#[cfg(not(feature = "server"))]
|
||||
server_fn::client::set_server_url("https://hot-dog.fly.dev");
|
||||
dioxus::fullstack::set_server_url("https://hot-dog.fly.dev");
|
||||
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! {
|
||||
document::Stylesheet { href: asset!("/assets/main.css") }
|
||||
Stylesheet { href: asset!("/assets/main.css") }
|
||||
Router::<Route> {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app)
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut api_key = use_signal(|| "".to_string());
|
||||
let mut prompt = use_signal(|| "".to_string());
|
||||
let mut num_images = use_signal(|| 1.to_string());
|
||||
|
||||
let mut image = use_action(move || async move {
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Props, Clone, Default)]
|
||||
struct ImageResponse {
|
||||
created: i32,
|
||||
data: Vec<UrlImage>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Props, Clone)]
|
||||
struct UrlImage {
|
||||
url: String,
|
||||
}
|
||||
|
||||
if api_key.peek().is_empty() || prompt.peek().is_empty() || num_images.peek().is_empty() {
|
||||
return dioxus::Ok(ImageResponse::default());
|
||||
}
|
||||
|
||||
let res = reqwest::Client::new()
|
||||
.post("https://api.openai.com/v1/images/generations")
|
||||
.json(&serde_json::json!({
|
||||
"prompt": prompt.cloned(),
|
||||
"n": num_images.cloned().parse::<i32>().unwrap_or(1),
|
||||
"size":"1024x1024",
|
||||
}))
|
||||
.bearer_auth(api_key)
|
||||
.send()
|
||||
.await?
|
||||
.json::<ImageResponse>()
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
});
|
||||
|
||||
rsx! {
|
||||
Stylesheet { href: "https://unpkg.com/bulma@0.9.0/css/bulma.min.css" }
|
||||
div { class: "container",
|
||||
div { class: "columns",
|
||||
div { class: "column",
|
||||
input { class: "input is-primary mt-4",
|
||||
value: "{api_key}",
|
||||
r#type: "text",
|
||||
placeholder: "Your OpenAI API Key",
|
||||
oninput: move |evt| api_key.set(evt.value()),
|
||||
}
|
||||
input { class: "input is-primary mt-4",
|
||||
placeholder: "MAX 1000 Dgts",
|
||||
r#type: "text",
|
||||
value:"{prompt}",
|
||||
oninput: move |evt| prompt.set(evt.value())
|
||||
}
|
||||
input { class: "input is-primary mt-4",
|
||||
r#type: "number",
|
||||
min:"1",
|
||||
max:"10",
|
||||
value:"{num_images}",
|
||||
oninput: move |evt| num_images.set(evt.value()),
|
||||
}
|
||||
}
|
||||
}
|
||||
button {
|
||||
class: "button is-primary",
|
||||
class: if image.is_pending() { "is-loading" },
|
||||
onclick: move |_| {
|
||||
image.call();
|
||||
},
|
||||
"Generate image"
|
||||
}
|
||||
if let Some(image) = image.value() {
|
||||
for image in image.read().data.as_slice() {
|
||||
section { class: "is-flex",
|
||||
div { class: "container is-fluid",
|
||||
div { class: "container has-text-centered",
|
||||
div { class: "is-justify-content-center",
|
||||
div { class: "level",
|
||||
div { class: "level-item",
|
||||
figure { class: "image", img { alt: "", src: "{image.url}", } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -63,7 +63,7 @@ fn app() -> Element {
|
||||
};
|
||||
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
section { class: "todoapp",
|
||||
TodoHeader { todos }
|
||||
section { class: "main",
|
||||
@@ -124,7 +124,7 @@ fn app() -> Element {
|
||||
};
|
||||
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
section { class: "todoapp",
|
||||
TodoHeader { todos }
|
||||
section { class: "main",
|
||||
@@ -1,7 +1,8 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus::{fullstack::Loading, prelude::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
@@ -16,10 +17,10 @@ fn app() -> Element {
|
||||
id: 2950159,
|
||||
});
|
||||
|
||||
let current_weather = use_resource(move || async move { get_weather(&country()).await });
|
||||
let current_weather = use_loader(move || get_weather(country()));
|
||||
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" }
|
||||
Stylesheet { href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" }
|
||||
div { class: "mx-auto p-4 bg-gray-100 h-screen flex justify-center",
|
||||
div { class: "flex items-center justify-center flex-row",
|
||||
div { class: "flex items-start justify-center flex-row",
|
||||
@@ -27,14 +28,20 @@ fn app() -> Element {
|
||||
div { class: "flex flex-wrap w-full px-2",
|
||||
div { class: "bg-gray-900 text-white relative min-w-0 break-words rounded-lg overflow-hidden shadow-sm mb-4 w-full dark:bg-gray-600",
|
||||
div { class: "px-6 py-6 relative",
|
||||
if let Some(Ok(weather)) = current_weather.read().as_ref() {
|
||||
CountryData {
|
||||
country: country.read().clone(),
|
||||
weather: weather.clone(),
|
||||
match current_weather {
|
||||
Ok(weather) => rsx! {
|
||||
CountryData {
|
||||
country: country.read().clone(),
|
||||
weather: weather.cloned(),
|
||||
}
|
||||
Forecast { weather: weather.cloned() }
|
||||
},
|
||||
Err(Loading::Pending(_)) => rsx! {
|
||||
div { "Loading weather data..." }
|
||||
},
|
||||
Err(Loading::Failed(_)) => rsx! {
|
||||
div { "Failed to load weather data." }
|
||||
}
|
||||
Forecast { weather: weather.clone() }
|
||||
} else {
|
||||
p { "Loading.." }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,8 +117,7 @@ fn Forecast(weather: WeatherResponse) -> Element {
|
||||
#[component]
|
||||
fn SearchBox(mut country: WriteSignal<WeatherLocation>) -> Element {
|
||||
let mut input = use_signal(|| "".to_string());
|
||||
|
||||
let locations = use_resource(move || async move { get_locations(&input()).await });
|
||||
let locations = use_resource(move || get_locations(input()));
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
@@ -197,7 +203,7 @@ struct SearchResponse {
|
||||
results: WeatherLocations,
|
||||
}
|
||||
|
||||
async fn get_locations(input: &str) -> reqwest::Result<WeatherLocations> {
|
||||
async fn get_locations(input: impl Display) -> reqwest::Result<WeatherLocations> {
|
||||
let res = reqwest::get(&format!(
|
||||
"https://geocoding-api.open-meteo.com/v1/search?name={input}"
|
||||
))
|
||||
@@ -226,13 +232,9 @@ struct DailyWeather {
|
||||
temperature_2m_max: Vec<f32>,
|
||||
}
|
||||
|
||||
async fn get_weather(location: &WeatherLocation) -> reqwest::Result<WeatherResponse> {
|
||||
let res = reqwest::get(&format!("https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}&hourly=temperature_2m&daily=temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min&timezone=GMT", location.latitude, location.longitude))
|
||||
.await
|
||||
?
|
||||
async fn get_weather(location: WeatherLocation) -> reqwest::Result<WeatherResponse> {
|
||||
reqwest::get(&format!("https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}&hourly=temperature_2m&daily=temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min&timezone=GMT", location.latitude, location.longitude))
|
||||
.await?
|
||||
.json::<WeatherResponse>()
|
||||
.await
|
||||
?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
//! A websocket chat demo using Dioxus' built-in websocket support.
|
||||
//!
|
||||
//! We setup an endpoint at `/api/chat` that accepts a `name` and `user_id` query parameter.
|
||||
//! Each client connects to that endpoint, and we use a `tokio::broadcast` channel
|
||||
//! to send messages to all connected clients.
|
||||
//!
|
||||
//! In practice, you'd use a distributed messaging system (Redis PubSub / Kafka / etc) to coordinate
|
||||
//! between multiple server instances and an additional database to persist chat history.
|
||||
|
||||
use dioxus::fullstack::{WebSocketOptions, Websocket, use_websocket};
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
fn main() {
|
||||
println!("IP and PORT: {:#?}", std::env::var("IP"));
|
||||
println!("IP and PORT: {:#?}", std::env::var("PORT"));
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
// store the user's current input
|
||||
let mut input = use_signal(|| "".to_string());
|
||||
|
||||
// Select a unique id for the user, and then use that entropy to pick a random name
|
||||
let user_id = use_signal(uuid::Uuid::new_v4);
|
||||
let user_name = use_signal(|| {
|
||||
match user_id.read().as_bytes()[0] % 7 {
|
||||
0 => "Alice",
|
||||
1 => "Bob",
|
||||
2 => "Eve",
|
||||
3 => "Mallory",
|
||||
4 => "Trent",
|
||||
5 => "Peggy",
|
||||
6 => "Victor",
|
||||
_ => "Charlie",
|
||||
}
|
||||
.to_string()
|
||||
});
|
||||
|
||||
// Store the messages we've received from the server
|
||||
let mut message_list = use_signal(Vec::<ChatMessage>::new);
|
||||
|
||||
// Connect to the websocket endpoint
|
||||
let mut socket =
|
||||
use_websocket(move || uppercase_ws(user_name(), user_id(), Default::default()));
|
||||
|
||||
use_future(move || async move {
|
||||
while let Ok(msg) = socket.recv().await {
|
||||
match msg {
|
||||
ServerEvent::ReceiveMessage(message) => message_list.write().push(message),
|
||||
ServerEvent::Connected { messages } => message_list.set(messages),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rsx! {
|
||||
h1 { "WebSocket Chat" }
|
||||
p { "Connection status: {socket.status():?} as {user_name}" }
|
||||
input {
|
||||
placeholder: "Type a message",
|
||||
value: "{input}",
|
||||
oninput: move |e| async move { input.set(e.value()) },
|
||||
onkeydown: move |e| async move {
|
||||
if e.key() == Key::Enter {
|
||||
_ = socket.send(ClientEvent::SendMessage(input.read().clone())).await;
|
||||
input.set("".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
for message in message_list.read().iter().rev() {
|
||||
pre { "{message.name}: {message.message}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The events that the client can send to the server
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
enum ClientEvent {
|
||||
SendMessage(String),
|
||||
}
|
||||
|
||||
/// The events that the server can send to the client
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
enum ServerEvent {
|
||||
Connected { messages: Vec<ChatMessage> },
|
||||
ReceiveMessage(ChatMessage),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
struct ChatMessage {
|
||||
user_id: Uuid,
|
||||
name: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[get("/api/chat?name&user_id")]
|
||||
async fn uppercase_ws(
|
||||
name: String,
|
||||
user_id: Uuid,
|
||||
options: WebSocketOptions,
|
||||
) -> Result<Websocket<ClientEvent, ServerEvent>> {
|
||||
use std::sync::LazyLock;
|
||||
use tokio::sync::{
|
||||
Mutex,
|
||||
broadcast::{self, Sender},
|
||||
};
|
||||
|
||||
// Every chat app needs a chat room! For this demo, we just use a tokio broadcast channel and a mutex-protected
|
||||
// list of messages to store chat history.
|
||||
//
|
||||
// We place these types in the body of this serverfn since they're not used on the client, only the server.
|
||||
static MESSAGES: LazyLock<Mutex<Vec<ChatMessage>>> = LazyLock::new(|| Mutex::new(Vec::new()));
|
||||
static BROADCAST: LazyLock<Sender<ChatMessage>> = LazyLock::new(|| broadcast::channel(100).0);
|
||||
|
||||
Ok(options.on_upgrade(move |mut socket| async move {
|
||||
// Send back all the messages from the room to the new client
|
||||
let messages = MESSAGES.lock().await.clone();
|
||||
_ = socket.send(ServerEvent::Connected { messages }).await;
|
||||
|
||||
// Subscriber to the broadcast channel
|
||||
let sender = BROADCAST.clone();
|
||||
let mut broadcast = sender.subscribe();
|
||||
|
||||
// Announce that we've joined
|
||||
let _ = sender.send(ChatMessage {
|
||||
message: format!("{name} has connected."),
|
||||
user_id,
|
||||
name: "[CONSOLE]".to_string(),
|
||||
});
|
||||
|
||||
// Loop poll the broadcast receiver and the websocket for new messages
|
||||
// If we receive a message from the broadcast channel, send it to the client
|
||||
// If we receive a message from the client, broadcast it to all other clients and save it to the message list
|
||||
loop {
|
||||
tokio::select! {
|
||||
Ok(msg) = broadcast.recv() => {
|
||||
let _ = socket.send(ServerEvent::ReceiveMessage(msg)).await;
|
||||
}
|
||||
Ok(ClientEvent::SendMessage(message)) = socket.recv() => {
|
||||
let chat_message = ChatMessage {
|
||||
user_id,
|
||||
name: name.clone(),
|
||||
message,
|
||||
};
|
||||
let _ = sender.send(chat_message.clone());
|
||||
MESSAGES.lock().await.push(chat_message.clone());
|
||||
},
|
||||
else => break,
|
||||
}
|
||||
}
|
||||
|
||||
_ = sender.send(ChatMessage {
|
||||
name: "[CONSOLE]".to_string(),
|
||||
message: format!("{name} has disconnected."),
|
||||
user_id,
|
||||
});
|
||||
}))
|
||||
}
|
||||
@@ -12,8 +12,9 @@ fn app() -> Element {
|
||||
let mut disabled = use_signal(|| false);
|
||||
|
||||
rsx! {
|
||||
div { style: "text-align: center; margin: 20px; display: flex; flex-direction: column; align-items: center;",
|
||||
button { onclick: move |_| disabled.toggle(),
|
||||
div { text_align: "center", margin: "20px", display: "flex", flex_direction: "column", align_items: "center",
|
||||
button {
|
||||
onclick: move |_| disabled.toggle(),
|
||||
"click to "
|
||||
if disabled() { "enable" } else { "disable" }
|
||||
" the lower button"
|
||||
File renamed without changes.
@@ -7,7 +7,7 @@
|
||||
//! If you `go-to-definition` on the `svg` element, you'll see its custom namespace.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use rand::{rng, Rng};
|
||||
use rand::{Rng, rng};
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(|| {
|
||||
@@ -45,7 +45,7 @@ fn Dice() -> Element {
|
||||
value.set(rng().random_range(1..=6))
|
||||
},
|
||||
rect { x: -1000, y: -1000, width: 2000, height: 2000, rx: 200, fill: "#aaa" }
|
||||
for ((x, y), _) in DOTS.iter().zip(active_dots.read().iter()).filter(|(_, &active)| active) {
|
||||
for ((x, y), _) in DOTS.iter().zip(active_dots.read().iter()).filter(|(_, active)| **active) {
|
||||
circle {
|
||||
cx: *x * 600,
|
||||
cy: *y * 600,
|
||||
File renamed without changes.
@@ -20,11 +20,11 @@ fn app() -> Element {
|
||||
return;
|
||||
}
|
||||
|
||||
response.respond(Response::new(include_bytes!("./assets/logo.png").to_vec()));
|
||||
response.respond(Response::new(include_bytes!("../assets/logo.png").to_vec()));
|
||||
});
|
||||
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
h1 { "Dynamic Assets" }
|
||||
img { src: "/logos/logo.png" }
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,20 @@
|
||||
//! This example shows how to add metadata to the page with the Meta component
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! {
|
||||
// You can use the Meta component to render a meta tag into the head of the page
|
||||
// Meta tags are useful to provide information about the page to search engines and social media sites
|
||||
// This example sets up meta tags for the open graph protocol for social media previews
|
||||
Meta { property: "og:title", content: "My Site" }
|
||||
Meta { property: "og:type", content: "website" }
|
||||
Meta { property: "og:url", content: "https://www.example.com" }
|
||||
Meta { property: "og:image", content: "https://example.com/image.jpg" }
|
||||
Meta { name: "description", content: "My Site is a site" }
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ fn app() -> Element {
|
||||
use_context_provider(|| Signal::new(Theme::Light));
|
||||
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
main {
|
||||
class: "main-container",
|
||||
|
||||
@@ -54,9 +54,20 @@ fn Show() -> Element {
|
||||
ErrorBoundary {
|
||||
handle_error: |errors: ErrorContext| {
|
||||
rsx! {
|
||||
for error in errors.errors() {
|
||||
if let Some(error) = error.show() {
|
||||
{error}
|
||||
for error in errors.error() {
|
||||
// You can downcast the error to see if it's a specific type and render something specific for it
|
||||
if let Some(_error) = error.downcast_ref::<std::num::ParseIntError>() {
|
||||
div {
|
||||
background_color: "red",
|
||||
border: "black",
|
||||
border_width: "2px",
|
||||
border_radius: "5px",
|
||||
p { "Failed to parse data" }
|
||||
Link {
|
||||
to: Route::Home {},
|
||||
"Go back to the homepage"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pre {
|
||||
color: "red",
|
||||
@@ -79,24 +90,8 @@ fn ParseNumberWithShow() -> Element {
|
||||
button {
|
||||
onclick: move |_| {
|
||||
let request_data = "0.5";
|
||||
let data: i32 = request_data.parse()
|
||||
// You can attach rsx to results that can be displayed in the Error Boundary
|
||||
.show(|_| rsx! {
|
||||
div {
|
||||
background_color: "red",
|
||||
border: "black",
|
||||
border_width: "2px",
|
||||
border_radius: "5px",
|
||||
p { "Failed to parse data" }
|
||||
Link {
|
||||
to: Route::Home {},
|
||||
"Go back to the homepage"
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
let data: i32 = request_data.parse()?;
|
||||
println!("parsed {data}");
|
||||
|
||||
Ok(())
|
||||
},
|
||||
"Click to throw an error"
|
||||
@@ -18,7 +18,7 @@ fn main() {
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
Increment {}
|
||||
Decrement {}
|
||||
Reset {}
|
||||
File renamed without changes.
@@ -17,7 +17,7 @@ fn app() -> Element {
|
||||
let mut state = use_signal(|| PlayerState { is_playing: false });
|
||||
|
||||
rsx!(
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
h1 {"Select an option"}
|
||||
|
||||
// Add some cute animations if the radio is playing!
|
||||
@@ -68,7 +68,7 @@ fn app() -> Element {
|
||||
}
|
||||
|
||||
// We can also use the signal value as a slice
|
||||
if let [ref first, .., ref last] = saved_values.read().as_slice() {
|
||||
if let [first, .., last] = saved_values.read().as_slice() {
|
||||
li { "First and last: {first}, {last}" }
|
||||
} else {
|
||||
"No saved values"
|
||||
File renamed without changes.
@@ -12,7 +12,7 @@ fn app() -> Element {
|
||||
let mut millis = use_signal(|| 0);
|
||||
|
||||
use_future(move || async move {
|
||||
// Save our initial timea
|
||||
// Save our initial time
|
||||
let start = Instant::now();
|
||||
|
||||
loop {
|
||||
File renamed without changes.
@@ -2,7 +2,7 @@
|
||||
|
||||
use async_std::task::sleep;
|
||||
use dioxus::prelude::*;
|
||||
use futures_util::{future, stream, Stream, StreamExt};
|
||||
use futures_util::{Stream, StreamExt, future, stream};
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
@@ -0,0 +1,62 @@
|
||||
//! Suspense in Dioxus
|
||||
//!
|
||||
//! Suspense allows components to bubble up loading states to parent components, simplifying data fetching.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app)
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
h1 { "Dogs are very important" }
|
||||
p {
|
||||
"The dog or domestic dog (Canis familiaris[4][5] or Canis lupus familiaris[5])"
|
||||
"is a domesticated descendant of the wolf which is characterized by an upturning tail."
|
||||
"The dog derived from an ancient, extinct wolf,[6][7] and the modern grey wolf is the"
|
||||
"dog's nearest living relative.[8] The dog was the first species to be domesticated,[9][8]"
|
||||
"by hunter–gatherers over 15,000 years ago,[7] before the development of agriculture.[1]"
|
||||
}
|
||||
h3 { "Illustrious Dog Photo" }
|
||||
ErrorBoundary { handle_error: |_| rsx! { p { "Error loading doggos" } },
|
||||
SuspenseBoundary { fallback: move |_| rsx! { "Loading doggos..." },
|
||||
Doggo {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Doggo() -> Element {
|
||||
// `use_loader` returns a Result<Loader<T>, Loading>. Loading can either be "Pending" or "Failed".
|
||||
// When we use the `?` operator, the pending/error state will be thrown to the nearest Suspense or Error boundary.
|
||||
//
|
||||
// During SSR, `use_loader` will serialize the contents of the fetch, and during hydration, the client will
|
||||
// use the pre-fetched data instead of re-fetching to render.
|
||||
let mut dog = use_loader(move || async move {
|
||||
#[derive(serde::Deserialize, serde::Serialize, PartialEq)]
|
||||
struct DogApi {
|
||||
message: String,
|
||||
}
|
||||
|
||||
reqwest::get("https://dog.ceo/api/breeds/image/random/")
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<DogApi>()
|
||||
.await
|
||||
})?;
|
||||
|
||||
rsx! {
|
||||
button { onclick: move |_| dog.restart(), "Click to fetch another doggo" }
|
||||
div {
|
||||
img {
|
||||
max_width: "500px",
|
||||
max_height: "500px",
|
||||
src: "{dog.read().message}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ const STYLE: Asset = asset!("/examples/assets/flat_router.css");
|
||||
fn main() {
|
||||
dioxus::launch(|| {
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
Router::<Route> {}
|
||||
}
|
||||
})
|
||||
File renamed without changes.
@@ -16,7 +16,7 @@ fn main() {
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! (
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
Router::<Route> {}
|
||||
)
|
||||
}
|
||||
File renamed without changes.
@@ -13,7 +13,7 @@ const STYLE: Asset = asset!("/examples/assets/router.css");
|
||||
fn main() {
|
||||
dioxus::launch(|| {
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
Router::<Route> {}
|
||||
}
|
||||
});
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,61 @@
|
||||
[package]
|
||||
name = "fullstack-auth-example-with-fullstack"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
|
||||
dioxus = { features = ["fullstack"], workspace = true }
|
||||
axum = { workspace = true, optional = true, features = ["macros"]}
|
||||
tokio = { workspace = true, features = ["full"], optional = true }
|
||||
tower-http = { workspace = true, features = ["auth"], optional = true }
|
||||
async-trait = { version = "0.1.88", optional = true }
|
||||
sqlx = { version = "0.8.6", features = [
|
||||
"macros",
|
||||
"migrate",
|
||||
"postgres",
|
||||
"sqlite",
|
||||
"_unstable-all-types",
|
||||
"tls-native-tls",
|
||||
"runtime-tokio",
|
||||
], optional = true }
|
||||
http = { workspace = true, optional = true }
|
||||
tower = { workspace = true, optional = true }
|
||||
|
||||
execute = "0.2.13"
|
||||
serde = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[dependencies.axum_session]
|
||||
workspace = true
|
||||
optional = true
|
||||
|
||||
[dependencies.axum_session_auth]
|
||||
workspace = true
|
||||
optional = true
|
||||
|
||||
[dependencies.axum_session_sqlx]
|
||||
workspace = true
|
||||
features = ["sqlite"]
|
||||
optional = true
|
||||
|
||||
[features]
|
||||
default = ["web", "server"]
|
||||
server = [
|
||||
"dioxus/server",
|
||||
"dep:axum",
|
||||
"dep:tokio",
|
||||
"dep:tower-http",
|
||||
"dep:async-trait",
|
||||
"dep:sqlx",
|
||||
"dep:axum_session",
|
||||
"dep:axum_session_auth",
|
||||
"dep:axum_session_sqlx",
|
||||
"dep:http",
|
||||
"dep:tower",
|
||||
]
|
||||
web = ["dioxus/web", "dep:dioxus-web"]
|
||||
@@ -0,0 +1,81 @@
|
||||
//! The code here is pulled from the `axum-session-auth` crate examples, requiring little to no
|
||||
//! modification to work with dioxus fullstack.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use axum_session_auth::*;
|
||||
use axum_session_sqlx::SessionSqlitePool;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::sqlite::SqlitePool;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub(crate) type Session = axum_session_auth::AuthSession<User, i64, SessionSqlitePool, SqlitePool>;
|
||||
pub(crate) type AuthLayer =
|
||||
axum_session_auth::AuthSessionLayer<User, i64, SessionSqlitePool, SqlitePool>;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct User {
|
||||
pub id: i32,
|
||||
pub anonymous: bool,
|
||||
pub username: String,
|
||||
pub permissions: HashSet<String>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub(crate) struct SqlPermissionTokens {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Authentication<User, i64, SqlitePool> for User {
|
||||
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
|
||||
let db = pool.unwrap();
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
struct SqlUser {
|
||||
id: i32,
|
||||
anonymous: bool,
|
||||
username: String,
|
||||
}
|
||||
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = $1")
|
||||
.bind(userid)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifying them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
"SELECT token FROM user_permissions WHERE user_id = $1;",
|
||||
)
|
||||
.bind(userid)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(User {
|
||||
id: sqluser.id,
|
||||
anonymous: sqluser.anonymous,
|
||||
username: sqluser.username,
|
||||
permissions: sql_user_perms.into_iter().map(|x| x.token).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn is_authenticated(&self) -> bool {
|
||||
!self.anonymous
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
!self.anonymous
|
||||
}
|
||||
|
||||
fn is_anonymous(&self) -> bool {
|
||||
self.anonymous
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HasPermission<SqlitePool> for User {
|
||||
async fn has(&self, perm: &str, _pool: &Option<&SqlitePool>) -> bool {
|
||||
self.permissions.contains(perm)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
//! This example showcases how to use the `axum-session-auth` crate with Dioxus fullstack.
|
||||
//! We add the `auth::Session` extractor to our server functions to get access to the current user session.
|
||||
//!
|
||||
//! To initialize the axum router, we use `dioxus::serve` to spawn a custom axum server that creates
|
||||
//! our database, session store, and authentication layer.
|
||||
//!
|
||||
//! The `.serve_dioxus_application` method is used to mount our Dioxus app as a fallback service to
|
||||
//! handle HTML rendering and static assets.
|
||||
//!
|
||||
//! We easily share the "permissions" between the server and client by using a `HashSet<String>`
|
||||
//! which is serialized to/from JSON automatically by the server function system.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
mod auth;
|
||||
|
||||
fn main() {
|
||||
// On the client, we simply launch the app as normal, taking over the main thread
|
||||
#[cfg(not(feature = "server"))]
|
||||
dioxus::launch(app);
|
||||
|
||||
// On the server, we can use `dioxus::serve` to create a server that serves our app.
|
||||
//
|
||||
// The `serve` function takes a closure that returns a `Future` which resolves to an `axum::Router`.
|
||||
//
|
||||
// We return a `Router` such that dioxus sets up logging, hot-reloading, devtools, and wires up the
|
||||
// IP and PORT environment variables to our server.
|
||||
#[cfg(feature = "server")]
|
||||
dioxus::serve(|| async {
|
||||
use crate::auth::*;
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_session_auth::AuthConfig;
|
||||
use axum_session_sqlx::SessionSqlitePool;
|
||||
use sqlx::{sqlite::SqlitePoolOptions, Executor};
|
||||
|
||||
// Create an in-memory SQLite database and set up our tables
|
||||
let db = SqlitePoolOptions::new()
|
||||
.max_connections(20)
|
||||
.connect_with("sqlite::memory:".parse()?)
|
||||
.await?;
|
||||
|
||||
// Create the tables (sessions, users)
|
||||
db.execute(r#"CREATE TABLE IF NOT EXISTS users ( "id" INTEGER PRIMARY KEY, "anonymous" BOOLEAN NOT NULL, "username" VARCHAR(256) NOT NULL )"#,)
|
||||
.await?;
|
||||
db.execute(r#"CREATE TABLE IF NOT EXISTS user_permissions ( "user_id" INTEGER NOT NULL, "token" VARCHAR(256) NOT NULL)"#,)
|
||||
.await?;
|
||||
|
||||
// Insert in some test data for two users (one anonymous, one normal)
|
||||
db.execute(r#"INSERT INTO users (id, anonymous, username) SELECT 1, true, 'Guest' ON CONFLICT(id) DO UPDATE SET anonymous = EXCLUDED.anonymous, username = EXCLUDED.username"#,)
|
||||
.await?;
|
||||
db.execute(r#"INSERT INTO users (id, anonymous, username) SELECT 2, false, 'Test' ON CONFLICT(id) DO UPDATE SET anonymous = EXCLUDED.anonymous, username = EXCLUDED.username"#,)
|
||||
.await?;
|
||||
|
||||
// Make sure our test user has the ability to view categories
|
||||
db.execute(r#"INSERT INTO user_permissions (user_id, token) SELECT 2, 'Category::View'"#)
|
||||
.await?;
|
||||
|
||||
// Create an axum router that dioxus will attach the app to
|
||||
Ok(dioxus::server::router(app)
|
||||
.layer(
|
||||
AuthLayer::new(Some(db.clone()))
|
||||
.with_config(AuthConfig::<i64>::default().with_anonymous_user_id(Some(1))),
|
||||
)
|
||||
.layer(SessionLayer::new(
|
||||
SessionStore::<SessionSqlitePool>::new(
|
||||
Some(db.into()),
|
||||
SessionConfig::default().with_table_name("test_table"),
|
||||
)
|
||||
.await?,
|
||||
)))
|
||||
});
|
||||
}
|
||||
|
||||
/// The UI for our app - is just a few buttons to call our server functions and display the results.
|
||||
fn app() -> Element {
|
||||
let mut login = use_action(login);
|
||||
let mut user_name = use_action(get_user_name);
|
||||
let mut permissions = use_action(get_permissions);
|
||||
let mut logout = use_action(logout);
|
||||
|
||||
let fetch_new = move |_| async move {
|
||||
user_name.call().await;
|
||||
permissions.call().await;
|
||||
};
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
login.call().await;
|
||||
},
|
||||
"Login Test User"
|
||||
}
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
logout.call().await;
|
||||
},
|
||||
"Logout"
|
||||
}
|
||||
button {
|
||||
onclick: fetch_new,
|
||||
"Fetch User Info"
|
||||
}
|
||||
|
||||
pre { "Logged in: {login.result():?}" }
|
||||
pre { "User name: {user_name.result():?}" }
|
||||
pre { "Permissions: {permissions.result():?}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// We use the `auth::Session` extractor to get access to the current user session.
|
||||
/// This lets us modify the user session, log in/out, and access the current user.
|
||||
#[post("/api/user/login", auth: auth::Session)]
|
||||
pub async fn login() -> Result<()> {
|
||||
auth.login_user(2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Just like `login`, but this time we log out the user.
|
||||
#[post("/api/user/logout", auth: auth::Session)]
|
||||
pub async fn logout() -> Result<()> {
|
||||
auth.logout_user();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// We can access the current user via `auth.current_user`.
|
||||
/// We can have both anonymous user (id 1) and a logged in user (id 2).
|
||||
///
|
||||
/// Logged-in users will have more permissions which we can modify.
|
||||
#[post("/api/user/name", auth: auth::Session)]
|
||||
pub async fn get_user_name() -> Result<String> {
|
||||
Ok(auth.current_user.unwrap().username)
|
||||
}
|
||||
|
||||
/// Get the current user's permissions, guarding the endpoint with the `Auth` validator.
|
||||
/// If this returns false, we use the `or_unauthorized` extension to return a 401 error.
|
||||
#[get("/api/user/permissions", auth: auth::Session)]
|
||||
pub async fn get_permissions() -> Result<HashSet<String>> {
|
||||
use crate::auth::User;
|
||||
use axum_session_auth::{Auth, Rights};
|
||||
|
||||
let user = auth.current_user.unwrap();
|
||||
|
||||
Auth::<User, i64, sqlx::SqlitePool>::build([axum::http::Method::GET], false)
|
||||
.requires(Rights::any([
|
||||
Rights::permission("Category::View"),
|
||||
Rights::permission("Admin::View"),
|
||||
]))
|
||||
.validate(&user, &axum::http::Method::GET, None)
|
||||
.await
|
||||
.or_unauthorized("You do not have permission to view categories")?;
|
||||
|
||||
Ok(user.permissions)
|
||||
}
|
||||
@@ -10,8 +10,6 @@ publish = false
|
||||
[dependencies]
|
||||
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
|
||||
dioxus = { features = ["fullstack"], workspace = true }
|
||||
dioxus-fullstack = { workspace = true }
|
||||
dioxus-cli-config = { workspace = true, optional = true }
|
||||
axum = { workspace = true, optional = true }
|
||||
tokio = { workspace = true, features = ["full"], optional = true }
|
||||
tower-http = { workspace = true, features = ["auth"], optional = true }
|
||||
@@ -49,7 +47,6 @@ optional = true
|
||||
default = []
|
||||
server = [
|
||||
"dioxus/server",
|
||||
"dep:dioxus-cli-config",
|
||||
"dep:axum",
|
||||
"dep:tokio",
|
||||
"dep:tower-http",
|
||||
@@ -1,23 +1,15 @@
|
||||
use async_trait::async_trait;
|
||||
use axum::{
|
||||
http::Method,
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_session_auth::*;
|
||||
use axum_session_sqlx::SessionSqlitePool;
|
||||
use core::pin::Pin;
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
|
||||
use std::error::Error;
|
||||
use std::future::Future;
|
||||
use std::{collections::HashSet, net::SocketAddr, str::FromStr};
|
||||
use sqlx::sqlite::SqlitePool;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub(crate) type Session =
|
||||
axum_session_auth::AuthSession<User, i64, SessionSqlitePool, sqlx::SqlitePool>;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub(crate) struct User {
|
||||
pub id: i32,
|
||||
pub anonymous: bool,
|
||||
pub username: String,
|
||||
@@ -25,25 +17,10 @@ pub struct User {
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub struct SqlPermissionTokens {
|
||||
pub(crate) struct SqlPermissionTokens {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
impl Default for User {
|
||||
fn default() -> Self {
|
||||
let mut permissions = HashSet::new();
|
||||
|
||||
permissions.insert("Category::View".to_owned());
|
||||
|
||||
Self {
|
||||
id: 1,
|
||||
anonymous: true,
|
||||
username: "Guest".into(),
|
||||
permissions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Authentication<User, i64, SqlitePool> for User {
|
||||
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
|
||||
@@ -76,6 +53,13 @@ impl HasPermission<SqlitePool> for User {
|
||||
|
||||
impl User {
|
||||
pub async fn get_user(id: i64, pool: &SqlitePool) -> Option<Self> {
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
struct SqlUser {
|
||||
id: i32,
|
||||
anonymous: bool,
|
||||
username: String,
|
||||
}
|
||||
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = $1")
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
@@ -91,10 +75,15 @@ impl User {
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
Some(sqluser.into_user(Some(sql_user_perms)))
|
||||
Some(User {
|
||||
id: sqluser.id,
|
||||
anonymous: sqluser.anonymous,
|
||||
username: sqluser.username,
|
||||
permissions: sql_user_perms.into_iter().map(|x| x.token).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_user_tables(pool: &SqlitePool) {
|
||||
pub async fn create_user_tables(pool: &SqlitePool) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
@@ -105,20 +94,17 @@ impl User {
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS user_permissions (
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"token" VARCHAR(256) NOT NULL
|
||||
)
|
||||
"#,
|
||||
)"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -130,8 +116,7 @@ impl User {
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -143,8 +128,7 @@ impl User {
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -153,51 +137,8 @@ impl User {
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub struct SqlUser {
|
||||
pub id: i32,
|
||||
pub anonymous: bool,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
impl SqlUser {
|
||||
pub fn into_user(self, sql_user_perms: Option<Vec<SqlPermissionTokens>>) -> User {
|
||||
User {
|
||||
id: self.id,
|
||||
anonymous: self.anonymous,
|
||||
username: self.username,
|
||||
permissions: if let Some(user_perms) = sql_user_perms {
|
||||
user_perms
|
||||
.into_iter()
|
||||
.map(|x| x.token)
|
||||
.collect::<HashSet<String>>()
|
||||
} else {
|
||||
HashSet::<String>::new()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect_to_database() -> SqlitePool {
|
||||
let connect_opts = SqliteConnectOptions::from_str("sqlite::memory:").unwrap();
|
||||
|
||||
SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect_with(connect_opts)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub type Session =
|
||||
axum_session_auth::AuthSession<crate::auth::User, i64, SessionSqlitePool, sqlx::SqlitePool>;
|
||||
|
||||
pub async fn get_session() -> ServerFnResult<Session> {
|
||||
extract::<Session, _>()
|
||||
.await
|
||||
.map_err(|_| ServerFnError::new("AuthSessionLayer was not found"))
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
#[cfg(feature = "server")]
|
||||
mod auth;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "server")]
|
||||
dioxus::serve(|| async {
|
||||
use crate::auth::*;
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_session_auth::AuthConfig;
|
||||
use axum_session_sqlx::SessionSqlitePool;
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
||||
use std::str::FromStr;
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect_with(SqliteConnectOptions::from_str("sqlite::memory:").unwrap())
|
||||
.await?;
|
||||
|
||||
// This Defaults as normal Cookies.
|
||||
// To enable Private cookies for integrity, and authenticity please check the next Example.
|
||||
let session_config = SessionConfig::default().with_table_name("test_table");
|
||||
let auth_config = AuthConfig::<i64>::default().with_anonymous_user_id(Some(1));
|
||||
let session_store =
|
||||
SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
User::create_user_tables(&pool).await?;
|
||||
|
||||
// build our application with some routes
|
||||
Ok(
|
||||
dioxus::server::router(app)
|
||||
.layer(
|
||||
axum_session_auth::AuthSessionLayer::<
|
||||
User,
|
||||
i64,
|
||||
SessionSqlitePool,
|
||||
sqlx::SqlitePool,
|
||||
>::new(Some(pool))
|
||||
.with_config(auth_config),
|
||||
)
|
||||
.layer(SessionLayer::new(session_store)),
|
||||
)
|
||||
});
|
||||
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut user_name = use_action(get_user_name);
|
||||
let mut permissions = use_action(get_permissions);
|
||||
let mut login = use_action(login);
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
button {
|
||||
onclick: move |_| {
|
||||
login.call();
|
||||
},
|
||||
"Login Test User"
|
||||
}
|
||||
}
|
||||
div {
|
||||
button {
|
||||
onclick: move |_| {
|
||||
user_name.call();
|
||||
},
|
||||
"Get User Name"
|
||||
}
|
||||
"User name: {user_name.value().unwrap_or_default()}"
|
||||
}
|
||||
div {
|
||||
button {
|
||||
onclick: move |_| {
|
||||
permissions.call();
|
||||
},
|
||||
"Get Permissions"
|
||||
}
|
||||
"Permissions: {permissions.value().unwrap_or_default()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/user/name", auth: auth::Session)]
|
||||
pub async fn get_user_name() -> Result<String> {
|
||||
Ok(auth
|
||||
.current_user
|
||||
.context("UNAUTHORIZED")?
|
||||
.username
|
||||
.to_string())
|
||||
}
|
||||
|
||||
#[post("/api/user/login", auth: auth::Session)]
|
||||
pub async fn login() -> Result<()> {
|
||||
auth.login_user(2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[get("/api/user/permissions", auth: auth::Session)]
|
||||
pub async fn get_permissions() -> Result<String> {
|
||||
use crate::auth::User;
|
||||
use axum_session_auth::{Auth, Rights};
|
||||
|
||||
let user = auth.current_user.as_ref().context("UNAUTHORIZED")?;
|
||||
|
||||
// lets check permissions only and not worry about if they are anon or not
|
||||
let has_permissions =
|
||||
Auth::<User, i64, sqlx::SqlitePool>::build([axum::http::Method::POST], false)
|
||||
.requires(Rights::any([
|
||||
Rights::permission("Category::View"),
|
||||
Rights::permission("Admin::View"),
|
||||
]))
|
||||
.validate(user, &axum::http::Method::GET, None)
|
||||
.await;
|
||||
|
||||
if !has_permissions {
|
||||
dioxus::core::bail!(
|
||||
"User {}, Does not have permissions needed to view this page please login",
|
||||
user.username
|
||||
);
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
"User has Permissions needed. {:?}",
|
||||
user.permissions
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//! This example demonstrates how to use `dioxus::serve` with a custom Axum router.
|
||||
//!
|
||||
//! By default, `dioxus::launch` takes over the main thread and runs the Dioxus application.
|
||||
//! However, if you want to integrate Dioxus into an existing web server or use a custom router,
|
||||
//! you can use `dioxus::serve` to create a server that serves your Dioxus application alongside
|
||||
//! other routes.
|
||||
//!
|
||||
//! `dioxus::serve` sets up an async runtime, logging, hot-reloading, crash handling, and more.
|
||||
//! You can then use the `.serve_dioxus_application` method on your router to serve the Dioxus app.
|
||||
//!
|
||||
//! `dioxus::serve` is most useful for customizing the server setup, such as adding middleware,
|
||||
//! custom routes, or integrating with existing axum backend code.
|
||||
//!
|
||||
//! Note that `dioxus::serve` is accepts a Router from `axum`. Dioxus will use the IP and PORT
|
||||
//! environment variables to determine where to bind the server. To customize the port, use environment
|
||||
//! variables or a `.env` file.
|
||||
//!
|
||||
//! On other platforms (like desktop or mobile), you'll want to use `dioxus::launch` instead and then
|
||||
//! handle async loading of data through hooks like `use_future` or `use_resource` and give the user
|
||||
//! a loading state while data is being fetched.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
// On the client we just launch the app as normal.
|
||||
#[cfg(not(feature = "server"))]
|
||||
dioxus::launch(app);
|
||||
|
||||
// On the server, we can use `dioxus::serve` and `.serve_dioxus_application` to serve our app with routing.
|
||||
// The `dioxus::server::router` function creates a new axum Router with the necessary routes to serve the Dioxus app.
|
||||
#[cfg(feature = "server")]
|
||||
dioxus::serve(|| async move {
|
||||
use dioxus::server::axum::routing::{get, post};
|
||||
|
||||
Ok(dioxus::server::router(app)
|
||||
.route("/submit", post(|| async { "Form submitted!" }))
|
||||
.route("/about", get(|| async { "About us" }))
|
||||
.route("/contact", get(|| async { "Contact us" })))
|
||||
});
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! {
|
||||
div { "Hello from Dioxus!" }
|
||||
}
|
||||
}
|
||||
File renamed without changes.
Loaded 100 of 424 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user