diff --git a/.cspell/backend_custom_words.txt b/.cspell/backend_custom_words.txt new file mode 100644 index 000000000..4df68be4e --- /dev/null +++ b/.cspell/backend_custom_words.txt @@ -0,0 +1,36 @@ +tauri +rustup +aarch +sdcore +dotenv +dotenvy +prismjs +actix +rtype +healthcheck +sdserver +ipfs +impls +crdt +quicktime +creationdate +imageops +thumbnailer +HEXLOWER +chrono +walkdir +thiserror +thumbstrip +repr +Deque +oneshot +sdlibrary +sdconfig +DOTFILE +sysinfo +initialising +struct +UHLC +CRDTs +PRRTT +filesystems \ No newline at end of file diff --git a/.cspell/frontend_custom_words.txt b/.cspell/frontend_custom_words.txt new file mode 100644 index 000000000..5506b5162 --- /dev/null +++ b/.cspell/frontend_custom_words.txt @@ -0,0 +1,84 @@ +pnpm +titlebar +consts +pallete +unlisten +svgr +middlewares +clsx +SDWEB +tryghost +tsparticles +Opencollective +Waitlist +heroicons +roadmap +semibold +noreferer +Rescan +subpackage +photoslibrary +fontsource +audiomp +audioogg +audiowav +browserslist +bsconfig +cheader +compodoc +cssmap +dartlang +dockerdebug +folderlight +folderopen +fontotf +fontttf +fontwoff +gopackage +haml +imagegif +imageico +imagejpg +imagepng +ipynb +jsmap +lighteditorconfig +nestjscontroller +nestjs +nestjsdecorator +nestjsfilter +nestjsguard +nestjsmodule +nestjsservice +npmlock +nuxt +opengl +photoshop +postcssconfig +powershelldata +reactjs +rjson +symfony +testjs +tmpl +typescriptdef +windi +yarnerror +unlisten +imagewebp +powershellmodule +reactts +testts +zustand +overscan +webp +headlessui +falsey +nums +lacie +classname +wunsub +immer +tada +moti +pressable \ No newline at end of file diff --git a/.cspell/project_words.txt b/.cspell/project_words.txt new file mode 100644 index 000000000..882cf4dda --- /dev/null +++ b/.cspell/project_words.txt @@ -0,0 +1,42 @@ +spacedrive +spacedriveapp +vdfs +haoyuan +brendonovich +codegen +elon +deel +haden +akar +benja +haris +mehrzad +OSSC +josephjacks +rauch +ravikant +neha +narkhede +allred +lütke +tobiaslutke +justinhoffman +rywalker +zacharysmith +sanjay +poonen +mytton +davidmytton +richelsen +lesterlee +alluxio +augusto +marietti +vijay +sharma +naveen +noco +rspc +rspcws +stringly +specta diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ebfdf771d..bb30b4082 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,9 +8,11 @@ # frontend apps (Rust bridges and tech functionality -- no real visual implications) /apps/desktop/ @jamiepine @Brendonovich @oscartbeaumont -/apps/mobile/ @jamiepine @Brendonovich @oscartbeaumont /apps/web/ @jamiepine @maxichrome +# mobile +/apps/mobile/ @jamiepine @Brendonovich @oscartbeaumont @utkubakir + # core logic /core/ @jamiepine @Brendonovich @oscartbeaumont /packages/macos/ @jamiepine @Brendonovich @oscartbeaumont @@ -22,8 +24,9 @@ /apps/landing/ @jamiepine @maxichrome # UI -/packages/interface/ @jamiepine @maxichrome +/packages/interface/ @jamiepine @maxichrome @utkubakir /packages/ui/ @jamiepine @maxichrome +/packages/assets/ @jamiepine @utkubakir # base config files /* @jamiepine diff --git a/.github/scripts/setup-system.ps1 b/.github/scripts/setup-system.ps1 index 9a8a206b5..cb098db7c 100644 --- a/.github/scripts/setup-system.ps1 +++ b/.github/scripts/setup-system.ps1 @@ -1,10 +1,181 @@ -Write-Host "This script is currently being used by CI and will need some more work before anyone can use it like the 'setup-system.sh' script for macOS and Linux!" +# Get ci parameter to check if running with ci +param( + [Parameter()] + [Switch]$ci +) -$VCINSTALLDIR = $(& "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath) -Add-Content $env:GITHUB_ENV "LIBCLANG_PATH=${VCINSTALLDIR}\VC\Tools\LLVM\x64\bin`n" -Invoke-WebRequest "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full-shared.7z" -OutFile ffmpeg-release-full-shared.7z -7z x ffmpeg-release-full-shared.7z -mkdir ffmpeg -mv ffmpeg-*/* ffmpeg/ -Add-Content $env:GITHUB_ENV "FFMPEG_DIR=${pwd}\ffmpeg`n" -Add-Content $env:GITHUB_PATH "${pwd}\ffmpeg\bin`n" \ No newline at end of file +# Get temp folder +$temp = [System.IO.Path]::GetTempPath() + +# Get current running dir +$currentLocation = $((Get-Location).path) + +# Check to see if a command exists (eg if an app is installed) +Function CheckCommand { + + Param ($command) + + $oldPreference = $ErrorActionPreference + + $ErrorActionPreference = 'stop' + + try { if (Get-Command $command) { RETURN $true } } + + Catch { RETURN $false } + + Finally { $ErrorActionPreference = $oldPreference } + +} + +Write-Host "Spacedrive Development Environment Setup" -ForegroundColor Magenta +Write-Host @" + +To set up your machine for Spacedrive development, this script will do the following: + +1) Check for Rust and Cargo + +2) Install pnpm (if not installed) + +3) Install the latest version of Node.js using pnpm + +4) Install LLVM (compiler for ffmpeg-rust) + +4) Download ffmpeg and set as an environment variable + +"@ + +Write-Host "Checking for Rust and Cargo..." -ForegroundColor Yellow +Start-Sleep -Milliseconds 150 + +$cargoCheck = CheckCommand cargo + +if ($cargoCheck -eq $false) { + Write-Host @" +Cargo is not installed. + +To use Spacedrive on Windows, Cargo needs to be installed. +The Visual Studio C++ Build tools are also required. +Instructions can be found here: + +https://tauri.app/v1/guides/getting-started/prerequisites/#setting-up-windows + +Once you have installed Cargo, re-run this script. + +"@ + Exit +} +else { + Write-Host "Cargo is installed." +} + +Write-Host +Write-Host "Checking for pnpm..." -ForegroundColor Yellow +Start-Sleep -Milliseconds 150 + +$pnpmCheck = CheckCommand pnpm +if ($pnpmCheck -eq $false) { + + Write-Host "pnpm is not installed. Installing now." + Write-Host "Running the pnpm installer..." + + #pnpm installer taken from https://pnpm.io + Invoke-WebRequest https://get.pnpm.io/install.ps1 -useb | Invoke-Expression + + # Reset the PATH env variables to make sure pnpm is accessible + $env:PNPM_HOME = [System.Environment]::GetEnvironmentVariable("PNPM_HOME", "User") + $env:Path = [System.Environment]::ExpandEnvironmentVariables([System.Environment]::GetEnvironmentVariable("Path", "User")) + +} +else { + Write-Host "pnpm is installed." +} + +# A GitHub Action takes care of installing node, so this isn't necessary if running in the ci. +if ($ci -eq $True) { + Write-Host + Write-Host "Running with Ci, skipping Node install." -ForegroundColor Yellow +} +else { + Write-Host + Write-Host "Using pnpm to install the latest version of Node..." -ForegroundColor Yellow + Write-Host "This will set your global Node version to the latest!" + Start-Sleep -Milliseconds 150 + + # Runs the pnpm command to use the latest version of node, which also installs it + Start-Process -Wait -FilePath "pnpm" -ArgumentList "env use --global latest" -PassThru -Verb runAs +} + + + +# The ci has LLVM installed already, so we instead just set the env variables. +if ($ci -eq $True) { + Write-Host + Write-Host "Running with Ci, skipping LLVM install." -ForegroundColor Yellow + + $VCINSTALLDIR = $(& "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath) + Add-Content $env:GITHUB_ENV "LIBCLANG_PATH=${VCINSTALLDIR}\VC\Tools\LLVM\x64\bin`n" + +} else { + Write-Host + Write-Host "Downloading the LLVM installer..." -ForegroundColor Yellow + # Downloads latest installer for LLVM + $filenamePattern = "*-win64.exe" + $releasesUri = "https://api.github.com/repos/llvm/llvm-project/releases/latest" + $downloadUri = ((Invoke-RestMethod -Method GET -Uri $releasesUri).assets | Where-Object name -like $filenamePattern ).browser_download_url + + Start-BitsTransfer -Source $downloadUri -Destination "$temp\llvm.exe" + + Write-Host + Write-Host "Running the LLVM installer..." -ForegroundColor Yellow + Write-Host "Please follow the instructions to install LLVM." + Write-Host "Ensure you add LLVM to your PATH." + + Start-Process "$temp\llvm.exe" -Wait +} + + + +Write-Host +Write-Host "Downloading the latest ffmpeg build..." -ForegroundColor Yellow + +# Downloads the latest shared build of ffmpeg from GitHub +# $filenamePattern = "*-full_build-shared.zip" +# $releasesUri = "https://api.github.com/repos/GyanD/codexffmpeg/releases/latest" +$downloadUri = "https://github.com/GyanD/codexffmpeg/releases/download/5.0.1/ffmpeg-5.0.1-full_build-shared.zip" # ((Invoke-RestMethod -Method GET -Uri $releasesUri).assets | Where-Object name -like $filenamePattern ).browser_download_url +$filename = "ffmpeg-5.0.1-full_build-shared.zip" # ((Invoke-RestMethod -Method GET -Uri $releasesUri).assets | Where-Object name -like $filenamePattern ).name +$remove = ".zip" +$foldername = $filename.Substring(0, ($filename.Length - $remove.Length)) + +Start-BitsTransfer -Source $downloadUri -Destination "$temp\ffmpeg.zip" + +Write-Host +Write-Host "Expanding ffmpeg zip..." -ForegroundColor Yellow + +Expand-Archive "$temp\ffmpeg.zip" $HOME -ErrorAction SilentlyContinue + +Remove-Item "$temp\ffmpeg.zip" + +Write-Host +Write-Host "Setting environment variables..." -ForegroundColor Yellow + +if ($ci -eq $True) { + # If running in ci, we need to use GITHUB_ENV and GITHUB_PATH instead of the normal PATH env variables, so we set them here + Add-Content $env:GITHUB_ENV "FFMPEG_DIR=$HOME\$foldername`n" + Add-Content $env:GITHUB_PATH "$HOME\$foldername\bin`n" +} +else { + # Sets environment variable for ffmpeg + [System.Environment]::SetEnvironmentVariable('FFMPEG_DIR', "$HOME\$foldername", [System.EnvironmentVariableTarget]::User) +} + +Write-Host +Write-Host "Copying Required .dll files..." -ForegroundColor Yellow + +# Create target\debug folder, continue if already exists +New-Item -Path $currentLocation\target\debug -ItemType Directory -ErrorAction SilentlyContinue + +# Copies all .dll required for rust-ffmpeg to target\debug folder +Get-ChildItem "$HOME\$foldername\bin" -recurse -filter *.dll | Copy-Item -Destination "$currentLocation\target\debug" + +Write-Host +Write-Host "Your machine has been setup for Spacedrive development!" diff --git a/.github/scripts/setup-system.sh b/.github/scripts/setup-system.sh index a3e557318..433f73112 100755 --- a/.github/scripts/setup-system.sh +++ b/.github/scripts/setup-system.sh @@ -2,17 +2,23 @@ set -e +script_failure() { + echo "An error occurred while performing the task on line $1" >&2 + echo "Setup for Spacedrive development failed" >&2 +} + +trap 'script_failure $LINENO' ERR + echo "Setting up your system for Spacedrive development!" -which cargo &> /dev/null -if [ $? -eq 1 ]; then +if ! which cargo &> /dev/null; then echo "Rust was not detected on your system. Ensure the 'rustc' and 'cargo' binaries are in your \$PATH." exit 1 fi if [ "${SPACEDRIVE_SKIP_PNPM_CHECK:-}" != "true" ]; then - which pnpm &> /dev/null - if [ $? -eq 1 ]; then + + if ! which pnpm &> /dev/null; then echo "PNPM was not detected on your system. Ensure the 'pnpm' command is in your \$PATH. You are not able to use Yarn or NPM." exit 1 fi @@ -20,6 +26,42 @@ else echo "Skipped PNPM check!" fi +if [ "$1" == "mobile" ]; then + echo "Setting up for mobile development!" + + # IOS targets + if [[ "$OSTYPE" == "darwin"* ]]; then + echo "Installing IOS Rust targets..." + + if ! /usr/bin/xcodebuild -version; then + echo "Xcode is not installed! Ensure you have it installed!" + exit 1 + fi + + rustup target add aarch64-apple-ios + fi + + # Android requires python + if ! command -v python3 &> /dev/null + then + echo "Python3 could not be found. This is required for Android mobile development!" + exit 1 + fi + + # Android targets + echo "Installing Android Rust targets..." + rustup target add armv7-linux-androideabi # for arm + rustup target add i686-linux-android # for x86 + rustup target add aarch64-linux-android # for arm64 + rustup target add x86_64-linux-android # for x86_64 + rustup target add x86_64-unknown-linux-gnu # for linux-x86-64 + rustup target add x86_64-apple-darwin # for darwin x86_64 (if you have an Intel MacOS) + rustup target add aarch64-apple-darwin # for darwin arm64 (if you have a M1 MacOS) + rustup target add x86_64-pc-windows-gnu # for win32-x86-64-gnu + rustup target add x86_64-pc-windows-msvc # for win32-x86-64-msvc +fi + + if [[ "$OSTYPE" == "linux-gnu"* ]]; then if which apt-get &> /dev/null; then echo "Detected 'apt' based distro!" @@ -29,7 +71,7 @@ if [[ "$OSTYPE" == "linux-gnu"* ]]; then else DEBIAN_FFMPEG_DEPS="libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev libavresample-dev libavutil-dev libswscale-dev libswresample-dev ffmpeg" # FFMPEG dependencies fi - DEBIAN_TAURI_DEPS="libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libappindicator3-dev librsvg2-dev" # Tauri dependencies + DEBIAN_TAURI_DEPS="libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev" # Tauri dependencies DEBIAN_BINDGEN_DEPS="pkg-config clang" # Bindgen dependencies - it's used by a dependency of Spacedrive sudo apt-get -y update @@ -58,9 +100,16 @@ if [[ "$OSTYPE" == "linux-gnu"* ]]; then echo "Your machine has been setup for Spacedrive development!" elif [[ "$OSTYPE" == "darwin"* ]]; then - brew install ffmpeg + if ! brew tap | grep spacedriveapp/deps > /dev/null; then + brew tap-new spacedriveapp/deps > /dev/null + fi + brew extract --force --version 5.0.1 ffmpeg spacedriveapp/deps > /dev/null + brew unlink ffmpeg &> /dev/null || true + brew install spacedriveapp/deps/ffmpeg@5.0.1 &> /dev/null + + echo "ffmpeg v5.0.1 has been installed and is now being used on your system." else echo "Your OS '$OSTYPE' is not supported by this script. We would welcome a PR or some help adding your OS to this script. https://github.com/spacedriveapp/spacedrive/issues" exit 1 -fi \ No newline at end of file +fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 013354f45..d78d7b382 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: id: pnpm-cache run: | echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" - + - uses: actions/cache@v3 name: Setup pnpm cache with: @@ -44,7 +44,7 @@ jobs: key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - + - name: Install pnpm dependencies run: pnpm --frozen-lockfile i @@ -81,7 +81,7 @@ jobs: with: version: 7 run_install: false - + - name: Install Rust stable uses: actions-rs/toolchain@v1 with: @@ -89,7 +89,7 @@ jobs: profile: minimal override: true components: rustfmt, rust-src - + - name: Cache Rust Dependencies uses: Swatinem/rust-cache@v1 with: @@ -98,10 +98,10 @@ jobs: - name: Run 'setup-system.sh' script if: matrix.platform == 'ubuntu-latest' || matrix.platform == 'macos-latest' run: ./.github/scripts/setup-system.sh - + - name: Run 'setup-system.ps1' script if: matrix.platform == 'windows-latest' - run: ./.github/scripts/setup-system.ps1 + run: ./.github/scripts/setup-system.ps1 -ci - name: Get pnpm store directory id: pnpm-cache @@ -116,7 +116,7 @@ jobs: ${{ runner.os }}-pnpm-store- - name: Install pnpm dependencies run: pnpm --frozen-lockfile i - + - name: Cache Prisma codegen id: cache-prisma uses: actions/cache@v3 @@ -127,13 +127,13 @@ jobs: - name: Generate Prisma client working-directory: core if: steps.cache-prisma.outputs.cache-hit != 'true' - run: cargo run --frozen -p prisma-cli --release -- generate + run: cargo run -p prisma-cli --release -- generate - name: Cargo fetch run: cargo fetch - name: Check Core - run: cargo check --frozen -p sdcore --release + run: cargo check -p sdcore --release - name: Bundle Desktop run: pnpm desktop tauri build @@ -141,7 +141,7 @@ jobs: - name: Build Server if: matrix.platform == 'ubuntu-latest' run: | - cargo build --frozen -p server --release + cargo build -p server --release cp ./target/release/server ./apps/server/server - name: Determine image name & tag diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index 1a11968c4..3de8c04df 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -47,10 +47,10 @@ jobs: - name: Generate Prisma client working-directory: core if: steps.cache-prisma.outputs.cache-hit != 'true' - run: cargo run --frozen -p prisma-cli --release -- generate - + run: cargo run -p prisma-cli --release -- generate + - name: Run Clippy uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features \ No newline at end of file + args: --all-features diff --git a/.github/workflows/org-readme.yml b/.github/workflows/org-readme.yml index fbf405dc7..10d6a3dfc 100644 --- a/.github/workflows/org-readme.yml +++ b/.github/workflows/org-readme.yml @@ -18,7 +18,7 @@ jobs: - name: Update README uses: dmnemec/copy_file_to_another_repo_action@main env: - API_TOKEN_GITHUB: ${{ secrets.REPOS_PAT }} + API_TOKEN_GITHUB: ${{ secrets.SD_BOT_PAT }} with: source_file: 'README.md' destination_repo: 'spacedriveapp/.github' diff --git a/.gitignore b/.gitignore index 53da2a22b..500449750 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules .next dist +!apps/desktop/dist *.tsbuildinfo package-lock.json .eslintcache @@ -15,7 +16,6 @@ storybook-static/ cache .env vendor/ -dist data node_modules packages/turbo-server/data/ @@ -61,4 +61,4 @@ todos.md examples/*/*.lock /target -/sdserver_data \ No newline at end of file +/sdserver_data diff --git a/apps/desktop/dist/.placeholder b/.prettierignore similarity index 100% rename from apps/desktop/dist/.placeholder rename to .prettierignore diff --git a/.vscode/settings.json b/.vscode/settings.json index cedc4f182..a21f04485 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,7 @@ "ipfs", "Keepsafe", "nodestate", + "overscan", "pathctx", "prismjs", "proptype", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 942c66d63..26e2000ab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,27 +37,43 @@ This project uses [Cargo](https://doc.rust-lang.org/cargo/getting-started/instal > Note: MacOS M1 users should choose the customize option in the rustup init script and enter `x86_64-apple-darwin` as the default host triple instead of the default `aarch64-apple-darwin` -- `$ git clone https://github.com/spacedriveapp/spacedrive` -- `$ cd spacedrive` +- `git clone https://github.com/spacedriveapp/spacedrive` +- `cd spacedrive` - For Linux or MacOS users run: `./.github/scripts/setup-system.sh` - This will install FFMPEG and any other required dependencies for Spacedrive to build. -- `$ pnpm i` -- `$ pnpm prep` - Runs all necessary codegen & builds required dependencies. +- For Windows users run using PowerShell: `.\.github\scripts\setup-system.ps1` + - This will install pnpm, LLVM, FFMPEG and any other required dependencies for Spacedrive to build. + - Ensure you run it like documented above as it expects it is executed from the root of the repository. +- `pnpm i` +- `pnpm prep` - Runs all necessary codegen & builds required dependencies. To quickly run only the desktop app after `prep` you can use: -- `$ pnpm desktop dev` +- `pnpm desktop dev` To run the landing page -- `$ pnpm web dev` - runs the web app for the embed -- `$ pnpm landing dev` +- `pnpm web dev` - runs the web app for the embed +- `pnpm landing dev` If you are having issues ensure you are using the following versions of Rust and Node: -- Rust version: **1.60.0** +- Rust version: **1.63.0** - Node version: **17** +##### Mobile app + +To run mobile app + +- Install [Android Studio](https://developer.android.com/studio) for Android and [Xcode](https://apps.apple.com/au/app/xcode/id497799835) for IOS development +- `./.github/scripts/setup-system.sh mobile` + - The should setup most of the dependencies for the mobile app to build. +- You must also ensure [you must have NDK 24.0.8215888 and CMake](https://developer.android.com/studio/projects/install-ndk#default-version) in Android Studio +- `cd apps/mobile && pnpm i` - This is a separate workspace, you need to do this! +- `pnpm android` - runs on Android Emulator +- `pnpm ios` - runs on iOS Emulator +- `pnpm dev` - For already bundled app - This is only temporarily supported. The final app will require the Spacedrive Rust code which isn't included in Expo Go. + ### Pull Request When you're finished with the changes, create a pull request, also known as a PR. @@ -76,6 +92,16 @@ Congratulations :tada::tada: The Spacedrive team thanks you :sparkles:. Once your PR is merged, your contributions will be included in the next release of the application. +### Common Errors + +#### `xcrun: error: unable to find utility "xctest", not a developer tool or in PATH` + +You either don't have Xcode installed, or don't have the Xcode command line tools in your `PATH`. + +- Install XCode from the Mac App Store +- Run `xcode-select -s /Applications/Xcode.app/Contents/Developer`. + This will use Xcode's developer tools instead of macOS's default tools. + ### Credits This CONTRIBUTING.md file was modelled after the [github/docs CONTRIBUTING.md](https://github.com/github/docs/blob/main/CONTRIBUTING.md) file, and we thank the original author. diff --git a/Cargo.lock b/Cargo.lock index ca71bf05e..a61940408 100644 Binary files a/Cargo.lock and b/Cargo.lock differ diff --git a/Cargo.toml b/Cargo.toml index 5b06d38b2..507f92891 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,13 @@ [workspace] +resolver = "2" members = [ "apps/desktop/src-tauri", + "apps/mobile/rust", "core", "core/prisma", - "core/derive", "apps/server" ] + +[patch.crates-io] +# We use this patch so we can compile for the IOS simulator on M1 +openssl-sys = { git = "https://github.com/spacedriveapp/rust-openssl" } \ No newline at end of file diff --git a/README.md b/README.md index ae9203fd8..36891b3e6 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ For independent creatives, hoarders and those that want to own their digital foo # What is a VDFS? -A VDFS (virtual distributed filesystem) is a filesystem designed to work across a variety of storage layers. With a uniform API to manipulate and access content across many devices, VSFS is not restricted to a single machine. It achieves this by maintaining a virtual index of all storage locations, synchronizing the database between clients in realtime. This implementation also uses [CAS](https://en.wikipedia.org/wiki/Content-addressable_storage) (Content-addressable storage) to uniquely identify files, while keeping record of logical file paths relative to the storage locations. +A VDFS (virtual distributed filesystem) is a filesystem designed to work across a variety of storage layers. With a uniform API to manipulate and access content across many devices, VDFS is not restricted to a single machine. It achieves this by maintaining a virtual index of all storage locations, synchronizing the database between clients in realtime. This implementation also uses [CAS](https://en.wikipedia.org/wiki/Content-addressable_storage) (Content-addressable storage) to uniquely identify files, while keeping record of logical file paths relative to the storage locations. The first implementation of a VDFS can be found in this UC Berkeley [paper](https://www2.eecs.berkeley.edu/Pubs/TechRpts/2018/EECS-2018-29.pdf) by Haoyuan Li. This paper describes its use for cloud computing, however the underlying concepts can be translated to open consumer software. @@ -85,7 +85,7 @@ _Note: Links are for highlight purposes only until feature specific documentatio **To be developed (MVP):** - **[Photos](#features)** - Photo and video albums similar to Apple/Google photos. -- **[Search](#features)** - Deep search into your filesystem with a keybind, including offline locations. +- **[Search](#features)** - Deep search into your filesystem with a keybinding, including offline locations. - **[Tags](#features)** - Define routines on custom tags to automate workflows, easily tag files individually, in bulk and automatically via rules. - **[Extensions](#features)** - Build tools on top of Spacedrive, extend functionality and integrate third party services. Extension directory on [spacedrive.com/extensions](#features). @@ -124,16 +124,16 @@ This project is using what I'm calling the **"PRRTT"** stack (Prisma, Rust, Reac ### Core: -- `core`: The [Rust](#) core, referred to internally as `sdcore`. Contains filesystem, database and networking logic. Can be deployed in a variety of host applications. +- `core`: The [Rust](https://www.rust-lang.org) core, referred to internally as `sdcore`. Contains filesystem, database and networking logic. Can be deployed in a variety of host applications. ### Packages: -- `client`: A [TypeScript](#) client library to handle dataflow via RPC between UI and the Rust core. -- `ui`: A [React](<[#](https://reactjs.org)>) Shared component library. +- `client`: A [TypeScript](https://www.typescriptlang.org/) client library to handle dataflow via RPC between UI and the Rust core. +- `ui`: A [React](https://reactjs.org) Shared component library. - `interface`: The complete user interface in React (used by apps `desktop`, `web` and `landing`) - `config`: `eslint` configurations (includes `eslint-config-next`, `eslint-config-prettier` and all `tsconfig.json` configs used throughout the monorepo. -- `macos`: A [Swift](#) Native binary for MacOS system extensions. -- `ios`: A [Swift](#) Native binary (planned). -- `windows`: A [C#](#) Native binary (planned). -- `android`: A [Kotlin](#) Native binary (planned). +- `macos`: A [Swift](https://developer.apple.com/swift/) Native binary for MacOS system extensions. +- `ios`: A [Swift](https://developer.apple.com/swift/) Native binary (planned). +- `windows`: A [C#](https://docs.microsoft.com/en-us/dotnet/csharp/) Native binary (planned). +- `android`: A [Kotlin](https://kotlinlang.org/) Native binary (planned). diff --git a/apps/desktop/dist/.gitignore b/apps/desktop/dist/.gitignore new file mode 100644 index 000000000..c53272268 --- /dev/null +++ b/apps/desktop/dist/.gitignore @@ -0,0 +1,5 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore +# This is done so that Tauri never complains that '../dist does not exist' diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 7218ea08f..7b6dfae6f 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -11,31 +11,33 @@ "build": "tauri build" }, "dependencies": { + "@rspc/client": "^0.0.5", "@sd/client": "workspace:*", "@sd/core": "workspace:*", "@sd/interface": "workspace:*", "@sd/ui": "workspace:*", - "@tauri-apps/api": "1.0.0", - "react": "^18.1.0", - "react-dom": "^18.1.0" + "@tanstack/react-query": "^4.0.10", + "@tauri-apps/api": "1.0.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "devDependencies": { - "@tauri-apps/cli": "1.0.0", + "@tauri-apps/cli": "1.0.5", "@tauri-apps/tauricon": "github:tauri-apps/tauricon", "@types/babel-core": "^6.25.7", "@types/byte-size": "^8.1.0", - "@types/react": "^18.0.9", - "@types/react-dom": "^18.0.5", + "@types/react": "^18.0.15", + "@types/react-dom": "^18.0.6", "@types/react-router-dom": "^5.3.3", "@types/react-window": "^1.8.5", - "@types/tailwindcss": "^3.0.10", - "@vitejs/plugin-react": "^1.3.2", - "concurrently": "^7.2.1", - "prettier": "^2.6.2", - "sass": "^1.52.1", - "typescript": "^4.7.2", - "vite": "^2.9.9", + "@types/tailwindcss": "^3.1.0", + "@vitejs/plugin-react": "^2.0.0", + "concurrently": "^7.3.0", + "prettier": "^2.7.1", + "sass": "^1.54.0", + "typescript": "^4.7.4", + "vite": "^3.0.3", "vite-plugin-filter-replace": "^0.1.9", - "vite-plugin-svgr": "^2.1.0" + "vite-plugin-svgr": "^2.2.1" } } diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index ab91a3359..9bbcfb57c 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -9,25 +9,20 @@ default-run = "spacedrive" edition = "2021" build = "build.rs" -[build-dependencies] -tauri-build = { version = "1.0.0", features = [] } - [dependencies] -# Project dependencies -tauri = { version = "1.0.0", features = ["api-all", "macos-private-api"] } +tauri = { version = "1.0.4", features = ["api-all", "macos-private-api"] } +rspc = { version = "0.0.4", features = ["tauri"] } sdcore = { path = "../../../core" } -# tauri-plugin-shadows = { git = "https://github.com/tauri-apps/tauri-plugin-shadows", features = ["tauri-impl"] } - -# Universal Dependencies tokio = { version = "1.17.0", features = ["sync"] } window-shadows = "0.1.2" -env_logger = "0.9.0" -dotenvy = "0.15.1" +tracing = "0.1.35" -# macOS system libs [target.'cfg(target_os = "macos")'.dependencies] swift-rs = { git = "https://github.com/Brendonovich/swift-rs.git", branch = "autorelease" } +[build-dependencies] +tauri-build = { version = "1.0.0", features = [] } + [target.'cfg(target_os = "macos")'.build-dependencies] swift-rs = { git = "https://github.com/Brendonovich/swift-rs.git", branch = "autorelease", features = ["build"] } diff --git a/apps/desktop/src-tauri/rustfmt.toml b/apps/desktop/src-tauri/rustfmt.toml index a231bfab7..054510e14 100644 --- a/apps/desktop/src-tauri/rustfmt.toml +++ b/apps/desktop/src-tauri/rustfmt.toml @@ -10,4 +10,3 @@ merge_derives = true use_try_shorthand = false use_field_init_shorthand = false force_explicit_abi = true -imports_granularity = "Crate" diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 3dcad488c..8ae8ba8e3 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -1,41 +1,12 @@ -use std::time::{Duration, Instant}; +use std::path::PathBuf; -use dotenvy::dotenv; -use sdcore::{ClientCommand, ClientQuery, CoreController, CoreEvent, CoreResponse, Node}; -use tauri::api::path; -use tauri::Manager; +use sdcore::Node; +use tauri::{api::path, Manager, RunEvent}; +use tracing::{debug, error}; #[cfg(target_os = "macos")] mod macos; mod menu; -#[tauri::command(async)] -async fn client_query_transport( - core: tauri::State<'_, CoreController>, - data: ClientQuery, -) -> Result { - match core.query(data).await { - Ok(response) => Ok(response), - Err(err) => { - println!("query error: {:?}", err); - Err(err.to_string()) - } - } -} - -#[tauri::command(async)] -async fn client_command_transport( - core: tauri::State<'_, CoreController>, - data: ClientCommand, -) -> Result { - match core.command(data).await { - Ok(response) => Ok(response), - Err(err) => { - println!("command error: {:?}", err); - Err(err.to_string()) - } - } -} - #[tauri::command(async)] async fn app_ready(app_handle: tauri::AppHandle) { let window = app_handle.get_window("main").unwrap(); @@ -45,24 +16,17 @@ async fn app_ready(app_handle: tauri::AppHandle) { #[tokio::main] async fn main() { - dotenv().ok(); - env_logger::init(); + let data_dir = path::data_dir() + .unwrap_or_else(|| PathBuf::from("./")) + .join("spacedrive"); - let data_dir = path::data_dir().unwrap_or(std::path::PathBuf::from("./")); - // create an instance of the core - let (mut node, mut event_receiver) = Node::new(data_dir).await; - // run startup tasks - node.initializer().await; - // extract the node controller - let controller = node.get_controller(); - // throw the node into a dedicated thread - tokio::spawn(async move { - node.start().await; - }); - // create tauri app - tauri::Builder::default() - // pass controller to the tauri state manager - .manage(controller) + let (node, router) = Node::new(data_dir).await; + + let app = tauri::Builder::default() + .plugin(rspc::integrations::tauri::plugin(router, { + let node = node.clone(); + move || node.get_request_context() + })) .setup(|app| { let app = app.handle(); @@ -89,35 +53,29 @@ async fn main() { } }); - // core event transport - tokio::spawn(async move { - let mut last = Instant::now(); - // handle stream output - while let Some(event) = event_receiver.recv().await { - match event { - CoreEvent::InvalidateQueryDebounced(_) => { - let current = Instant::now(); - if current.duration_since(last) > Duration::from_millis(1000 / 60) { - last = current; - app.emit_all("core_event", &event).unwrap(); - } - } - event => { - app.emit_all("core_event", &event).unwrap(); - } - } - } - }); - Ok(()) }) - .on_menu_event(|event| menu::handle_menu_event(event)) - .invoke_handler(tauri::generate_handler![ - client_query_transport, - client_command_transport, - app_ready, - ]) + .on_menu_event(menu::handle_menu_event) + .invoke_handler(tauri::generate_handler![app_ready,]) .menu(menu::get_menu()) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + .build(tauri::generate_context!()) + .expect("error while building tauri application"); + + app.run(move |app_handler, event| { + if let RunEvent::ExitRequested { .. } = event { + debug!("Closing all open windows..."); + app_handler + .windows() + .iter() + .for_each(|(window_name, window)| { + debug!("closing window: {window_name}"); + if let Err(e) = window.close() { + error!("failed to close window '{}': {:#?}", window_name, e); + } + }); + + node.shutdown(); + app_handler.exit(0); + } + }) } diff --git a/apps/desktop/src-tauri/src/menu.rs b/apps/desktop/src-tauri/src/menu.rs index 8e501161b..acc467b59 100644 --- a/apps/desktop/src-tauri/src/menu.rs +++ b/apps/desktop/src-tauri/src/menu.rs @@ -1,6 +1,8 @@ use std::env::consts; -use tauri::{AboutMetadata, CustomMenuItem, Menu, MenuItem, Submenu, WindowMenuEvent, Wry}; +use tauri::{ + AboutMetadata, CustomMenuItem, Manager, Menu, MenuItem, Submenu, WindowMenuEvent, Wry, +}; pub(crate) fn get_menu() -> Menu { match consts::OS { @@ -35,12 +37,14 @@ fn custom_menu_bar() -> Menu { ); let edit_menu = Menu::new() .add_native_item(MenuItem::Copy) - .add_native_item(MenuItem::Paste); + .add_native_item(MenuItem::Paste) + .add_native_item(MenuItem::SelectAll); let view_menu = Menu::new() - .add_item( - CustomMenuItem::new("command_pallete".to_string(), "Command Pallete") - .accelerator("CmdOrCtrl+P"), - ) + .add_item(CustomMenuItem::new("search".to_string(), "Search").accelerator("CmdOrCtrl+L")) + // .add_item( + // CustomMenuItem::new("command_pallete".to_string(), "Command Pallete") + // .accelerator("CmdOrCtrl+P"), + // ) .add_item(CustomMenuItem::new("layout".to_string(), "Layout").disabled()); let window_menu = Menu::new().add_native_item(MenuItem::EnterFullScreen); @@ -53,28 +57,25 @@ fn custom_menu_bar() -> Menu { CustomMenuItem::new("reload_app".to_string(), "Reload").accelerator("CmdOrCtrl+R"), ); - let view_menu = view_menu.add_item( + view_menu.add_item( CustomMenuItem::new("toggle_devtools".to_string(), "Toggle Developer Tools") .accelerator("CmdOrCtrl+Alt+I"), - ); - - view_menu + ) }; - let menu = Menu::new() + Menu::new() .add_submenu(Submenu::new("Spacedrive", app_menu)) .add_submenu(Submenu::new("File", file_menu)) .add_submenu(Submenu::new("Edit", edit_menu)) .add_submenu(Submenu::new("View", view_menu)) - .add_submenu(Submenu::new("Window", window_menu)); - - menu + .add_submenu(Submenu::new("Window", window_menu)) } pub(crate) fn handle_menu_event(event: WindowMenuEvent) { match event.menu_item_id() { "quit" => { - std::process::exit(0); + let app = event.window().app_handle(); + app.exit(0); } "close" => { let window = event.window(); @@ -82,7 +83,6 @@ pub(crate) fn handle_menu_event(event: WindowMenuEvent) { #[cfg(debug_assertions)] if window.is_devtools_open() { window.close_devtools(); - return; } else { window.close().unwrap(); } @@ -91,17 +91,17 @@ pub(crate) fn handle_menu_event(event: WindowMenuEvent) { window.close().unwrap(); } "reload_app" => { - event - .window() - .with_webview(|webview| { - #[cfg(target_os = "macos")] - { + #[cfg(target_os = "macos")] + { + event + .window() + .with_webview(|webview| { use crate::macos::reload_webview; reload_webview(webview.inner() as _); - } - }) - .unwrap(); + }) + .unwrap(); + } } #[cfg(debug_assertions)] "toggle_devtools" => { diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 5ebb80265..7c5bfc74c 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -63,7 +63,7 @@ "windows": [ { "title": "Spacedrive", - "width": 1200, + "width": 1400, "height": 725, "minWidth": 700, "minHeight": 500, diff --git a/apps/desktop/src-tauri/tauri.linux.conf.json b/apps/desktop/src-tauri/tauri.linux.conf.json index 51b5a339d..5fc781e7f 100644 --- a/apps/desktop/src-tauri/tauri.linux.conf.json +++ b/apps/desktop/src-tauri/tauri.linux.conf.json @@ -15,7 +15,13 @@ "active": true, "targets": "all", "identifier": "com.spacedrive.desktop", - "icon": ["icons/icon.icns"], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], "resources": [], "externalBin": [], "copyright": "Spacedrive Technology Inc.", diff --git a/apps/desktop/src/index.tsx b/apps/desktop/src/index.tsx index 692f7149e..d1f54a364 100644 --- a/apps/desktop/src/index.tsx +++ b/apps/desktop/src/index.tsx @@ -1,10 +1,7 @@ // import Spacedrive JS client -import { BaseTransport } from '@sd/client'; -// import types from Spacedrive core (TODO: re-export from client would be cleaner) -import { ClientCommand, ClientQuery, CoreEvent } from '@sd/core'; -// import Spacedrive interface +import { TauriTransport, createClient } from '@rspc/client'; +import { Operations, queryClient, rspc } from '@sd/client'; import SpacedriveInterface, { Platform } from '@sd/interface'; -// import tauri apis import { dialog, invoke, os, shell } from '@tauri-apps/api'; import { Event, listen } from '@tauri-apps/api/event'; import { convertFileSrc } from '@tauri-apps/api/tauri'; @@ -14,22 +11,9 @@ import { createRoot } from 'react-dom/client'; import '@sd/ui/style'; -// bind state to core via Tauri -class Transport extends BaseTransport { - constructor() { - super(); - - listen('core_event', (e: Event) => { - this.emit('core_event', e.payload); - }); - } - async query(query: ClientQuery) { - return await invoke('client_query_transport', { data: query }); - } - async command(query: ClientCommand) { - return await invoke('client_command_transport', { data: query }); - } -} +const client = createClient({ + transport: new TauriTransport() +}); function App() { function getPlatform(platform: string): Platform { @@ -45,7 +29,7 @@ function App() { } } - const [platform, setPlatform] = useState('macOS'); + const [platform, setPlatform] = useState('unknown'); const [focused, setFocused] = useState(true); useEffect(() => { @@ -64,23 +48,24 @@ function App() { }, []); return ( - { - return dialog.open(options); - }} - isFocused={focused} - onClose={() => appWindow.close()} - onFullscreen={() => appWindow.setFullscreen(true)} - onMinimize={() => appWindow.minimize()} - onOpen={(path: string) => shell.open(path)} - /> + + { + return dialog.open(options); + }} + isFocused={focused} + onClose={() => appWindow.close()} + onFullscreen={() => appWindow.setFullscreen(true)} + onMinimize={() => appWindow.minimize()} + onOpen={(path: string) => shell.open(path)} + /> + ); } diff --git a/apps/mobile/.buckconfig b/apps/mobile/.buckconfig new file mode 100644 index 000000000..934256cb2 --- /dev/null +++ b/apps/mobile/.buckconfig @@ -0,0 +1,6 @@ + +[android] + target = Google Inc.:Google APIs:23 + +[maven_repositories] + central = https://repo1.maven.org/maven2 diff --git a/apps/mobile/.eslintrc.js b/apps/mobile/.eslintrc.js new file mode 100644 index 000000000..d440a4ae9 --- /dev/null +++ b/apps/mobile/.eslintrc.js @@ -0,0 +1,42 @@ +module.exports = { + env: { + 'react-native/react-native': true + }, + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaFeatures: { + jsx: true + }, + ecmaVersion: 12, + sourceType: 'module' + }, + extends: [ + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:@typescript-eslint/recommended' + ], + plugins: ['react', 'react-native'], + rules: { + 'react/display-name': 'off', + 'react/prop-types': 'off', + 'react/no-unescaped-entities': 'off', + 'react/react-in-jsx-scope': 'off', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + 'no-control-regex': 'off', + 'no-mixed-spaces-and-tabs': ['warn', 'smart-tabs'] + }, + ignorePatterns: ['**/*.js', '**/*.json', 'node_modules', 'android', 'ios', '.expo'], + settings: { + react: { + version: 'detect' + } + } +}; diff --git a/apps/mobile/.gitattributes b/apps/mobile/.gitattributes new file mode 100644 index 000000000..d42ff1835 --- /dev/null +++ b/apps/mobile/.gitattributes @@ -0,0 +1 @@ +*.pbxproj -text diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore new file mode 100644 index 000000000..c8eb0f9a6 --- /dev/null +++ b/apps/mobile/.gitignore @@ -0,0 +1,55 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +*.keystore +!debug.keystore + +# Bundle artifacts +*.jsbundle + +# CocoaPods +/ios/Pods/ + +# Expo +.expo/ +web-build/ +dist/ diff --git a/apps/mobile/.npmrc b/apps/mobile/.npmrc new file mode 100644 index 000000000..a8fd4724d --- /dev/null +++ b/apps/mobile/.npmrc @@ -0,0 +1,3 @@ +strict-peer-dependencies = false +ignore-workspace-root-check = true +shamefully-hoist = true diff --git a/apps/mobile/README.md b/apps/mobile/README.md new file mode 100644 index 000000000..8a79c0202 --- /dev/null +++ b/apps/mobile/README.md @@ -0,0 +1,3 @@ +Make sure to run `pnpm i` in this folder after making changes to the `packages`. + +- Note: If you add/remove something from `packages/assets` folder, you need to delete node_modules and run `pnpm i` again to link it. diff --git a/apps/mobile/android/.gitignore b/apps/mobile/android/.gitignore new file mode 100644 index 000000000..64436baaf --- /dev/null +++ b/apps/mobile/android/.gitignore @@ -0,0 +1,21 @@ +# OSX +# +.DS_Store + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof + +# BUCK +buck-out/ +\.buckd/ +*.keystore +!debug.keystore + +# Bundle artifacts +*.jsbundle diff --git a/apps/mobile/android/app/BUCK b/apps/mobile/android/app/BUCK new file mode 100644 index 000000000..cabced2a6 --- /dev/null +++ b/apps/mobile/android/app/BUCK @@ -0,0 +1,55 @@ +# To learn about Buck see [Docs](https://buckbuild.com/). +# To run your application with Buck: +# - install Buck +# - `npm start` - to start the packager +# - `cd android` +# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` +# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck +# - `buck install -r android/app` - compile, install and run application +# + +load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") + +lib_deps = [] + +create_aar_targets(glob(["libs/*.aar"])) + +create_jar_targets(glob(["libs/*.jar"])) + +android_library( + name = "all-libs", + exported_deps = lib_deps, +) + +android_library( + name = "app-code", + srcs = glob([ + "src/main/java/**/*.java", + ]), + deps = [ + ":all-libs", + ":build_config", + ":res", + ], +) + +android_build_config( + name = "build_config", + package = "com.spacedrive.app", +) + +android_resource( + name = "res", + package = "com.spacedrive.app", + res = "src/main/res", +) + +android_binary( + name = "app", + keystore = "//android/keystores:debug", + manifest = "src/main/AndroidManifest.xml", + package_type = "debug", + deps = [ + ":app-code", + ], +) diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle new file mode 100644 index 000000000..d825f46f4 --- /dev/null +++ b/apps/mobile/android/app/build.gradle @@ -0,0 +1,394 @@ +apply plugin: "com.android.application" + +import com.android.build.OutputFile +import org.apache.tools.ant.taskdefs.condition.Os + +apply plugin: 'org.mozilla.rust-android-gradle.rust-android' + +cargo { + module = "../../rust" + libname = "sdcore" + // profile = 'release', + pythonCommand = 'python3' + targets = ["arm", "arm64", "x86", "x86_64"] + targetDirectory = "../.././../../target" // Monorepo moment + +} + +tasks.whenTaskAdded { task -> + if ((task.name == 'javaPreCompileDebug' || task.name == 'javaPreCompileRelease')) { + task.dependsOn 'cargoBuild' + } +} + +/** + * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets + * and bundleReleaseJsAndAssets). + * These basically call `react-native bundle` with the correct arguments during the Android build + * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the + * bundle directly from the development server. Below you can see all the possible configurations + * and their defaults. If you decide to add a configuration block, make sure to add it before the + * `apply from: "../../node_modules/react-native/react.gradle"` line. + * + * project.ext.react = [ + * // the name of the generated asset file containing your JS bundle + * bundleAssetName: "index.android.bundle", + * + * // the entry file for bundle generation. If none specified and + * // "index.android.js" exists, it will be used. Otherwise "index.js" is + * // default. Can be overridden with ENTRY_FILE environment variable. + * entryFile: "index.android.js", + * + * // https://reactnative.dev/docs/performance#enable-the-ram-format + * bundleCommand: "ram-bundle", + * + * // whether to bundle JS and assets in debug mode + * bundleInDebug: false, + * + * // whether to bundle JS and assets in release mode + * bundleInRelease: true, + * + * // whether to bundle JS and assets in another build variant (if configured). + * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants + * // The configuration property can be in the following formats + * // 'bundleIn${productFlavor}${buildType}' + * // 'bundleIn${buildType}' + * // bundleInFreeDebug: true, + * // bundleInPaidRelease: true, + * // bundleInBeta: true, + * + * // whether to disable dev mode in custom build variants (by default only disabled in release) + * // for example: to disable dev mode in the staging build type (if configured) + * devDisabledInStaging: true, + * // The configuration property can be in the following formats + * // 'devDisabledIn${productFlavor}${buildType}' + * // 'devDisabledIn${buildType}' + * + * // the root of your project, i.e. where "package.json" lives + * root: "../../", + * + * // where to put the JS bundle asset in debug mode + * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", + * + * // where to put the JS bundle asset in release mode + * jsBundleDirRelease: "$buildDir/intermediates/assets/release", + * + * // where to put drawable resources / React Native assets, e.g. the ones you use via + * // require('./image.png')), in debug mode + * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", + * + * // where to put drawable resources / React Native assets, e.g. the ones you use via + * // require('./image.png')), in release mode + * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", + * + * // by default the gradle tasks are skipped if none of the JS files or assets change; this means + * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to + * // date; if you have any other folders that you want to ignore for performance reasons (gradle + * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ + * // for example, you might want to remove it from here. + * inputExcludes: ["android/**", "ios/**"], + * + * // override which node gets called and with what additional arguments + * nodeExecutableAndArgs: ["node"], + * + * // supply additional arguments to the packager + * extraPackagerArgs: [] + * ] + */ + +def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath() + +def reactNativeRoot = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + +project.ext.react = [ + entryFile: ["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android"].execute(null, rootDir).text.trim(), + enableHermes: (findProperty('expo.jsEngine') ?: "jsc") == "hermes", + hermesCommand: new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc", + cliPath: "${reactNativeRoot}/cli.js", + composeSourceMapsPath: "${reactNativeRoot}/scripts/compose-source-maps.js", +] + +apply from: new File(reactNativeRoot, "react.gradle") + +/** + * Set this to true to create two separate APKs instead of one: + * - An APK that only works on ARM devices + * - An APK that only works on x86 devices + * The advantage is the size of the APK is reduced by about 4MB. + * Upload all the APKs to the Play Store and people will download + * the correct one based on the CPU architecture of their device. + */ +def enableSeparateBuildPerCPUArchitecture = false + +/** + * Run Proguard to shrink the Java bytecode in release builds. + */ +def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean() + +/** + * The preferred build flavor of JavaScriptCore. + * + * For example, to use the international variant, you can use: + * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` + * + * The international variant includes ICU i18n library and necessary data + * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that + * give correct results when using with locales other than en-US. Note that + * this variant is about 6MiB larger per architecture than default. + */ +def jscFlavor = 'org.webkit:android-jsc:+' + +/** + * Whether to enable the Hermes VM. + * + * This should be set on project.ext.react and that value will be read here. If it is not set + * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode + * and the benefits of using Hermes will therefore be sharply reduced. + */ +def enableHermes = project.ext.react.get("enableHermes", false); + +/** + * Architectures to build native code for. + */ +def reactNativeArchitectures() { + def value = project.getProperties().get("reactNativeArchitectures") + return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] +} + +android { + ndkVersion rootProject.ext.ndkVersion + + compileSdkVersion rootProject.ext.compileSdkVersion + + defaultConfig { + applicationId 'com.spacedrive.app' + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "0.0.1" + buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() + + if (isNewArchitectureEnabled()) { + // We configure the NDK build only if you decide to opt-in for the New Architecture. + externalNativeBuild { + ndkBuild { + arguments "APP_PLATFORM=android-21", + "APP_STL=c++_shared", + "NDK_TOOLCHAIN_VERSION=clang", + "GENERATED_SRC_DIR=$buildDir/generated/source", + "PROJECT_BUILD_DIR=$buildDir", + "REACT_ANDROID_DIR=${reactNativeRoot}/ReactAndroid", + "REACT_ANDROID_BUILD_DIR=${reactNativeRoot}/ReactAndroid/build", + "NODE_MODULES_DIR=$rootDir/../node_modules" + cFlags "-Wall", "-Werror", "-fexceptions", "-frtti", "-DWITH_INSPECTOR=1" + cppFlags "-std=c++17" + // Make sure this target name is the same you specify inside the + // src/main/jni/Android.mk file for the `LOCAL_MODULE` variable. + targets "spacedrive_appmodules" + + // Fix for windows limit on number of character in file paths and in command lines + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + arguments "NDK_APP_SHORT_COMMANDS=true" + } + } + } + if (!enableSeparateBuildPerCPUArchitecture) { + ndk { + abiFilters (*reactNativeArchitectures()) + } + } + } + } + + if (isNewArchitectureEnabled()) { + // We configure the NDK build only if you decide to opt-in for the New Architecture. + externalNativeBuild { + ndkBuild { + path "$projectDir/src/main/jni/Android.mk" + } + } + def reactAndroidProjectDir = project(':ReactAndroid').projectDir + def packageReactNdkDebugLibs = tasks.register("packageReactNdkDebugLibs", Copy) { + dependsOn(":ReactAndroid:packageReactNdkDebugLibsForBuck") + from("$reactAndroidProjectDir/src/main/jni/prebuilt/lib") + into("$buildDir/react-ndk/exported") + } + def packageReactNdkReleaseLibs = tasks.register("packageReactNdkReleaseLibs", Copy) { + dependsOn(":ReactAndroid:packageReactNdkReleaseLibsForBuck") + from("$reactAndroidProjectDir/src/main/jni/prebuilt/lib") + into("$buildDir/react-ndk/exported") + } + afterEvaluate { + // If you wish to add a custom TurboModule or component locally, + // you should uncomment this line. + // preBuild.dependsOn("generateCodegenArtifactsFromSchema") + preDebugBuild.dependsOn(packageReactNdkDebugLibs) + preReleaseBuild.dependsOn(packageReactNdkReleaseLibs) + + // Due to a bug inside AGP, we have to explicitly set a dependency + // between configureNdkBuild* tasks and the preBuild tasks. + // This can be removed once this is solved: https://issuetracker.google.com/issues/207403732 + configureNdkBuildRelease.dependsOn(preReleaseBuild) + configureNdkBuildDebug.dependsOn(preDebugBuild) + reactNativeArchitectures().each { architecture -> + tasks.findByName("configureNdkBuildDebug[${architecture}]")?.configure { + dependsOn("preDebugBuild") + } + tasks.findByName("configureNdkBuildRelease[${architecture}]")?.configure { + dependsOn("preReleaseBuild") + } + } + } + } + + splits { + abi { + reset() + enable enableSeparateBuildPerCPUArchitecture + universalApk false // If true, also generate a universal APK + include (*reactNativeArchitectures()) + } + } + signingConfigs { + debug { + storeFile file('debug.keystore') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } + } + buildTypes { + debug { + signingConfig signingConfigs.debug + } + release { + // Caution! In production, you need to generate your own keystore file. + // see https://reactnative.dev/docs/signed-apk-android. + signingConfig signingConfigs.debug + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + } + } + + // applicationVariants are e.g. debug, release + applicationVariants.all { variant -> + variant.outputs.each { output -> + // For each separate APK per architecture, set a unique version code as described here: + // https://developer.android.com/studio/build/configure-apk-splits.html + def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] + def abi = output.getFilter(OutputFile.ABI) + if (abi != null) { // null for the universal-debug, universal-release variants + output.versionCodeOverride = + versionCodes.get(abi) * 1048576 + defaultConfig.versionCode + } + + } + } +} + +// Apply static values from `gradle.properties` to the `android.packagingOptions` +// Accepts values in comma delimited lists, example: +// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini +["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop -> + // Split option: 'foo,bar' -> ['foo', 'bar'] + def options = (findProperty("android.packagingOptions.$prop") ?: "").split(","); + // Trim all elements in place. + for (i in 0.. 0) { + println "android.packagingOptions.$prop += $options ($options.length)" + // Ex: android.packagingOptions.pickFirsts += '**/SCCS/**' + options.each { + android.packagingOptions[prop] += it + } + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + + //noinspection GradleDynamicVersion + implementation "com.facebook.react:react-native:+" // From node_modules + + def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true"; + def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true"; + def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true"; + def frescoVersion = rootProject.ext.frescoVersion + + // If your app supports Android versions before Ice Cream Sandwich (API level 14) + if (isGifEnabled || isWebpEnabled) { + implementation "com.facebook.fresco:fresco:${frescoVersion}" + implementation "com.facebook.fresco:imagepipeline-okhttp3:${frescoVersion}" + } + + if (isGifEnabled) { + // For animated gif support + implementation "com.facebook.fresco:animated-gif:${frescoVersion}" + } + + if (isWebpEnabled) { + // For webp support + implementation "com.facebook.fresco:webpsupport:${frescoVersion}" + if (isWebpAnimatedEnabled) { + // Animated webp support + implementation "com.facebook.fresco:animated-webp:${frescoVersion}" + } + } + + implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" + debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") { + exclude group:'com.facebook.fbjni' + } + debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { + exclude group:'com.facebook.flipper' + exclude group:'com.squareup.okhttp3', module:'okhttp' + } + debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") { + exclude group:'com.facebook.flipper' + } + + if (enableHermes) { + //noinspection GradleDynamicVersion + implementation("com.facebook.react:hermes-engine:+") { // From node_modules + exclude group:'com.facebook.fbjni' + } + } else { + implementation jscFlavor + } +} + +if (isNewArchitectureEnabled()) { + // If new architecture is enabled, we let you build RN from source + // Otherwise we fallback to a prebuilt .aar bundled in the NPM package. + // This will be applied to all the imported transtitive dependency. + configurations.all { + resolutionStrategy.dependencySubstitution { + substitute(module("com.facebook.react:react-native")) + .using(project(":ReactAndroid")) + .because("On New Architecture we're building React Native from source") + substitute(module("com.facebook.react:hermes-engine")) + .using(project(":ReactAndroid:hermes-engine")) + .because("On New Architecture we're building Hermes from source") + } + } +} + +// Run this once to be able to run the application with BUCK +// puts all compile dependencies into folder libs for BUCK to use +task copyDownloadableDepsToLibs(type: Copy) { + from configurations.implementation + into 'libs' +} + +apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); +applyNativeModulesAppBuildGradle(project) + +def isNewArchitectureEnabled() { + // To opt-in for the New Architecture, you can either: + // - Set `newArchEnabled` to true inside the `gradle.properties` file + // - Invoke gradle with `-newArchEnabled=true` + // - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true` + return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true" +} diff --git a/apps/mobile/android/app/build_defs.bzl b/apps/mobile/android/app/build_defs.bzl new file mode 100644 index 000000000..fff270f8d --- /dev/null +++ b/apps/mobile/android/app/build_defs.bzl @@ -0,0 +1,19 @@ +"""Helper definitions to glob .aar and .jar targets""" + +def create_aar_targets(aarfiles): + for aarfile in aarfiles: + name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] + lib_deps.append(":" + name) + android_prebuilt_aar( + name = name, + aar = aarfile, + ) + +def create_jar_targets(jarfiles): + for jarfile in jarfiles: + name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] + lib_deps.append(":" + name) + prebuilt_jar( + name = name, + binary_jar = jarfile, + ) diff --git a/apps/mobile/android/app/debug.keystore b/apps/mobile/android/app/debug.keystore new file mode 100644 index 000000000..364e105ed Binary files /dev/null and b/apps/mobile/android/app/debug.keystore differ diff --git a/apps/mobile/android/app/proguard-rules.pro b/apps/mobile/android/app/proguard-rules.pro new file mode 100644 index 000000000..551eb41da --- /dev/null +++ b/apps/mobile/android/app/proguard-rules.pro @@ -0,0 +1,14 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# react-native-reanimated +-keep class com.swmansion.reanimated.** { *; } +-keep class com.facebook.react.turbomodule.** { *; } + +# Add any project specific keep options here: diff --git a/apps/mobile/android/app/src/debug/AndroidManifest.xml b/apps/mobile/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..99e38fc5f --- /dev/null +++ b/apps/mobile/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/apps/mobile/android/app/src/debug/java/com/spacedrive/app/ReactNativeFlipper.java b/apps/mobile/android/app/src/debug/java/com/spacedrive/app/ReactNativeFlipper.java new file mode 100644 index 000000000..00c9f2231 --- /dev/null +++ b/apps/mobile/android/app/src/debug/java/com/spacedrive/app/ReactNativeFlipper.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + *

This source code is licensed under the MIT license found in the LICENSE file in the root + * directory of this source tree. + */ +package com.spacedrive.app; + +import android.content.Context; +import com.facebook.flipper.android.AndroidFlipperClient; +import com.facebook.flipper.android.utils.FlipperUtils; +import com.facebook.flipper.core.FlipperClient; +import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; +import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; +import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; +import com.facebook.flipper.plugins.inspector.DescriptorMapping; +import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; +import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; +import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; +import com.facebook.flipper.plugins.react.ReactFlipperPlugin; +import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.modules.network.NetworkingModule; +import okhttp3.OkHttpClient; + +public class ReactNativeFlipper { + public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { + if (FlipperUtils.shouldEnableFlipper(context)) { + final FlipperClient client = AndroidFlipperClient.getInstance(context); + client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); + client.addPlugin(new ReactFlipperPlugin()); + client.addPlugin(new DatabasesFlipperPlugin(context)); + client.addPlugin(new SharedPreferencesFlipperPlugin(context)); + client.addPlugin(CrashReporterPlugin.getInstance()); + NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); + NetworkingModule.setCustomClientBuilder( + new NetworkingModule.CustomClientBuilder() { + @Override + public void apply(OkHttpClient.Builder builder) { + builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); + } + }); + client.addPlugin(networkFlipperPlugin); + client.start(); + // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized + // Hence we run if after all native modules have been initialized + ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); + if (reactContext == null) { + reactInstanceManager.addReactInstanceEventListener( + new ReactInstanceManager.ReactInstanceEventListener() { + @Override + public void onReactContextInitialized(ReactContext reactContext) { + reactInstanceManager.removeReactInstanceEventListener(this); + reactContext.runOnNativeModulesQueueThread( + new Runnable() { + @Override + public void run() { + client.addPlugin(new FrescoFlipperPlugin()); + } + }); + } + }); + } else { + client.addPlugin(new FrescoFlipperPlugin()); + } + } + } +} \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..72543be31 --- /dev/null +++ b/apps/mobile/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/java/com/spacedrive/app/MainActivity.java b/apps/mobile/android/app/src/main/java/com/spacedrive/app/MainActivity.java new file mode 100644 index 000000000..94ac57750 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/spacedrive/app/MainActivity.java @@ -0,0 +1,83 @@ +package com.spacedrive.app; + +import android.os.Build; +import android.os.Bundle; + +import com.facebook.react.ReactActivity; +import com.facebook.react.ReactActivityDelegate; +import com.facebook.react.ReactRootView; + +import expo.modules.ReactActivityDelegateWrapper; + +public class MainActivity extends ReactActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + // Set the theme to AppTheme BEFORE onCreate to support + // coloring the background, status bar, and navigation bar. + // This is required for expo-splash-screen. + setTheme(R.style.AppTheme); + super.onCreate(null); + } + + /** + * Returns the name of the main component registered from JavaScript. + * This is used to schedule rendering of the component. + */ + @Override + protected String getMainComponentName() { + return "main"; + } + + /** + * Returns the instance of the {@link ReactActivityDelegate}. There the RootView is created and + * you can specify the renderer you wish to use - the new renderer (Fabric) or the old renderer + * (Paper). + */ + @Override + protected ReactActivityDelegate createReactActivityDelegate() { + return new ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, + new MainActivityDelegate(this, getMainComponentName()) + ); + } + + /** + * Align the back button behavior with Android S + * where moving root activities to background instead of finishing activities. + * @see onBackPressed + */ + @Override + public void invokeDefaultOnBackPressed() { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + if (!moveTaskToBack(false)) { + // For non-root activities, use the default implementation to finish them. + super.invokeDefaultOnBackPressed(); + } + return; + } + + // Use the default back button implementation on Android S + // because it's doing more than {@link Activity#moveTaskToBack} in fact. + super.invokeDefaultOnBackPressed(); + } + + public static class MainActivityDelegate extends ReactActivityDelegate { + public MainActivityDelegate(ReactActivity activity, String mainComponentName) { + super(activity, mainComponentName); + } + + @Override + protected ReactRootView createRootView() { + ReactRootView reactRootView = new ReactRootView(getContext()); + // If you opted-in for the New Architecture, we enable the Fabric Renderer. + reactRootView.setIsFabric(BuildConfig.IS_NEW_ARCHITECTURE_ENABLED); + return reactRootView; + } + + @Override + protected boolean isConcurrentRootEnabled() { + // If you opted-in for the New Architecture, we enable Concurrent Root (i.e. React 18). + // More on this on https://reactjs.org/blog/2022/03/29/react-v18.html + return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; + } + } +} diff --git a/apps/mobile/android/app/src/main/java/com/spacedrive/app/MainApplication.java b/apps/mobile/android/app/src/main/java/com/spacedrive/app/MainApplication.java new file mode 100644 index 000000000..1d47d6150 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/spacedrive/app/MainApplication.java @@ -0,0 +1,106 @@ +package com.spacedrive.app; + +import android.app.Application; +import android.content.Context; +import android.content.res.Configuration; +import androidx.annotation.NonNull; + +import com.facebook.react.PackageList; +import com.facebook.react.ReactApplication; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactNativeHost; +import com.facebook.react.ReactPackage; +import com.facebook.react.config.ReactFeatureFlags; +import com.facebook.soloader.SoLoader; +import com.spacedrive.app.newarchitecture.MainApplicationReactNativeHost; + +import expo.modules.ApplicationLifecycleDispatcher; +import expo.modules.ReactNativeHostWrapper; + +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +public class MainApplication extends Application implements ReactApplication { + private final ReactNativeHost mReactNativeHost = new ReactNativeHostWrapper( + this, + new ReactNativeHost(this) { + @Override + public boolean getUseDeveloperSupport() { + return BuildConfig.DEBUG; + } + + @Override + protected List getPackages() { + @SuppressWarnings("UnnecessaryLocalVariable") + List packages = new PackageList(this).getPackages(); + // Packages that cannot be autolinked yet can be added manually here, for example: + packages.add(new com.spacedrive.app.SpacedrivePackage()); + return packages; + } + + @Override + protected String getJSMainModuleName() { + return "index"; + } + }); + + private final ReactNativeHost mNewArchitectureNativeHost = + new ReactNativeHostWrapper(this, new MainApplicationReactNativeHost(this)); + + @Override + public ReactNativeHost getReactNativeHost() { + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + return mNewArchitectureNativeHost; + } else { + return mReactNativeHost; + } + } + + @Override + public void onCreate() { + super.onCreate(); + // If you opted-in for the New Architecture, we enable the TurboModule system + ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; + SoLoader.init(this, /* native exopackage */ false); + + initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); + ApplicationLifecycleDispatcher.onApplicationCreate(this); + } + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + super.onConfigurationChanged(newConfig); + ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig); + } + + /** + * Loads Flipper in React Native templates. Call this in the onCreate method with something like + * initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); + * + * @param context + * @param reactInstanceManager + */ + private static void initializeFlipper( + Context context, ReactInstanceManager reactInstanceManager) { + if (BuildConfig.DEBUG) { + try { + /* + We use reflection here to pick up the class that initializes Flipper, + since Flipper library is not available in release mode + */ + Class aClass = Class.forName("com.spacedrive.app.ReactNativeFlipper"); + aClass + .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class) + .invoke(null, context, reactInstanceManager); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + } + } +} diff --git a/apps/mobile/android/app/src/main/java/com/spacedrive/app/SDCore.java b/apps/mobile/android/app/src/main/java/com/spacedrive/app/SDCore.java new file mode 100644 index 000000000..782f44a1b --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/spacedrive/app/SDCore.java @@ -0,0 +1,76 @@ +package com.spacedrive.app; + +import android.content.Context; +import android.os.Build; + +import androidx.annotation.RequiresApi; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +import javax.annotation.Nullable; + +public class SDCore extends ReactContextBaseJavaModule { + SDCore(ReactApplicationContext context) { super(context); } + + private boolean registeredWithRust = false; + private int listeners = 0; + + @Override + public String getName() + { + return "SDCore"; + } + + static { + System.loadLibrary("sdcore"); + } + + // is exposed by Rust and is used to register the subscription + private native void registerCoreEventListener(); + + private native void handleCoreMsg(String query, Promise promise); + + @ReactMethod + public void sd_core_msg(String query, Promise promise) + { + this.handleCoreMsg(query, promise); + } + + public String getDataDirectory() + { + return getCurrentActivity().getFilesDir().toString(); + } + + @ReactMethod + public void addListener(String eventName) + { + if (!registeredWithRust) + { + this.registerCoreEventListener(); + } + + this.listeners++; + } + + @ReactMethod + public void removeListeners(Integer count) + { + this.listeners--; + } + + public void sendCoreEvent(String body) + { + if (this.listeners > 0) + { + this.getReactApplicationContext() + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("SDCoreEvent", body); + } + } +} \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/java/com/spacedrive/app/SpacedrivePackage.java b/apps/mobile/android/app/src/main/java/com/spacedrive/app/SpacedrivePackage.java new file mode 100644 index 000000000..b9fe3bc95 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/spacedrive/app/SpacedrivePackage.java @@ -0,0 +1,28 @@ +package com.spacedrive.app; +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SpacedrivePackage implements ReactPackage { + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + @Override + public List createNativeModules( + ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + + modules.add(new SDCore(reactContext)); + + return modules; + } + +} \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/java/com/spacedrive/app/newarchitecture/MainApplicationReactNativeHost.java b/apps/mobile/android/app/src/main/java/com/spacedrive/app/newarchitecture/MainApplicationReactNativeHost.java new file mode 100644 index 000000000..1a3e3aa1d --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/spacedrive/app/newarchitecture/MainApplicationReactNativeHost.java @@ -0,0 +1,117 @@ +package com.spacedrive.app.newarchitecture; + +import android.app.Application; +import androidx.annotation.NonNull; +import com.facebook.react.PackageList; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactNativeHost; +import com.facebook.react.ReactPackage; +import com.facebook.react.ReactPackageTurboModuleManagerDelegate; +import com.facebook.react.bridge.JSIModulePackage; +import com.facebook.react.bridge.JSIModuleProvider; +import com.facebook.react.bridge.JSIModuleSpec; +import com.facebook.react.bridge.JSIModuleType; +import com.facebook.react.bridge.JavaScriptContextHolder; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.UIManager; +import com.facebook.react.fabric.ComponentFactory; +import com.facebook.react.fabric.CoreComponentsRegistry; +import com.facebook.react.fabric.EmptyReactNativeConfig; +import com.facebook.react.fabric.FabricJSIModuleProvider; +import com.facebook.react.fabric.ReactNativeConfig; +import com.facebook.react.uimanager.ViewManagerRegistry; +import com.spacedrive.app.BuildConfig; +import com.spacedrive.app.newarchitecture.components.MainComponentsRegistry; +import com.spacedrive.app.newarchitecture.modules.MainApplicationTurboModuleManagerDelegate; +import java.util.ArrayList; +import java.util.List; + +/** + * A {@link ReactNativeHost} that helps you load everything needed for the New Architecture, both + * TurboModule delegates and the Fabric Renderer. + * + *

Please note that this class is used ONLY if you opt-in for the New Architecture (see the + * `newArchEnabled` property). Is ignored otherwise. + */ +public class MainApplicationReactNativeHost extends ReactNativeHost { + public MainApplicationReactNativeHost(Application application) { + super(application); + } + + @Override + public boolean getUseDeveloperSupport() { + return BuildConfig.DEBUG; + } + + @Override + protected List getPackages() { + List packages = new PackageList(this).getPackages(); + // Packages that cannot be autolinked yet can be added manually here, for example: + // packages.add(new MyReactNativePackage()); + // TurboModules must also be loaded here providing a valid TurboReactPackage implementation: + // packages.add(new TurboReactPackage() { ... }); + // If you have custom Fabric Components, their ViewManagers should also be loaded here + // inside a ReactPackage. + return packages; + } + + @Override + protected String getJSMainModuleName() { + return "index"; + } + + @NonNull + @Override + protected ReactPackageTurboModuleManagerDelegate.Builder + getReactPackageTurboModuleManagerDelegateBuilder() { + // Here we provide the ReactPackageTurboModuleManagerDelegate Builder. This is necessary + // for the new architecture and to use TurboModules correctly. + return new MainApplicationTurboModuleManagerDelegate.Builder(); + } + + @Override + protected JSIModulePackage getJSIModulePackage() { + return new JSIModulePackage() { + @Override + public List getJSIModules( + final ReactApplicationContext reactApplicationContext, + final JavaScriptContextHolder jsContext) { + final List specs = new ArrayList<>(); + + // Here we provide a new JSIModuleSpec that will be responsible of providing the + // custom Fabric Components. + specs.add( + new JSIModuleSpec() { + @Override + public JSIModuleType getJSIModuleType() { + return JSIModuleType.UIManager; + } + + @Override + public JSIModuleProvider getJSIModuleProvider() { + final ComponentFactory componentFactory = new ComponentFactory(); + CoreComponentsRegistry.register(componentFactory); + + // Here we register a Components Registry. + // The one that is generated with the template contains no components + // and just provides you the one from React Native core. + MainComponentsRegistry.register(componentFactory); + + final ReactInstanceManager reactInstanceManager = getReactInstanceManager(); + + ViewManagerRegistry viewManagerRegistry = + new ViewManagerRegistry( + reactInstanceManager.getOrCreateViewManagers(reactApplicationContext)); + + return new FabricJSIModuleProvider( + reactApplicationContext, + componentFactory, + ReactNativeConfig.DEFAULT_CONFIG, + viewManagerRegistry); + } + }); + return specs; + } + }; + } +} diff --git a/apps/mobile/android/app/src/main/java/com/spacedrive/app/newarchitecture/components/MainComponentsRegistry.java b/apps/mobile/android/app/src/main/java/com/spacedrive/app/newarchitecture/components/MainComponentsRegistry.java new file mode 100644 index 000000000..0c5bce1e3 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/spacedrive/app/newarchitecture/components/MainComponentsRegistry.java @@ -0,0 +1,36 @@ +package com.spacedrive.app.newarchitecture.components; + +import com.facebook.jni.HybridData; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.fabric.ComponentFactory; +import com.facebook.soloader.SoLoader; + +/** + * Class responsible to load the custom Fabric Components. This class has native methods and needs a + * corresponding C++ implementation/header file to work correctly (already placed inside the jni/ + * folder for you). + * + *

Please note that this class is used ONLY if you opt-in for the New Architecture (see the + * `newArchEnabled` property). Is ignored otherwise. + */ +@DoNotStrip +public class MainComponentsRegistry { + static { + SoLoader.loadLibrary("fabricjni"); + } + + @DoNotStrip private final HybridData mHybridData; + + @DoNotStrip + private native HybridData initHybrid(ComponentFactory componentFactory); + + @DoNotStrip + private MainComponentsRegistry(ComponentFactory componentFactory) { + mHybridData = initHybrid(componentFactory); + } + + @DoNotStrip + public static MainComponentsRegistry register(ComponentFactory componentFactory) { + return new MainComponentsRegistry(componentFactory); + } +} diff --git a/apps/mobile/android/app/src/main/java/com/spacedrive/app/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate.java b/apps/mobile/android/app/src/main/java/com/spacedrive/app/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate.java new file mode 100644 index 000000000..a2cd752da --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/spacedrive/app/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate.java @@ -0,0 +1,48 @@ +package com.spacedrive.app.newarchitecture.modules; + +import com.facebook.jni.HybridData; +import com.facebook.react.ReactPackage; +import com.facebook.react.ReactPackageTurboModuleManagerDelegate; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.soloader.SoLoader; +import java.util.List; + +/** + * Class responsible to load the TurboModules. This class has native methods and needs a + * corresponding C++ implementation/header file to work correctly (already placed inside the jni/ + * folder for you). + * + *

Please note that this class is used ONLY if you opt-in for the New Architecture (see the + * `newArchEnabled` property). Is ignored otherwise. + */ +public class MainApplicationTurboModuleManagerDelegate + extends ReactPackageTurboModuleManagerDelegate { + + private static volatile boolean sIsSoLibraryLoaded; + + protected MainApplicationTurboModuleManagerDelegate( + ReactApplicationContext reactApplicationContext, List packages) { + super(reactApplicationContext, packages); + } + + protected native HybridData initHybrid(); + + native boolean canCreateTurboModule(String moduleName); + + public static class Builder extends ReactPackageTurboModuleManagerDelegate.Builder { + protected MainApplicationTurboModuleManagerDelegate build( + ReactApplicationContext context, List packages) { + return new MainApplicationTurboModuleManagerDelegate(context, packages); + } + } + + @Override + protected synchronized void maybeLoadOtherSoLibraries() { + if (!sIsSoLibraryLoaded) { + // If you change the name of your application .so file in the Android.mk file, + // make sure you update the name here as well. + SoLoader.loadLibrary("spacedrive_appmodules"); + sIsSoLibraryLoaded = true; + } + } +} diff --git a/apps/mobile/android/app/src/main/jni/Android.mk b/apps/mobile/android/app/src/main/jni/Android.mk new file mode 100644 index 000000000..57530bb1d --- /dev/null +++ b/apps/mobile/android/app/src/main/jni/Android.mk @@ -0,0 +1,48 @@ +THIS_DIR := $(call my-dir) + +include $(REACT_ANDROID_DIR)/Android-prebuilt.mk + +# If you wish to add a custom TurboModule or Fabric component in your app you +# will have to include the following autogenerated makefile. +# include $(GENERATED_SRC_DIR)/codegen/jni/Android.mk +include $(CLEAR_VARS) + +LOCAL_PATH := $(THIS_DIR) + +# You can customize the name of your application .so file here. +LOCAL_MODULE := spacedrive_appmodules + +LOCAL_C_INCLUDES := $(LOCAL_PATH) +LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp) +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) + +# If you wish to add a custom TurboModule or Fabric component in your app you +# will have to uncomment those lines to include the generated source +# files from the codegen (placed in $(GENERATED_SRC_DIR)/codegen/jni) +# +# LOCAL_C_INCLUDES += $(GENERATED_SRC_DIR)/codegen/jni +# LOCAL_SRC_FILES += $(wildcard $(GENERATED_SRC_DIR)/codegen/jni/*.cpp) +# LOCAL_EXPORT_C_INCLUDES += $(GENERATED_SRC_DIR)/codegen/jni + +# Here you should add any native library you wish to depend on. +LOCAL_SHARED_LIBRARIES := \ + libfabricjni \ + libfbjni \ + libfolly_runtime \ + libglog \ + libjsi \ + libreact_codegen_rncore \ + libreact_debug \ + libreact_nativemodule_core \ + libreact_render_componentregistry \ + libreact_render_core \ + libreact_render_debug \ + libreact_render_graphics \ + librrc_view \ + libruntimeexecutor \ + libturbomodulejsijni \ + libyoga + +LOCAL_CFLAGS := -DLOG_TAG=\"ReactNative\" -fexceptions -frtti -std=c++17 -Wall + +include $(BUILD_SHARED_LIBRARY) diff --git a/apps/mobile/android/app/src/main/jni/MainApplicationModuleProvider.cpp b/apps/mobile/android/app/src/main/jni/MainApplicationModuleProvider.cpp new file mode 100644 index 000000000..0ac23cc62 --- /dev/null +++ b/apps/mobile/android/app/src/main/jni/MainApplicationModuleProvider.cpp @@ -0,0 +1,24 @@ +#include "MainApplicationModuleProvider.h" + +#include + +namespace facebook { +namespace react { + +std::shared_ptr MainApplicationModuleProvider( + const std::string moduleName, + const JavaTurboModule::InitParams ¶ms) { + // Here you can provide your own module provider for TurboModules coming from + // either your application or from external libraries. The approach to follow + // is similar to the following (for a library called `samplelibrary`: + // + // auto module = samplelibrary_ModuleProvider(moduleName, params); + // if (module != nullptr) { + // return module; + // } + // return rncore_ModuleProvider(moduleName, params); + return rncore_ModuleProvider(moduleName, params); +} + +} // namespace react +} // namespace facebook diff --git a/apps/mobile/android/app/src/main/jni/MainApplicationModuleProvider.h b/apps/mobile/android/app/src/main/jni/MainApplicationModuleProvider.h new file mode 100644 index 000000000..0fa43fa69 --- /dev/null +++ b/apps/mobile/android/app/src/main/jni/MainApplicationModuleProvider.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +#include + +namespace facebook { +namespace react { + +std::shared_ptr MainApplicationModuleProvider( + const std::string moduleName, + const JavaTurboModule::InitParams ¶ms); + +} // namespace react +} // namespace facebook diff --git a/apps/mobile/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.cpp b/apps/mobile/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.cpp new file mode 100644 index 000000000..dbbdc3d13 --- /dev/null +++ b/apps/mobile/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.cpp @@ -0,0 +1,45 @@ +#include "MainApplicationTurboModuleManagerDelegate.h" +#include "MainApplicationModuleProvider.h" + +namespace facebook { +namespace react { + +jni::local_ref +MainApplicationTurboModuleManagerDelegate::initHybrid( + jni::alias_ref) { + return makeCxxInstance(); +} + +void MainApplicationTurboModuleManagerDelegate::registerNatives() { + registerHybrid({ + makeNativeMethod( + "initHybrid", MainApplicationTurboModuleManagerDelegate::initHybrid), + makeNativeMethod( + "canCreateTurboModule", + MainApplicationTurboModuleManagerDelegate::canCreateTurboModule), + }); +} + +std::shared_ptr +MainApplicationTurboModuleManagerDelegate::getTurboModule( + const std::string name, + const std::shared_ptr jsInvoker) { + // Not implemented yet: provide pure-C++ NativeModules here. + return nullptr; +} + +std::shared_ptr +MainApplicationTurboModuleManagerDelegate::getTurboModule( + const std::string name, + const JavaTurboModule::InitParams ¶ms) { + return MainApplicationModuleProvider(name, params); +} + +bool MainApplicationTurboModuleManagerDelegate::canCreateTurboModule( + std::string name) { + return getTurboModule(name, nullptr) != nullptr || + getTurboModule(name, {.moduleName = name}) != nullptr; +} + +} // namespace react +} // namespace facebook diff --git a/apps/mobile/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.h b/apps/mobile/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.h new file mode 100644 index 000000000..f5670d9f8 --- /dev/null +++ b/apps/mobile/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.h @@ -0,0 +1,38 @@ +#include +#include + +#include +#include + +namespace facebook { +namespace react { + +class MainApplicationTurboModuleManagerDelegate + : public jni::HybridClass< + MainApplicationTurboModuleManagerDelegate, + TurboModuleManagerDelegate> { + public: + // Adapt it to the package you used for your Java class. + static constexpr auto kJavaDescriptor = + "Lcom/spacedrive/app/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate;"; + + static jni::local_ref initHybrid(jni::alias_ref); + + static void registerNatives(); + + std::shared_ptr getTurboModule( + const std::string name, + const std::shared_ptr jsInvoker) override; + std::shared_ptr getTurboModule( + const std::string name, + const JavaTurboModule::InitParams ¶ms) override; + + /** + * Test-only method. Allows user to verify whether a TurboModule can be + * created by instances of this class. + */ + bool canCreateTurboModule(std::string name); +}; + +} // namespace react +} // namespace facebook diff --git a/apps/mobile/android/app/src/main/jni/MainComponentsRegistry.cpp b/apps/mobile/android/app/src/main/jni/MainComponentsRegistry.cpp new file mode 100644 index 000000000..8f7edffd6 --- /dev/null +++ b/apps/mobile/android/app/src/main/jni/MainComponentsRegistry.cpp @@ -0,0 +1,61 @@ +#include "MainComponentsRegistry.h" + +#include +#include +#include +#include + +namespace facebook { +namespace react { + +MainComponentsRegistry::MainComponentsRegistry(ComponentFactory *delegate) {} + +std::shared_ptr +MainComponentsRegistry::sharedProviderRegistry() { + auto providerRegistry = CoreComponentsRegistry::sharedProviderRegistry(); + + // Custom Fabric Components go here. You can register custom + // components coming from your App or from 3rd party libraries here. + // + // providerRegistry->add(concreteComponentDescriptorProvider< + // AocViewerComponentDescriptor>()); + return providerRegistry; +} + +jni::local_ref +MainComponentsRegistry::initHybrid( + jni::alias_ref, + ComponentFactory *delegate) { + auto instance = makeCxxInstance(delegate); + + auto buildRegistryFunction = + [](EventDispatcher::Weak const &eventDispatcher, + ContextContainer::Shared const &contextContainer) + -> ComponentDescriptorRegistry::Shared { + auto registry = MainComponentsRegistry::sharedProviderRegistry() + ->createComponentDescriptorRegistry( + {eventDispatcher, contextContainer}); + + auto mutableRegistry = + std::const_pointer_cast(registry); + + mutableRegistry->setFallbackComponentDescriptor( + std::make_shared( + ComponentDescriptorParameters{ + eventDispatcher, contextContainer, nullptr})); + + return registry; + }; + + delegate->buildRegistryFunction = buildRegistryFunction; + return instance; +} + +void MainComponentsRegistry::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", MainComponentsRegistry::initHybrid), + }); +} + +} // namespace react +} // namespace facebook diff --git a/apps/mobile/android/app/src/main/jni/MainComponentsRegistry.h b/apps/mobile/android/app/src/main/jni/MainComponentsRegistry.h new file mode 100644 index 000000000..a7f222542 --- /dev/null +++ b/apps/mobile/android/app/src/main/jni/MainComponentsRegistry.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include + +namespace facebook { +namespace react { + +class MainComponentsRegistry + : public facebook::jni::HybridClass { + public: + // Adapt it to the package you used for your Java class. + constexpr static auto kJavaDescriptor = + "Lcom/spacedrive/app/newarchitecture/components/MainComponentsRegistry;"; + + static void registerNatives(); + + MainComponentsRegistry(ComponentFactory *delegate); + + private: + static std::shared_ptr + sharedProviderRegistry(); + + static jni::local_ref initHybrid( + jni::alias_ref, + ComponentFactory *delegate); +}; + +} // namespace react +} // namespace facebook diff --git a/apps/mobile/android/app/src/main/jni/OnLoad.cpp b/apps/mobile/android/app/src/main/jni/OnLoad.cpp new file mode 100644 index 000000000..c569b6e86 --- /dev/null +++ b/apps/mobile/android/app/src/main/jni/OnLoad.cpp @@ -0,0 +1,11 @@ +#include +#include "MainApplicationTurboModuleManagerDelegate.h" +#include "MainComponentsRegistry.h" + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { + return facebook::jni::initialize(vm, [] { + facebook::react::MainApplicationTurboModuleManagerDelegate:: + registerNatives(); + facebook::react::MainComponentsRegistry::registerNatives(); + }); +} diff --git a/apps/mobile/android/app/src/main/res/drawable-hdpi/splashscreen_image.png b/apps/mobile/android/app/src/main/res/drawable-hdpi/splashscreen_image.png new file mode 100644 index 000000000..0e0762212 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/drawable-hdpi/splashscreen_image.png differ diff --git a/apps/mobile/android/app/src/main/res/drawable-mdpi/splashscreen_image.png b/apps/mobile/android/app/src/main/res/drawable-mdpi/splashscreen_image.png new file mode 100644 index 000000000..0e0762212 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/drawable-mdpi/splashscreen_image.png differ diff --git a/apps/mobile/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png b/apps/mobile/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png new file mode 100644 index 000000000..0e0762212 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png differ diff --git a/apps/mobile/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png b/apps/mobile/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png new file mode 100644 index 000000000..0e0762212 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png differ diff --git a/apps/mobile/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png b/apps/mobile/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png new file mode 100644 index 000000000..0e0762212 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png differ diff --git a/apps/mobile/android/app/src/main/res/drawable/rn_edit_text_material.xml b/apps/mobile/android/app/src/main/res/drawable/rn_edit_text_material.xml new file mode 100644 index 000000000..f35d99620 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/drawable/rn_edit_text_material.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + diff --git a/apps/mobile/android/app/src/main/res/drawable/splashscreen.xml b/apps/mobile/android/app/src/main/res/drawable/splashscreen.xml new file mode 100644 index 000000000..c8568e162 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/drawable/splashscreen.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..3941bea9b --- /dev/null +++ b/apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..3941bea9b --- /dev/null +++ b/apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..9c9224cd6 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..9c9224cd6 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..0bbe4c0ee Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..8e4fcba73 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..8e4fcba73 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..ba54f3245 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..8458e371f Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..8458e371f Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..90358e89b Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..ee6a34f1a Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..ee6a34f1a Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..75cc649dd Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..92a254fc3 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..92a254fc3 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..3d0043983 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/main/res/values-night/colors.xml b/apps/mobile/android/app/src/main/res/values-night/colors.xml new file mode 100644 index 000000000..3c05de5be --- /dev/null +++ b/apps/mobile/android/app/src/main/res/values-night/colors.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/values/colors.xml b/apps/mobile/android/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..78219faf9 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + #000000 + #ffffff + #023c69 + #000000 + \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/values/strings.xml b/apps/mobile/android/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..291d93fd7 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + Spacedrive + contain + false + automatic + \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/values/styles.xml b/apps/mobile/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..f03e23f85 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/values/styles.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/apps/mobile/android/build.gradle b/apps/mobile/android/build.gradle new file mode 100644 index 000000000..ab32c2ff7 --- /dev/null +++ b/apps/mobile/android/build.gradle @@ -0,0 +1,63 @@ +import org.apache.tools.ant.taskdefs.condition.Os + +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext { + buildToolsVersion = findProperty('android.buildToolsVersion') ?: '31.0.0' + minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '21') + compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '31') + targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '31') + if (findProperty('android.kotlinVersion')) { + kotlinVersion = findProperty('android.kotlinVersion') + } + frescoVersion = findProperty('expo.frescoVersion') ?: '2.5.0' + + if (System.properties['os.arch'] == 'aarch64') { + // For M1 Users we need to use the NDK 24 which added support for aarch64 + ndkVersion = '24.0.8215888' + } else { + // Otherwise we default to the side-by-side NDK version from AGP. + ndkVersion = '21.4.7075529' + } + } + repositories { + google() + mavenCentral() + maven { + url "https://plugins.gradle.org/m2/" + } + } + dependencies { + classpath('com.android.tools.build:gradle:7.1.1') + classpath('com.facebook.react:react-native-gradle-plugin') + classpath('de.undercouch:gradle-download-task:5.0.1') + classpath('org.mozilla.rust-android-gradle:plugin:0.9.3') + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + mavenLocal() + maven { + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), '../android')) + } + maven { + // Android JSC is installed from npm + url(new File(['node', '--print', "require.resolve('jsc-android/package.json')"].execute(null, rootDir).text.trim(), '../dist')) + } + + google() + mavenCentral { + // We don't want to fetch react-native from Maven Central as there are + // older versions over there. + content { + excludeGroup 'com.facebook.react' + } + } + maven { url 'https://www.jitpack.io' } + } +} diff --git a/apps/mobile/android/gradle.properties b/apps/mobile/android/gradle.properties new file mode 100644 index 000000000..9911ac4af --- /dev/null +++ b/apps/mobile/android/gradle.properties @@ -0,0 +1,53 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true + +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true + +# Version of flipper SDK to use with React Native +FLIPPER_VERSION=0.125.0 + +# Use this property to specify which architecture you want to build. +# You can also override it from the CLI using +# ./gradlew -PreactNativeArchitectures=x86_64 +reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 + +# Use this property to enable support to the new architecture. +# This will allow you to use TurboModules and the Fabric render in +# your application. You should enable this flag either if you want +# to write custom TurboModules/Fabric components OR use libraries that +# are providing them. +newArchEnabled=false + +# The hosted JavaScript engine +# Supported values: expo.jsEngine = "hermes" | "jsc" +expo.jsEngine=hermes + +# Enable GIF support in React Native images (~200 B increase) +expo.gif.enabled=true +# Enable webp support in React Native images (~85 KB increase) +expo.webp.enabled=true +# Enable animated webp support (~3.4 MB increase) +# Disabled by default because iOS doesn't support animated webp +expo.webp.animated=false diff --git a/apps/mobile/android/gradle/wrapper/gradle-wrapper.jar b/apps/mobile/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..7454180f2 Binary files /dev/null and b/apps/mobile/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/apps/mobile/android/gradle/wrapper/gradle-wrapper.properties b/apps/mobile/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..669386b87 --- /dev/null +++ b/apps/mobile/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/apps/mobile/android/gradlew b/apps/mobile/android/gradlew new file mode 100755 index 000000000..1b6c78733 --- /dev/null +++ b/apps/mobile/android/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/apps/mobile/android/gradlew.bat b/apps/mobile/android/gradlew.bat new file mode 100644 index 000000000..ac1b06f93 --- /dev/null +++ b/apps/mobile/android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/apps/mobile/android/settings.gradle b/apps/mobile/android/settings.gradle new file mode 100644 index 000000000..d52a28091 --- /dev/null +++ b/apps/mobile/android/settings.gradle @@ -0,0 +1,17 @@ +rootProject.name = 'Spacedrive' + +apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle"); +useExpoModules() + +apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); +applyNativeModulesSettingsGradle(settings) + +include ':app' +includeBuild(new File(["node", "--print", "require.resolve('react-native-gradle-plugin/package.json')"].execute(null, rootDir).text.trim()).getParentFile()) + +if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") { + include(":ReactAndroid") + project(":ReactAndroid").projectDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../ReactAndroid"); + include(":ReactAndroid:hermes-engine") + project(":ReactAndroid:hermes-engine").projectDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../ReactAndroid/hermes-engine"); +} diff --git a/apps/mobile/app.json b/apps/mobile/app.json new file mode 100644 index 000000000..629507b7c --- /dev/null +++ b/apps/mobile/app.json @@ -0,0 +1,24 @@ +{ + "expo": { + "name": "Spacedrive", + "slug": "spacedrive", + "version": "0.0.1", + "orientation": "portrait", + "jsEngine": "hermes", + "scheme": "spacedrive", + "userInterfaceStyle": "automatic", + "updates": { + "enabled": false, + "fallbackToCacheTimeout": 0 + }, + "assetBundlePatterns": ["**/*"], + "ios": { + "supportsTablet": false, + "bundleIdentifier": "com.spacedrive.app" + }, + "android": { + "package": "com.spacedrive.app" + }, + "privacy": "hidden" + } +} diff --git a/apps/mobile/babel.config.js b/apps/mobile/babel.config.js new file mode 100644 index 000000000..224bce79e --- /dev/null +++ b/apps/mobile/babel.config.js @@ -0,0 +1,7 @@ +module.exports = function (api) { + api.cache(true); + return { + presets: ['babel-preset-expo'], + plugins: ['react-native-reanimated/plugin'] + }; +}; diff --git a/apps/mobile/index.js b/apps/mobile/index.js new file mode 100644 index 000000000..018d06f91 --- /dev/null +++ b/apps/mobile/index.js @@ -0,0 +1,8 @@ +import { registerRootComponent } from 'expo'; + +import App from './src/App'; + +// registerRootComponent calls AppRegistry.registerComponent('main', () => App); +// It also ensures that whether you load the app in Expo Go or in a native build, +// the environment is set up appropriately +registerRootComponent(App); diff --git a/apps/mobile/ios/.gitignore b/apps/mobile/ios/.gitignore new file mode 100644 index 000000000..8beb34430 --- /dev/null +++ b/apps/mobile/ios/.gitignore @@ -0,0 +1,30 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace +.xcode.env.local + +# Bundle artifacts +*.jsbundle + +# CocoaPods +/Pods/ diff --git a/apps/mobile/ios/.xcode.env b/apps/mobile/ios/.xcode.env new file mode 100644 index 000000000..366234920 --- /dev/null +++ b/apps/mobile/ios/.xcode.env @@ -0,0 +1 @@ +export NODE_BINARY=$(command -v node) \ No newline at end of file diff --git a/apps/mobile/ios/Podfile b/apps/mobile/ios/Podfile new file mode 100644 index 000000000..2b5d77e16 --- /dev/null +++ b/apps/mobile/ios/Podfile @@ -0,0 +1,49 @@ +require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking") +require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods") +require File.join(File.dirname(`node --print "require.resolve('@react-native-community/cli-platform-ios/package.json')"`), "native_modules") + +require 'json' +podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {} + +platform :ios, podfile_properties['ios.deploymentTarget'] || '13.0' +install! 'cocoapods', + :deterministic_uuids => false + +target 'Spacedrive' do + use_expo_modules! + config = use_native_modules! + + use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] + + # Flags change depending on the env values. + flags = get_default_flags() + + use_react_native!( + :path => config[:reactNativePath], + :hermes_enabled => flags[:hermes_enabled] || podfile_properties['expo.jsEngine'] == 'hermes', + :fabric_enabled => flags[:fabric_enabled], + # An absolute path to your application root. + :app_path => "#{Dir.pwd}/.." + ) + + # Uncomment to opt-in to using Flipper + # Note that if you have use_frameworks! enabled, Flipper will not work + # + # if !ENV['CI'] + # use_flipper!() + # end + + post_install do |installer| + react_native_post_install(installer) + __apply_Xcode_12_5_M1_post_install_workaround(installer) + end + + post_integrate do |installer| + begin + expo_patch_react_imports!(installer) + rescue => e + Pod::UI.warn e + end + end + +end diff --git a/apps/mobile/ios/Podfile.lock b/apps/mobile/ios/Podfile.lock new file mode 100644 index 000000000..00b80fd0f --- /dev/null +++ b/apps/mobile/ios/Podfile.lock @@ -0,0 +1,585 @@ +PODS: + - boost (1.76.0) + - DoubleConversion (1.1.6) + - EXApplication (4.2.2): + - ExpoModulesCore + - EXConstants (13.2.3): + - ExpoModulesCore + - EXFileSystem (14.1.0): + - ExpoModulesCore + - EXFont (10.2.0): + - ExpoModulesCore + - Expo (46.0.9): + - ExpoModulesCore + - ExpoKeepAwake (10.2.0): + - ExpoModulesCore + - ExpoModulesCore (0.11.4): + - React-Core + - ReactCommon/turbomodule/core + - EXSplashScreen (0.16.2): + - ExpoModulesCore + - React-Core + - FBLazyVector (0.69.4) + - FBReactNativeSpec (0.69.4): + - RCT-Folly (= 2021.06.28.00-v2) + - RCTRequired (= 0.69.4) + - RCTTypeSafety (= 0.69.4) + - React-Core (= 0.69.4) + - React-jsi (= 0.69.4) + - ReactCommon/turbomodule/core (= 0.69.4) + - fmt (6.2.1) + - glog (0.3.5) + - hermes-engine (0.69.4) + - libevent (2.1.12) + - RCT-Folly (2021.06.28.00-v2): + - boost + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - RCT-Folly/Default (= 2021.06.28.00-v2) + - RCT-Folly/Default (2021.06.28.00-v2): + - boost + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - RCT-Folly/Futures (2021.06.28.00-v2): + - boost + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - libevent + - RCTRequired (0.69.4) + - RCTTypeSafety (0.69.4): + - FBLazyVector (= 0.69.4) + - RCTRequired (= 0.69.4) + - React-Core (= 0.69.4) + - React (0.69.4): + - React-Core (= 0.69.4) + - React-Core/DevSupport (= 0.69.4) + - React-Core/RCTWebSocket (= 0.69.4) + - React-RCTActionSheet (= 0.69.4) + - React-RCTAnimation (= 0.69.4) + - React-RCTBlob (= 0.69.4) + - React-RCTImage (= 0.69.4) + - React-RCTLinking (= 0.69.4) + - React-RCTNetwork (= 0.69.4) + - React-RCTSettings (= 0.69.4) + - React-RCTText (= 0.69.4) + - React-RCTVibration (= 0.69.4) + - React-bridging (0.69.4): + - RCT-Folly (= 2021.06.28.00-v2) + - React-jsi (= 0.69.4) + - React-callinvoker (0.69.4) + - React-Codegen (0.69.4): + - FBReactNativeSpec (= 0.69.4) + - RCT-Folly (= 2021.06.28.00-v2) + - RCTRequired (= 0.69.4) + - RCTTypeSafety (= 0.69.4) + - React-Core (= 0.69.4) + - React-jsi (= 0.69.4) + - React-jsiexecutor (= 0.69.4) + - ReactCommon/turbomodule/core (= 0.69.4) + - React-Core (0.69.4): + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-Core/Default (= 0.69.4) + - React-cxxreact (= 0.69.4) + - React-jsi (= 0.69.4) + - React-jsiexecutor (= 0.69.4) + - React-perflogger (= 0.69.4) + - Yoga + - React-Core/CoreModulesHeaders (0.69.4): + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-Core/Default + - React-cxxreact (= 0.69.4) + - React-jsi (= 0.69.4) + - React-jsiexecutor (= 0.69.4) + - React-perflogger (= 0.69.4) + - Yoga + - React-Core/Default (0.69.4): + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-cxxreact (= 0.69.4) + - React-jsi (= 0.69.4) + - React-jsiexecutor (= 0.69.4) + - React-perflogger (= 0.69.4) + - Yoga + - React-Core/DevSupport (0.69.4): + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-Core/Default (= 0.69.4) + - React-Core/RCTWebSocket (= 0.69.4) + - React-cxxreact (= 0.69.4) + - React-jsi (= 0.69.4) + - React-jsiexecutor (= 0.69.4) + - React-jsinspector (= 0.69.4) + - React-perflogger (= 0.69.4) + - Yoga + - React-Core/RCTActionSheetHeaders (0.69.4): + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-Core/Default + - React-cxxreact (= 0.69.4) + - React-jsi (= 0.69.4) + - React-jsiexecutor (= 0.69.4) + - React-perflogger (= 0.69.4) + - Yoga + - React-Core/RCTAnimationHeaders (0.69.4): + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-Core/Default + - React-cxxreact (= 0.69.4) + - React-jsi (= 0.69.4) + - React-jsiexecutor (= 0.69.4) + - React-perflogger (= 0.69.4) + - Yoga + - React-Core/RCTBlobHeaders (0.69.4): + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-Core/Default + - React-cxxreact (= 0.69.4) + - React-jsi (= 0.69.4) + - React-jsiexecutor (= 0.69.4) + - React-perflogger (= 0.69.4) + - Yoga + - React-Core/RCTImageHeaders (0.69.4): + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-Core/Default + - React-cxxreact (= 0.69.4) + - React-jsi (= 0.69.4) + - React-jsiexecutor (= 0.69.4) + - React-perflogger (= 0.69.4) + - Yoga + - React-Core/RCTLinkingHeaders (0.69.4): + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-Core/Default + - React-cxxreact (= 0.69.4) + - React-jsi (= 0.69.4) + - React-jsiexecutor (= 0.69.4) + - React-perflogger (= 0.69.4) + - Yoga + - React-Core/RCTNetworkHeaders (0.69.4): + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-Core/Default + - React-cxxreact (= 0.69.4) + - React-jsi (= 0.69.4) + - React-jsiexecutor (= 0.69.4) + - React-perflogger (= 0.69.4) + - Yoga + - React-Core/RCTSettingsHeaders (0.69.4): + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-Core/Default + - React-cxxreact (= 0.69.4) + - React-jsi (= 0.69.4) + - React-jsiexecutor (= 0.69.4) + - React-perflogger (= 0.69.4) + - Yoga + - React-Core/RCTTextHeaders (0.69.4): + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-Core/Default + - React-cxxreact (= 0.69.4) + - React-jsi (= 0.69.4) + - React-jsiexecutor (= 0.69.4) + - React-perflogger (= 0.69.4) + - Yoga + - React-Core/RCTVibrationHeaders (0.69.4): + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-Core/Default + - React-cxxreact (= 0.69.4) + - React-jsi (= 0.69.4) + - React-jsiexecutor (= 0.69.4) + - React-perflogger (= 0.69.4) + - Yoga + - React-Core/RCTWebSocket (0.69.4): + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-Core/Default (= 0.69.4) + - React-cxxreact (= 0.69.4) + - React-jsi (= 0.69.4) + - React-jsiexecutor (= 0.69.4) + - React-perflogger (= 0.69.4) + - Yoga + - React-CoreModules (0.69.4): + - RCT-Folly (= 2021.06.28.00-v2) + - RCTTypeSafety (= 0.69.4) + - React-Codegen (= 0.69.4) + - React-Core/CoreModulesHeaders (= 0.69.4) + - React-jsi (= 0.69.4) + - React-RCTImage (= 0.69.4) + - ReactCommon/turbomodule/core (= 0.69.4) + - React-cxxreact (0.69.4): + - boost (= 1.76.0) + - DoubleConversion + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-callinvoker (= 0.69.4) + - React-jsi (= 0.69.4) + - React-jsinspector (= 0.69.4) + - React-logger (= 0.69.4) + - React-perflogger (= 0.69.4) + - React-runtimeexecutor (= 0.69.4) + - React-hermes (0.69.4): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2021.06.28.00-v2) + - RCT-Folly/Futures (= 2021.06.28.00-v2) + - React-cxxreact (= 0.69.4) + - React-jsi (= 0.69.4) + - React-jsiexecutor (= 0.69.4) + - React-jsinspector (= 0.69.4) + - React-perflogger (= 0.69.4) + - React-jsi (0.69.4): + - boost (= 1.76.0) + - DoubleConversion + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-jsi/Default (= 0.69.4) + - React-jsi/Default (0.69.4): + - boost (= 1.76.0) + - DoubleConversion + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-jsiexecutor (0.69.4): + - DoubleConversion + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-cxxreact (= 0.69.4) + - React-jsi (= 0.69.4) + - React-perflogger (= 0.69.4) + - React-jsinspector (0.69.4) + - React-logger (0.69.4): + - glog + - react-native-safe-area-context (4.3.1): + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React + - ReactCommon/turbomodule/core + - React-perflogger (0.69.4) + - React-RCTActionSheet (0.69.4): + - React-Core/RCTActionSheetHeaders (= 0.69.4) + - React-RCTAnimation (0.69.4): + - RCT-Folly (= 2021.06.28.00-v2) + - RCTTypeSafety (= 0.69.4) + - React-Codegen (= 0.69.4) + - React-Core/RCTAnimationHeaders (= 0.69.4) + - React-jsi (= 0.69.4) + - ReactCommon/turbomodule/core (= 0.69.4) + - React-RCTBlob (0.69.4): + - RCT-Folly (= 2021.06.28.00-v2) + - React-Codegen (= 0.69.4) + - React-Core/RCTBlobHeaders (= 0.69.4) + - React-Core/RCTWebSocket (= 0.69.4) + - React-jsi (= 0.69.4) + - React-RCTNetwork (= 0.69.4) + - ReactCommon/turbomodule/core (= 0.69.4) + - React-RCTImage (0.69.4): + - RCT-Folly (= 2021.06.28.00-v2) + - RCTTypeSafety (= 0.69.4) + - React-Codegen (= 0.69.4) + - React-Core/RCTImageHeaders (= 0.69.4) + - React-jsi (= 0.69.4) + - React-RCTNetwork (= 0.69.4) + - ReactCommon/turbomodule/core (= 0.69.4) + - React-RCTLinking (0.69.4): + - React-Codegen (= 0.69.4) + - React-Core/RCTLinkingHeaders (= 0.69.4) + - React-jsi (= 0.69.4) + - ReactCommon/turbomodule/core (= 0.69.4) + - React-RCTNetwork (0.69.4): + - RCT-Folly (= 2021.06.28.00-v2) + - RCTTypeSafety (= 0.69.4) + - React-Codegen (= 0.69.4) + - React-Core/RCTNetworkHeaders (= 0.69.4) + - React-jsi (= 0.69.4) + - ReactCommon/turbomodule/core (= 0.69.4) + - React-RCTSettings (0.69.4): + - RCT-Folly (= 2021.06.28.00-v2) + - RCTTypeSafety (= 0.69.4) + - React-Codegen (= 0.69.4) + - React-Core/RCTSettingsHeaders (= 0.69.4) + - React-jsi (= 0.69.4) + - ReactCommon/turbomodule/core (= 0.69.4) + - React-RCTText (0.69.4): + - React-Core/RCTTextHeaders (= 0.69.4) + - React-RCTVibration (0.69.4): + - RCT-Folly (= 2021.06.28.00-v2) + - React-Codegen (= 0.69.4) + - React-Core/RCTVibrationHeaders (= 0.69.4) + - React-jsi (= 0.69.4) + - ReactCommon/turbomodule/core (= 0.69.4) + - React-runtimeexecutor (0.69.4): + - React-jsi (= 0.69.4) + - ReactCommon/turbomodule/core (0.69.4): + - DoubleConversion + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-bridging (= 0.69.4) + - React-callinvoker (= 0.69.4) + - React-Core (= 0.69.4) + - React-cxxreact (= 0.69.4) + - React-jsi (= 0.69.4) + - React-logger (= 0.69.4) + - React-perflogger (= 0.69.4) + - RNCAsyncStorage (1.17.7): + - React-Core + - RNCMaskedView (0.2.7): + - React-Core + - RNGestureHandler (2.5.0): + - React-Core + - RNReanimated (2.9.1): + - DoubleConversion + - FBLazyVector + - FBReactNativeSpec + - glog + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-Core/DevSupport + - React-Core/RCTWebSocket + - React-CoreModules + - React-cxxreact + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-RCTActionSheet + - React-RCTAnimation + - React-RCTBlob + - React-RCTImage + - React-RCTLinking + - React-RCTNetwork + - React-RCTSettings + - React-RCTText + - ReactCommon/turbomodule/core + - Yoga + - RNScreens (3.15.0): + - React-Core + - React-RCTImage + - RNSVG (13.0.0): + - React-Core + - Yoga (1.14.0) + +DEPENDENCIES: + - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) + - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) + - "EXApplication (from `../node_modules/.pnpm/expo-application@4.2.2_expo@46.0.9/node_modules/expo-application/ios`)" + - "EXConstants (from `../node_modules/.pnpm/expo-constants@13.2.3_expo@46.0.9/node_modules/expo-constants/ios`)" + - "EXFileSystem (from `../node_modules/.pnpm/expo-file-system@14.1.0_expo@46.0.9/node_modules/expo-file-system/ios`)" + - "EXFont (from `../node_modules/.pnpm/expo-font@10.2.0_expo@46.0.9/node_modules/expo-font/ios`)" + - "Expo (from `../node_modules/.pnpm/expo@46.0.9_@babel+core@7.18.10/node_modules/expo`)" + - "ExpoKeepAwake (from `../node_modules/.pnpm/expo-keep-awake@10.2.0_expo@46.0.9/node_modules/expo-keep-awake/ios`)" + - "ExpoModulesCore (from `../node_modules/.pnpm/expo-modules-core@0.11.4/node_modules/expo-modules-core/ios`)" + - "EXSplashScreen (from `../node_modules/.pnpm/expo-splash-screen@0.16.2_expo@46.0.9/node_modules/expo-splash-screen/ios`)" + - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) + - FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`) + - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) + - hermes-engine (from `../node_modules/react-native/sdks/hermes/hermes-engine.podspec`) + - libevent (~> 2.1.12) + - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) + - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`) + - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) + - React (from `../node_modules/react-native/`) + - React-bridging (from `../node_modules/react-native/ReactCommon`) + - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) + - React-Codegen (from `build/generated/ios`) + - React-Core (from `../node_modules/react-native/`) + - React-Core/RCTWebSocket (from `../node_modules/react-native/`) + - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) + - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) + - React-hermes (from `../node_modules/react-native/ReactCommon/hermes`) + - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) + - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) + - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) + - React-logger (from `../node_modules/react-native/ReactCommon/logger`) + - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) + - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) + - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) + - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) + - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) + - React-RCTImage (from `../node_modules/react-native/Libraries/Image`) + - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`) + - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`) + - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`) + - React-RCTText (from `../node_modules/react-native/Libraries/Text`) + - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) + - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) + - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" + - "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)" + - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) + - RNReanimated (from `../node_modules/react-native-reanimated`) + - RNScreens (from `../node_modules/react-native-screens`) + - RNSVG (from `../node_modules/react-native-svg`) + - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) + +SPEC REPOS: + trunk: + - fmt + - libevent + +EXTERNAL SOURCES: + boost: + :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + DoubleConversion: + :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" + EXApplication: + :path: "../node_modules/.pnpm/expo-application@4.2.2_expo@46.0.9/node_modules/expo-application/ios" + EXConstants: + :path: "../node_modules/.pnpm/expo-constants@13.2.3_expo@46.0.9/node_modules/expo-constants/ios" + EXFileSystem: + :path: "../node_modules/.pnpm/expo-file-system@14.1.0_expo@46.0.9/node_modules/expo-file-system/ios" + EXFont: + :path: "../node_modules/.pnpm/expo-font@10.2.0_expo@46.0.9/node_modules/expo-font/ios" + Expo: + :path: "../node_modules/.pnpm/expo@46.0.9_@babel+core@7.18.10/node_modules/expo" + ExpoKeepAwake: + :path: "../node_modules/.pnpm/expo-keep-awake@10.2.0_expo@46.0.9/node_modules/expo-keep-awake/ios" + ExpoModulesCore: + :path: "../node_modules/.pnpm/expo-modules-core@0.11.4/node_modules/expo-modules-core/ios" + EXSplashScreen: + :path: "../node_modules/.pnpm/expo-splash-screen@0.16.2_expo@46.0.9/node_modules/expo-splash-screen/ios" + FBLazyVector: + :path: "../node_modules/react-native/Libraries/FBLazyVector" + FBReactNativeSpec: + :path: "../node_modules/react-native/React/FBReactNativeSpec" + glog: + :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" + hermes-engine: + :podspec: "../node_modules/react-native/sdks/hermes/hermes-engine.podspec" + RCT-Folly: + :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" + RCTRequired: + :path: "../node_modules/react-native/Libraries/RCTRequired" + RCTTypeSafety: + :path: "../node_modules/react-native/Libraries/TypeSafety" + React: + :path: "../node_modules/react-native/" + React-bridging: + :path: "../node_modules/react-native/ReactCommon" + React-callinvoker: + :path: "../node_modules/react-native/ReactCommon/callinvoker" + React-Codegen: + :path: build/generated/ios + React-Core: + :path: "../node_modules/react-native/" + React-CoreModules: + :path: "../node_modules/react-native/React/CoreModules" + React-cxxreact: + :path: "../node_modules/react-native/ReactCommon/cxxreact" + React-hermes: + :path: "../node_modules/react-native/ReactCommon/hermes" + React-jsi: + :path: "../node_modules/react-native/ReactCommon/jsi" + React-jsiexecutor: + :path: "../node_modules/react-native/ReactCommon/jsiexecutor" + React-jsinspector: + :path: "../node_modules/react-native/ReactCommon/jsinspector" + React-logger: + :path: "../node_modules/react-native/ReactCommon/logger" + react-native-safe-area-context: + :path: "../node_modules/react-native-safe-area-context" + React-perflogger: + :path: "../node_modules/react-native/ReactCommon/reactperflogger" + React-RCTActionSheet: + :path: "../node_modules/react-native/Libraries/ActionSheetIOS" + React-RCTAnimation: + :path: "../node_modules/react-native/Libraries/NativeAnimation" + React-RCTBlob: + :path: "../node_modules/react-native/Libraries/Blob" + React-RCTImage: + :path: "../node_modules/react-native/Libraries/Image" + React-RCTLinking: + :path: "../node_modules/react-native/Libraries/LinkingIOS" + React-RCTNetwork: + :path: "../node_modules/react-native/Libraries/Network" + React-RCTSettings: + :path: "../node_modules/react-native/Libraries/Settings" + React-RCTText: + :path: "../node_modules/react-native/Libraries/Text" + React-RCTVibration: + :path: "../node_modules/react-native/Libraries/Vibration" + React-runtimeexecutor: + :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" + ReactCommon: + :path: "../node_modules/react-native/ReactCommon" + RNCAsyncStorage: + :path: "../node_modules/@react-native-async-storage/async-storage" + RNCMaskedView: + :path: "../node_modules/@react-native-masked-view/masked-view" + RNGestureHandler: + :path: "../node_modules/react-native-gesture-handler" + RNReanimated: + :path: "../node_modules/react-native-reanimated" + RNScreens: + :path: "../node_modules/react-native-screens" + RNSVG: + :path: "../node_modules/react-native-svg" + Yoga: + :path: "../node_modules/react-native/ReactCommon/yoga" + +SPEC CHECKSUMS: + boost: a7c83b31436843459a1961bfd74b96033dc77234 + DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 + EXApplication: e418d737a036e788510f2c4ad6c10a7d54d18586 + EXConstants: 75c40827af38bd6bfcf69f880a5b45037eeff9c9 + EXFileSystem: 927e0a8885aa9c49e50fc38eaba2c2389f2f1019 + EXFont: a5d80bd9b3452b2d5abbce2487da89b0150e6487 + Expo: 73412414e62f5cbc6e713def821de70b92cd3ad6 + ExpoKeepAwake: 0e8f18142e71bbf2c7f6aa66ebed249ba1420320 + ExpoModulesCore: e281bb7b78ea47e227dd5af94d04b24d8b2e1255 + EXSplashScreen: 799bece80089219b2c989c1082d70f3b00995cda + FBLazyVector: c71b8c429a8af2aff1013934a7152e9d9d0c937d + FBReactNativeSpec: 3cc5cff7d792e74a875be91e56d6242335016f50 + fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 + glog: 3d02b25ca00c2d456734d0bcff864cbc62f6ae1a + hermes-engine: 761a544537e62df2a37189389b9d2654dc1f75af + libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 + RCT-Folly: b9d9fe1fc70114b751c076104e52f3b1b5e5a95a + RCTRequired: bd9d2ab0fda10171fcbcf9ba61a7df4dc15a28f4 + RCTTypeSafety: e44e139bf6ec8042db396201834fc2372f6a21cd + React: 482cd1ba23c471be1aed3800180be2427418d7be + React-bridging: c2ea4fed6fe4ed27c12fd71e88b5d5d3da107fde + React-callinvoker: d4d1f98163fb5e35545e910415ef6c04796bb188 + React-Codegen: ff35fb9c7f6ec2ed34fb6de2e1099d88dfb25f2f + React-Core: 4d3443a45b67c71d74d7243ddde9569d1e4f4fad + React-CoreModules: 70be25399366b5632ab18ecf6fe444a8165a7bea + React-cxxreact: 822d3794fc0bf206f4691592f90e086dd4f92228 + React-hermes: 7f67b8363288258c3b0cd4aef5975cb7f0b9549a + React-jsi: ffa51cbc9a78cc156cf61f79ed52ecb76dc6013b + React-jsiexecutor: a27badbbdbc0ff781813370736a2d1c7261181d4 + React-jsinspector: 8a3d3f5dcd23a91e8c80b1bf0e96902cd1dca999 + React-logger: 1088859f145b8f6dd0d3ed051a647ef0e3e80fad + react-native-safe-area-context: 6c12e3859b6f27b25de4fee8201cfb858432d8de + React-perflogger: cb386fd44c97ec7f8199c04c12b22066b0f2e1e0 + React-RCTActionSheet: f803a85e46cf5b4066c2ac5e122447f918e9c6e5 + React-RCTAnimation: 19c80fa950ccce7f4db76a2a7f2cf79baae07fc7 + React-RCTBlob: f36ab97e2d515c36df14a1571e50056be80413d5 + React-RCTImage: 2c8f0a329a116248e82f8972ffe806e47c6d1cfa + React-RCTLinking: 670f0223075aff33be3b89714f1da4f5343fc4af + React-RCTNetwork: 09385b73f4ff1f46bd5d749540fb33f69a7e5908 + React-RCTSettings: 33b12d3ac7a1f2eba069ec7bd1b84345263b3bbe + React-RCTText: a1a3ea902403bd9ae4cf6f7960551dc1d25711b5 + React-RCTVibration: 9adb4a3cbb598d1bbd46a05256f445e4b8c70603 + React-runtimeexecutor: 61ee22a8cdf8b6bb2a7fb7b4ba2cc763e5285196 + ReactCommon: 8f67bd7e0a6afade0f20718f859dc8c2275f2e83 + RNCAsyncStorage: d81ee5c3db1060afd49ea7045ad460eff82d2b7d + RNCMaskedView: cb9670ea9239998340eaab21df13fa12a1f9de15 + RNGestureHandler: bad495418bcbd3ab47017a38d93d290ebd406f50 + RNReanimated: 2cf7451318bb9cc430abeec8d67693f9cf4e039c + RNScreens: 4a1af06327774490d97342c00aee0c2bafb497b7 + RNSVG: 42a0c731b11179ebbd27a3eeeafa7201ebb476ff + Yoga: ff994563b2fd98c982ca58e8cd9db2cdaf4dda74 + +PODFILE CHECKSUM: b77befb1871220c1a94408eeae0857d78b685698 + +COCOAPODS: 1.11.3 diff --git a/apps/mobile/ios/Podfile.properties.json b/apps/mobile/ios/Podfile.properties.json new file mode 100644 index 000000000..35120e838 --- /dev/null +++ b/apps/mobile/ios/Podfile.properties.json @@ -0,0 +1,3 @@ +{ + "expo.jsEngine": "hermes" +} diff --git a/apps/mobile/ios/Spacedrive.xcodeproj/project.pbxproj b/apps/mobile/ios/Spacedrive.xcodeproj/project.pbxproj new file mode 100644 index 000000000..4299b55a6 --- /dev/null +++ b/apps/mobile/ios/Spacedrive.xcodeproj/project.pbxproj @@ -0,0 +1,668 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 08C620EC6F30A0663310BBC1 /* libPods-Spacedrive.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 080FB394AD883D9705C115A6 /* libPods-Spacedrive.a */; }; + 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; + 5574975428A2496C00851D5A /* SDCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 5574975328A2496C00851D5A /* SDCore.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; + C95AE27BB525EFF3F02CEC11 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E71A6EFA9EA8F4C11F42FA /* ExpoModulesProvider.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; + 080FB394AD883D9705C115A6 /* libPods-Spacedrive.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Spacedrive.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07F961A680F5B00A75B9A /* Spacedrive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Spacedrive.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AppDelegate.mm; sourceTree = ""; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 5574975328A2496C00851D5A /* SDCore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDCore.m; sourceTree = ""; }; + 5574975528A2498000851D5A /* SDCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDCore.h; sourceTree = ""; }; + 5574975628A24E0D00851D5A /* sdcore-universal-ios.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "sdcore-universal-ios.a"; path = "../../../target/sdcore-universal-ios.a"; sourceTree = ""; }; + 56E71A6EFA9EA8F4C11F42FA /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Spacedrive/ExpoModulesProvider.swift"; sourceTree = ""; }; + AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = SplashScreen.storyboard; sourceTree = ""; }; + BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Expo.plist; path = Supporting/Expo.plist; sourceTree = ""; }; + CD5534017C1F13AB6E57EC53 /* Pods-Spacedrive.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Spacedrive.debug.xcconfig"; path = "Target Support Files/Pods-Spacedrive/Pods-Spacedrive.debug.xcconfig"; sourceTree = ""; }; + E661E5E8069E06E621509713 /* Pods-Spacedrive.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Spacedrive.release.xcconfig"; path = "Target Support Files/Pods-Spacedrive/Pods-Spacedrive.release.xcconfig"; sourceTree = ""; }; + ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 08C620EC6F30A0663310BBC1 /* libPods-Spacedrive.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 13B07FAE1A68108700A75B9A /* Spacedrive */ = { + isa = PBXGroup; + children = ( + BB2F792B24A3F905000567C9 /* Supporting */, + 008F07F21AC5B25A0029DE68 /* main.jsbundle */, + 13B07FAF1A68108700A75B9A /* AppDelegate.h */, + 13B07FB01A68108700A75B9A /* AppDelegate.mm */, + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 13B07FB61A68108700A75B9A /* Info.plist */, + 13B07FB71A68108700A75B9A /* main.m */, + AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */, + 5574975328A2496C00851D5A /* SDCore.m */, + 5574975528A2498000851D5A /* SDCore.h */, + ); + path = Spacedrive; + sourceTree = ""; + }; + 2541029223E7A78AF70DBF1F /* Spacedrive */ = { + isa = PBXGroup; + children = ( + 56E71A6EFA9EA8F4C11F42FA /* ExpoModulesProvider.swift */, + ); + name = Spacedrive; + sourceTree = ""; + }; + 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5574975628A24E0D00851D5A /* sdcore-universal-ios.a */, + ED297162215061F000B7C4FE /* JavaScriptCore.framework */, + 080FB394AD883D9705C115A6 /* libPods-Spacedrive.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + ); + name = Libraries; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 13B07FAE1A68108700A75B9A /* Spacedrive */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 83CBBA001A601CBA00E9B192 /* Products */, + 2D16E6871FA4F8E400B85C8A /* Frameworks */, + D65327D7A22EEC0BE12398D9 /* Pods */, + D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* Spacedrive.app */, + ); + name = Products; + sourceTree = ""; + }; + BB2F792B24A3F905000567C9 /* Supporting */ = { + isa = PBXGroup; + children = ( + BB2F792C24A3F905000567C9 /* Expo.plist */, + ); + name = Supporting; + sourceTree = ""; + }; + D65327D7A22EEC0BE12398D9 /* Pods */ = { + isa = PBXGroup; + children = ( + CD5534017C1F13AB6E57EC53 /* Pods-Spacedrive.debug.xcconfig */, + E661E5E8069E06E621509713 /* Pods-Spacedrive.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */ = { + isa = PBXGroup; + children = ( + 2541029223E7A78AF70DBF1F /* Spacedrive */, + ); + name = ExpoModulesProviders; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 13B07F861A680F5B00A75B9A /* Spacedrive */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Spacedrive" */; + buildPhases = ( + 9F553C6F8AA059AB72DAA720 /* [CP] Check Pods Manifest.lock */, + FD10A7F022414F080027D42C /* Start Packager */, + 55B1130D28AB3061006C377F /* Build Spacedrive Core */, + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + C93832A891603A9ED877B5D2 /* [CP] Embed Pods Frameworks */, + 20C3591E751EF7109AA55859 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Spacedrive; + productName = mobilenew; + productReference = 13B07F961A680F5B00A75B9A /* Spacedrive.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1130; + TargetAttributes = { + 13B07F861A680F5B00A75B9A = { + DevelopmentTeam = 72SE38W7T9; + LastSwiftMigration = 1250; + }; + }; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Spacedrive" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* Spacedrive */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BB2F792D24A3F905000567C9 /* Expo.plist in Resources */, + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export NODE_BINARY=node\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\n`node --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n"; + }; + 20C3591E751EF7109AA55859 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Spacedrive/Pods-Spacedrive-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Spacedrive/Pods-Spacedrive-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 55B1130D28AB3061006C377F /* Build Spacedrive Core */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "../../../core/src/*.rs", + "../rust/src/*.rs", + "../../../core/src/*/*.rs", + ); + name = "Build Spacedrive Core"; + outputFileListPaths = ( + ); + outputPaths = ( + "../../../target/sdcore-universal-ios.a", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/zsh; + shellScript = "set -e\n\nif [[ -n \"${DEVELOPER_SDK_DIR:-}\" ]]; then\n # Assume we're in Xcode, which means we're probably cross-compiling.\n # In this case, we need to add an extra library search path for build scripts and proc-macros,\n # which run on the host instead of the target.\n # (macOS Big Sur does not have linkable libraries in /usr/lib/.)\n export LIBRARY_PATH=\"${DEVELOPER_SDK_DIR}/MacOSX.sdk/usr/lib:${LIBRARY_PATH:-}\"\nfi\n\nCARGO_FLAGS=\nif [[ \"$BUILDVARIANT\" != \"debug\" ]]; then\n CARGO_FLAGS=--release\nfi\n\nTARGET_DIRECTORY=../../../target\nif [[ $PLATFORM_NAME = \"iphonesimulator\" ]]\nthen\n cargo build -p sdcore-lib $CARGO_FLAGS --lib --target aarch64-apple-ios-sim\n lipo -create -output $TARGET_DIRECTORY/libsdcore-iossim.a $TARGET_DIRECTORY/aarch64-apple-ios-sim/release/libsdcore.a\nelse\n cargo build -p sdcore-lib $CARGO_FLAGS --lib --target aarch64-apple-ios\n lipo -create -output $TARGET_DIRECTORY/libsdcore-ios.a $TARGET_DIRECTORY/aarch64-apple-ios/release/libsdcore.a\nfi\n"; + }; + 9F553C6F8AA059AB72DAA720 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Spacedrive-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + C93832A891603A9ED877B5D2 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Spacedrive/Pods-Spacedrive-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/hermes.framework/hermes", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Spacedrive/Pods-Spacedrive-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + FD10A7F022414F080027D42C /* Start Packager */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Start Packager"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > `node --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/.packager.env'\"`\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open `node --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/launchPackager.command'\"` || echo \"Can't start packager automatically\"\n fi\nfi\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */, + 13B07FC11A68108700A75B9A /* main.m in Sources */, + C95AE27BB525EFF3F02CEC11 /* ExpoModulesProvider.swift in Sources */, + 5574975428A2496C00851D5A /* SDCore.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CD5534017C1F13AB6E57EC53 /* Pods-Spacedrive.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Spacedrive/Spacedrive.entitlements; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 72SE38W7T9; + ENABLE_BITCODE = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "FB_SONARKIT_ENABLED=1", + ); + INFOPLIST_FILE = Spacedrive/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/DoubleConversion\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/EXApplication\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/EXFont\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/Expo\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/ExpoKeepAwake\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/ExpoModulesCore\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/RCTTypeSafety\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/RNCMaskedView\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/RNGestureHandler\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/RNReanimated\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/RNScreens\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-Codegen\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-Core\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-CoreModules\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTAnimation\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTBlob\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTImage\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTLinking\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTNetwork\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTSettings\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTText\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTVibration\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-bridging\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-hermes\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-jsi\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-jsiexecutor\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-jsinspector\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-logger\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-perflogger\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/Yoga\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/fmt\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/glog\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/libevent\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/react-native-safe-area-context\"", + /usr/lib/swift, + ../../../target, + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + "OTHER_LDFLAGS[sdk=iphoneos*]" = ( + "$(inherited)", + "-ObjC", + "-lc++", + "-lsdcore-ios", + ); + "OTHER_LDFLAGS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "-ObjC", + "-lc++", + "-lsdcore-iossim", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = com.spacedrive.app; + PRODUCT_NAME = Spacedrive; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E661E5E8069E06E621509713 /* Pods-Spacedrive.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Spacedrive/Spacedrive.entitlements; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 72SE38W7T9; + INFOPLIST_FILE = Spacedrive/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/DoubleConversion\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/EXApplication\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/EXFont\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/Expo\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/ExpoKeepAwake\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/ExpoModulesCore\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/RCTTypeSafety\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/RNCMaskedView\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/RNGestureHandler\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/RNReanimated\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/RNScreens\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-Codegen\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-Core\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-CoreModules\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTAnimation\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTBlob\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTImage\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTLinking\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTNetwork\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTSettings\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTText\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTVibration\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-bridging\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-hermes\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-jsi\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-jsiexecutor\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-jsinspector\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-logger\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-perflogger\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/Yoga\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/fmt\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/glog\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/libevent\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/react-native-safe-area-context\"", + /usr/lib/swift, + ../../../target, + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + "OTHER_LDFLAGS[sdk=iphoneos*]" = ( + "$(inherited)", + "-ObjC", + "-lc++", + "-lsdcore-ios", + ); + "OTHER_LDFLAGS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "-ObjC", + "-lc++", + "-lsdcore-iossim", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = com.spacedrive.app; + PRODUCT_NAME = Spacedrive; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = "\"$(inherited)\""; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = "\"$(inherited)\""; + MTL_ENABLE_DEBUG_INFO = NO; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Spacedrive" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Spacedrive" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/apps/mobile/ios/Spacedrive.xcodeproj/xcshareddata/xcschemes/Spacedrive.xcscheme b/apps/mobile/ios/Spacedrive.xcodeproj/xcshareddata/xcschemes/Spacedrive.xcscheme new file mode 100644 index 000000000..05655c640 --- /dev/null +++ b/apps/mobile/ios/Spacedrive.xcodeproj/xcshareddata/xcschemes/Spacedrive.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/ios/Spacedrive/AppDelegate.h b/apps/mobile/ios/Spacedrive/AppDelegate.h new file mode 100644 index 000000000..f7d297204 --- /dev/null +++ b/apps/mobile/ios/Spacedrive/AppDelegate.h @@ -0,0 +1,9 @@ +#import +#import +#import + +#import + +@interface AppDelegate : EXAppDelegateWrapper + +@end diff --git a/apps/mobile/ios/Spacedrive/AppDelegate.mm b/apps/mobile/ios/Spacedrive/AppDelegate.mm new file mode 100644 index 000000000..a6e13e11a --- /dev/null +++ b/apps/mobile/ios/Spacedrive/AppDelegate.mm @@ -0,0 +1,166 @@ +#import "AppDelegate.h" + +#import +#import +#import +#import +#import + +#import + +#if RCT_NEW_ARCH_ENABLED +#import +#import +#import +#import +#import +#import + +#import + +static NSString *const kRNConcurrentRoot = @"concurrentRoot"; + +@interface AppDelegate () { + RCTTurboModuleManager *_turboModuleManager; + RCTSurfacePresenterBridgeAdapter *_bridgeAdapter; + std::shared_ptr _reactNativeConfig; + facebook::react::ContextContainer::Shared _contextContainer; +} +@end +#endif + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + RCTAppSetupPrepareApp(application); + + RCTBridge *bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:launchOptions]; + +#if RCT_NEW_ARCH_ENABLED + _contextContainer = std::make_shared(); + _reactNativeConfig = std::make_shared(); + _contextContainer->insert("ReactNativeConfig", _reactNativeConfig); + _bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer]; + bridge.surfacePresenter = _bridgeAdapter.surfacePresenter; +#endif + + NSDictionary *initProps = [self prepareInitialProps]; + UIView *rootView = [self.reactDelegate createRootViewWithBridge:bridge moduleName:@"main" initialProperties:initProps]; + + rootView.backgroundColor = [UIColor whiteColor]; + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + UIViewController *rootViewController = [self.reactDelegate createRootViewController]; + rootViewController.view = rootView; + self.window.rootViewController = rootViewController; + [self.window makeKeyAndVisible]; + + [super application:application didFinishLaunchingWithOptions:launchOptions]; + + return YES; +} + +- (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge +{ + // If you'd like to export some custom RCTBridgeModules, add them here! + return @[]; +} + +/// This method controls whether the `concurrentRoot`feature of React18 is turned on or off. +/// +/// @see: https://reactjs.org/blog/2022/03/29/react-v18.html +/// @note: This requires to be rendering on Fabric (i.e. on the New Architecture). +/// @return: `true` if the `concurrentRoot` feture is enabled. Otherwise, it returns `false`. +- (BOOL)concurrentRootEnabled +{ + // Switch this bool to turn on and off the concurrent root + return true; +} + +- (NSDictionary *)prepareInitialProps +{ + NSMutableDictionary *initProps = [NSMutableDictionary new]; +#if RCT_NEW_ARCH_ENABLED + initProps[kRNConcurrentRoot] = @([self concurrentRootEnabled]); +#endif + return initProps; +} + +- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge +{ +#if DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; +#else + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; +#endif +} + +// Linking API +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { + return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options]; +} + +// Universal Links +- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { + BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; + return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result; +} + +// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries +- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken +{ + return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; +} + +// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries +- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error +{ + return [super application:application didFailToRegisterForRemoteNotificationsWithError:error]; +} + +// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler +{ + return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; +} + +#if RCT_NEW_ARCH_ENABLED + +#pragma mark - RCTCxxBridgeDelegate + +- (std::unique_ptr)jsExecutorFactoryForBridge:(RCTBridge *)bridge +{ + _turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge + delegate:self + jsInvoker:bridge.jsCallInvoker]; + return RCTAppSetupDefaultJsExecutorFactory(bridge, _turboModuleManager); +} + +#pragma mark RCTTurboModuleManagerDelegate + +- (Class)getModuleClassFromName:(const char *)name +{ + return RCTCoreModulesClassProvider(name); +} + +- (std::shared_ptr)getTurboModule:(const std::string &)name + jsInvoker:(std::shared_ptr)jsInvoker +{ + return nullptr; +} + +- (std::shared_ptr)getTurboModule:(const std::string &)name + initParams: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return nullptr; +} + +- (id)getModuleInstanceFromClass:(Class)moduleClass +{ + return RCTAppSetupDefaultModuleFromClass(moduleClass); +} + +#endif + +@end diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png new file mode 100644 index 000000000..31eba8cc9 Binary files /dev/null and b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png differ diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png new file mode 100644 index 000000000..c74ab91fe Binary files /dev/null and b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png differ diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png new file mode 100644 index 000000000..e0312ff81 Binary files /dev/null and b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png differ diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png new file mode 100644 index 000000000..4d9155e1a Binary files /dev/null and b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png differ diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png new file mode 100644 index 000000000..30cf544d1 Binary files /dev/null and b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png differ diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png new file mode 100644 index 000000000..4285151f6 Binary files /dev/null and b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png differ diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png new file mode 100644 index 000000000..c74ab91fe Binary files /dev/null and b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png differ diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png new file mode 100644 index 000000000..b507515fc Binary files /dev/null and b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png differ diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png new file mode 100644 index 000000000..4ce4dc33e Binary files /dev/null and b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png differ diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png new file mode 100644 index 000000000..4ce4dc33e Binary files /dev/null and b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png differ diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png new file mode 100644 index 000000000..da848157a Binary files /dev/null and b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png differ diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png new file mode 100644 index 000000000..035654dc2 Binary files /dev/null and b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png differ diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png new file mode 100644 index 000000000..d37a38fe5 Binary files /dev/null and b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png differ diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png new file mode 100644 index 000000000..1f8761d3a Binary files /dev/null and b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png differ diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/Contents.json b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..f920cb0ec --- /dev/null +++ b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images": [ + { + "idiom": "iphone", + "size": "20x20", + "scale": "2x", + "filename": "App-Icon-20x20@2x.png" + }, + { + "idiom": "iphone", + "size": "20x20", + "scale": "3x", + "filename": "App-Icon-20x20@3x.png" + }, + { + "idiom": "iphone", + "size": "29x29", + "scale": "1x", + "filename": "App-Icon-29x29@1x.png" + }, + { + "idiom": "iphone", + "size": "29x29", + "scale": "2x", + "filename": "App-Icon-29x29@2x.png" + }, + { + "idiom": "iphone", + "size": "29x29", + "scale": "3x", + "filename": "App-Icon-29x29@3x.png" + }, + { + "idiom": "iphone", + "size": "40x40", + "scale": "2x", + "filename": "App-Icon-40x40@2x.png" + }, + { + "idiom": "iphone", + "size": "40x40", + "scale": "3x", + "filename": "App-Icon-40x40@3x.png" + }, + { + "idiom": "iphone", + "size": "60x60", + "scale": "2x", + "filename": "App-Icon-60x60@2x.png" + }, + { + "idiom": "iphone", + "size": "60x60", + "scale": "3x", + "filename": "App-Icon-60x60@3x.png" + }, + { + "idiom": "ipad", + "size": "20x20", + "scale": "1x", + "filename": "App-Icon-20x20@1x.png" + }, + { + "idiom": "ipad", + "size": "20x20", + "scale": "2x", + "filename": "App-Icon-20x20@2x.png" + }, + { + "idiom": "ipad", + "size": "29x29", + "scale": "1x", + "filename": "App-Icon-29x29@1x.png" + }, + { + "idiom": "ipad", + "size": "29x29", + "scale": "2x", + "filename": "App-Icon-29x29@2x.png" + }, + { + "idiom": "ipad", + "size": "40x40", + "scale": "1x", + "filename": "App-Icon-40x40@1x.png" + }, + { + "idiom": "ipad", + "size": "40x40", + "scale": "2x", + "filename": "App-Icon-40x40@2x.png" + }, + { + "idiom": "ipad", + "size": "76x76", + "scale": "1x", + "filename": "App-Icon-76x76@1x.png" + }, + { + "idiom": "ipad", + "size": "76x76", + "scale": "2x", + "filename": "App-Icon-76x76@2x.png" + }, + { + "idiom": "ipad", + "size": "83.5x83.5", + "scale": "2x", + "filename": "App-Icon-83.5x83.5@2x.png" + }, + { + "idiom": "ios-marketing", + "size": "1024x1024", + "scale": "1x", + "filename": "ItunesArtwork@2x.png" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png new file mode 100644 index 000000000..962808d14 Binary files /dev/null and b/apps/mobile/ios/Spacedrive/Images.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png differ diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/Contents.json b/apps/mobile/ios/Spacedrive/Images.xcassets/Contents.json new file mode 100644 index 000000000..ed285c2e5 --- /dev/null +++ b/apps/mobile/ios/Spacedrive/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "expo" + } +} diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/SplashScreen.imageset/Contents.json b/apps/mobile/ios/Spacedrive/Images.xcassets/SplashScreen.imageset/Contents.json new file mode 100644 index 000000000..3cf848977 --- /dev/null +++ b/apps/mobile/ios/Spacedrive/Images.xcassets/SplashScreen.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images": [ + { + "idiom": "universal", + "filename": "image.png", + "scale": "1x" + }, + { + "idiom": "universal", + "scale": "2x" + }, + { + "idiom": "universal", + "scale": "3x" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/SplashScreen.imageset/image.png b/apps/mobile/ios/Spacedrive/Images.xcassets/SplashScreen.imageset/image.png new file mode 100644 index 000000000..0e0762212 Binary files /dev/null and b/apps/mobile/ios/Spacedrive/Images.xcassets/SplashScreen.imageset/image.png differ diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/SplashScreenBackground.imageset/Contents.json b/apps/mobile/ios/Spacedrive/Images.xcassets/SplashScreenBackground.imageset/Contents.json new file mode 100644 index 000000000..3cf848977 --- /dev/null +++ b/apps/mobile/ios/Spacedrive/Images.xcassets/SplashScreenBackground.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images": [ + { + "idiom": "universal", + "filename": "image.png", + "scale": "1x" + }, + { + "idiom": "universal", + "scale": "2x" + }, + { + "idiom": "universal", + "scale": "3x" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/apps/mobile/ios/Spacedrive/Images.xcassets/SplashScreenBackground.imageset/image.png b/apps/mobile/ios/Spacedrive/Images.xcassets/SplashScreenBackground.imageset/image.png new file mode 100644 index 000000000..837b3d577 Binary files /dev/null and b/apps/mobile/ios/Spacedrive/Images.xcassets/SplashScreenBackground.imageset/image.png differ diff --git a/apps/mobile/ios/Spacedrive/Info.plist b/apps/mobile/ios/Spacedrive/Info.plist new file mode 100644 index 000000000..d9d78a1b6 --- /dev/null +++ b/apps/mobile/ios/Spacedrive/Info.plist @@ -0,0 +1,81 @@ + + + + + UIBackgroundModes + + remote-notification + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Spacedrive + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 0.0.1 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + spacedrive + com.spacedrive.app + + + + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSExceptionDomains + + localhost + + NSExceptionAllowsInsecureHTTPLoads + + + + + UILaunchStoryboardName + SplashScreen + UIRequiredDeviceCapabilities + + armv7 + + UIRequiresFullScreen + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIUserInterfaceStyle + Automatic + UIViewControllerBasedStatusBarAppearance + + + diff --git a/apps/mobile/ios/Spacedrive/SDCore.h b/apps/mobile/ios/Spacedrive/SDCore.h new file mode 100644 index 000000000..cdcac491d --- /dev/null +++ b/apps/mobile/ios/Spacedrive/SDCore.h @@ -0,0 +1,17 @@ +// +// SDCore.h +// Spacedrive +// +// Created by Oscar Beaumont on 9/8/2022. +// + +#import +#import + +#ifndef SDCore_h +#define SDCore_h + +@interface SDCore : RCTEventEmitter +@end + +#endif /* SDCore_h */ diff --git a/apps/mobile/ios/Spacedrive/SDCore.m b/apps/mobile/ios/Spacedrive/SDCore.m new file mode 100644 index 000000000..969b2c106 --- /dev/null +++ b/apps/mobile/ios/Spacedrive/SDCore.m @@ -0,0 +1,81 @@ +// +// SDCore.m +// Spacedrive +// +// This file will not work unless ARC is disabled. Do this by setting the compiler flag '-fno-objc-arc' on this file in Settings > Build Phases > Compile Sources. +// This file also expects the Spacedrive Rust library to be linked in Settings > Build Phases > Link Binary with Libraries. This is the crude way, you should link the core with custom linker flags so that you can do it conditonally based on target OS. +// This file also expects a Build Phase to be setup which compiles the Rust prior to linking the sdcore library to the IOS build. +// This file also expects you to add "remote-notification" to the list of your supported UIBackgroundModes in your Info.plist +// +// Created by Oscar Beaumont on 9/8/2022. +// + +#import "SDCore.h" +#import + +// is a function defined in Rust which starts a listener for Rust events. +void register_core_event_listener(id objc_class); + +// is a function defined in Rust which is responsible for handling messages from the frontend. +void sd_core_msg(const char* query, void* resolve); + +// is called by Rust to determine the base directory to store data in. This is only done when initialising the Node. +const char* get_data_directory(void) +{ + NSArray *dirPaths = dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, + NSUserDomainMask, YES); + const char *docDir = [ [dirPaths objectAtIndex:0] UTF8String]; + return docDir; +} + +// is called by Rust with a void* to the resolve function (of type RCTPromiseResolveBlock) to call it. +// Due to 'RCTPromiseResolveBlock' being an Objective-C block it is hard to call from Rust. +void call_resolve(void *resolvePtr, const char* resultRaw) +{ + RCTPromiseResolveBlock resolve = (__bridge RCTPromiseResolveBlock) resolvePtr; + NSString *result = [NSString stringWithUTF8String:resultRaw]; + resolve(result); + [result release]; +} + +@implementation SDCore +{ + bool registeredWithRust; + bool hasListeners; +} + +-(void)startObserving { + if (!registeredWithRust) + { + register_core_event_listener(self); + registeredWithRust = true; + } + + hasListeners = YES; +} + +-(void)stopObserving { + hasListeners = NO; +} + +- (void)sendCoreEvent: (NSString*)query { + if (hasListeners) { + [self sendEventWithName:@"SDCoreEvent" body:query]; + } +} + +- (NSArray *)supportedEvents { + return @[@"SDCoreEvent"]; +} + +RCT_EXPORT_MODULE(); + +RCT_EXPORT_METHOD(sd_core_msg: (NSString *)queryRaw + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + const char *query = [queryRaw UTF8String]; + sd_core_msg(query, (__bridge void*) [resolve retain]); +} + +@end diff --git a/apps/mobile/ios/Spacedrive/Spacedrive.entitlements b/apps/mobile/ios/Spacedrive/Spacedrive.entitlements new file mode 100644 index 000000000..018a6e20c --- /dev/null +++ b/apps/mobile/ios/Spacedrive/Spacedrive.entitlements @@ -0,0 +1,8 @@ + + + + + aps-environment + development + + \ No newline at end of file diff --git a/apps/mobile/ios/Spacedrive/SplashScreen.storyboard b/apps/mobile/ios/Spacedrive/SplashScreen.storyboard new file mode 100644 index 000000000..ed03a5299 --- /dev/null +++ b/apps/mobile/ios/Spacedrive/SplashScreen.storyboard @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/mobile/ios/Spacedrive/Supporting/Expo.plist b/apps/mobile/ios/Spacedrive/Supporting/Expo.plist new file mode 100644 index 000000000..3700426dd --- /dev/null +++ b/apps/mobile/ios/Spacedrive/Supporting/Expo.plist @@ -0,0 +1,16 @@ + + + + + EXUpdatesCheckOnLaunch + ALWAYS + EXUpdatesEnabled + + EXUpdatesLaunchWaitMs + 0 + EXUpdatesSDKVersion + 46.0.0 + EXUpdatesURL + https://exp.host/@utkudev/spacedrive + + \ No newline at end of file diff --git a/apps/mobile/ios/Spacedrive/main.m b/apps/mobile/ios/Spacedrive/main.m new file mode 100644 index 000000000..25181b6cc --- /dev/null +++ b/apps/mobile/ios/Spacedrive/main.m @@ -0,0 +1,10 @@ +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} + diff --git a/apps/mobile/ios/spacedrive.xcworkspace/contents.xcworkspacedata b/apps/mobile/ios/spacedrive.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..64dde6616 --- /dev/null +++ b/apps/mobile/ios/spacedrive.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/apps/mobile/ios/spacedrive.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/mobile/ios/spacedrive.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/apps/mobile/ios/spacedrive.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/mobile/metro.config.js b/apps/mobile/metro.config.js new file mode 100644 index 000000000..286802d1b --- /dev/null +++ b/apps/mobile/metro.config.js @@ -0,0 +1,61 @@ +const { makeMetroConfig, resolveUniqueModule, exclusionList } = require('@rnx-kit/metro-config'); +const MetroSymlinksResolver = require('@rnx-kit/metro-resolver-symlinks'); + +// Might not need these anymore. +const [SDAssetsPath, SDAssetsPathExclude] = resolveUniqueModule('@sd/assets', '.'); +const [babelRuntimePath, babelRuntimeExclude] = resolveUniqueModule('@babel/runtime'); +const [reactPath, reactExclude] = resolveUniqueModule('react'); + +// Needed for transforming svgs from @sd/assets +const [reactSVGPath, reactSVGExclude] = resolveUniqueModule('react-native-svg'); + +const { getDefaultConfig } = require('expo/metro-config'); +const expoDefaultConfig = getDefaultConfig(__dirname); + +const metroConfig = makeMetroConfig({ + projectRoot: __dirname, + resolver: { + ...expoDefaultConfig.resolver, + resolveRequest: MetroSymlinksResolver(), + extraNodeModules: { + '@babel/runtime': babelRuntimePath, + '@sd/assets': SDAssetsPath, + 'react': reactPath, + 'react-native-svg': reactSVGPath + }, + + blockList: exclusionList([ + babelRuntimeExclude, + SDAssetsPathExclude, + reactExclude, + reactSVGExclude + ]), + sourceExts: [...expoDefaultConfig.resolver.sourceExts, 'svg'], + assetExts: expoDefaultConfig.resolver.assetExts.filter((ext) => ext !== 'svg') + }, + transformer: { + // Metro default is "uglify-es" but terser should be faster and has better defaults. + minifierPath: 'metro-minify-terser', + minifierConfig: { + compress: { + drop_console: true, + // Sometimes improves performance? + reduce_funcs: false + }, + format: { + ascii_only: true, + wrap_iife: true, + quote_style: 3 + } + }, + getTransformOptions: async () => ({ + transform: { + experimentalImportSupport: false, + inlineRequires: true + } + }), + babelTransformerPath: require.resolve('react-native-svg-transformer') + } +}); + +module.exports = metroConfig; diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 3d4df4bf4..711926bd7 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -1,6 +1,66 @@ { "name": "mobile", - "version": "0.0.0", + "version": "1.0.0", "main": "index.js", - "license": "GPL-3.0-only" + "license": "GPL-3.0-only", + "scripts": { + "start": "expo start --dev-client", + "android": "expo run:android", + "ios": "expo run:ios", + "lint": "eslint src/**/*.{ts,tsx} && tsc --noEmit" + }, + "dependencies": { + "@expo/vector-icons": "^13.0.0", + "@gorhom/bottom-sheet": "^4.4.3", + "@react-native-async-storage/async-storage": "~1.17.3", + "@react-native-masked-view/masked-view": "0.2.7", + "@react-navigation/bottom-tabs": "^6.3.3", + "@react-navigation/drawer": "^6.4.4", + "@react-navigation/native": "^6.0.12", + "@react-navigation/stack": "^6.2.3", + "@rspc/client": "^0.0.5", + "@sd/assets": "file:../../packages/assets", + "@tanstack/react-query": "^4.2.3", + "byte-size": "^8.1.0", + "class-variance-authority": "^0.2.3", + "date-fns": "^2.29.2", + "expo": "~46.0.9", + "expo-font": "~10.2.0", + "expo-linking": "~3.2.2", + "expo-splash-screen": "~0.16.2", + "expo-status-bar": "~1.4.0", + "intl": "^1.2.5", + "moti": "^0.18.0", + "phosphor-react-native": "^1.1.2", + "react": "18.0.0", + "react-native": "0.69.4", + "react-native-gesture-handler": "~2.5.0", + "react-native-heroicons": "^2.2.0", + "react-native-reanimated": "~2.9.1", + "react-native-safe-area-context": "4.3.1", + "react-native-screens": "~3.15.0", + "react-native-svg": "13.0.0", + "twrnc": "^3.4.0", + "use-count-up": "^3.0.1", + "zustand": "^4.1.1" + }, + "devDependencies": { + "@babel/core": "^7.18.6", + "@babel/runtime": "^7.18.9", + "@rnx-kit/metro-config": "^1.2.36", + "@rnx-kit/metro-resolver-symlinks": "^0.1.21", + "@types/react": "~18.0.0", + "@types/react-native": "~0.69.1", + "@typescript-eslint/eslint-plugin": "^5.30.7", + "@typescript-eslint/parser": "^5.30.7", + "eslint": "^8.21.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-react": "^7.30.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-native": "^4.0.0", + "metro-minify-terser": "^0.72.1", + "react-native-svg-transformer": "^1.0.0", + "typescript": "^4.7.4" + }, + "private": true } diff --git a/apps/mobile/pnpm-lock.yaml b/apps/mobile/pnpm-lock.yaml new file mode 100644 index 000000000..75295d7a7 Binary files /dev/null and b/apps/mobile/pnpm-lock.yaml differ diff --git a/docs/architecture/rust-typescript-messaging.md b/apps/mobile/pnpm-workspace.yaml similarity index 100% rename from docs/architecture/rust-typescript-messaging.md rename to apps/mobile/pnpm-workspace.yaml diff --git a/apps/mobile/rust/Cargo.toml b/apps/mobile/rust/Cargo.toml new file mode 100644 index 000000000..d2140b3c6 --- /dev/null +++ b/apps/mobile/rust/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "sdcore-lib" +version = "0.1.0" +edition = "2021" +rust-version = "1.63.0" + +[lib] +name = "sdcore" +crate-type = ["staticlib", "cdylib"] # staticlib for IOS and cdylib for Android + +[dependencies] +once_cell = "1.13.0" +sdcore = { path = "../../../core", features = ["mobile", "p2p"], default-features = false } +rspc = { version = "0.0.4", features = [] } +serde_json = "1.0.83" +tokio = "1.20.1" +openssl = { version = "0.10.41", features = ["vendored"] } # Override features of transitive dependencies +openssl-sys = { version = "0.9.75", features = ["vendored"] } # Override features of transitive dependencies to support IOS Simulator on M1 + +[target.'cfg(target_os = "ios")'.dependencies] +objc = "0.2.7" +objc_id = "0.1.1" +objc-foundation = "0.1.1" + +# This is `not(ios)` instead of `android` because of https://github.com/mozilla/rust-android-gradle/issues/93 +[target.'cfg(not(target_os = "ios"))'.dependencies] +jni = "0.19.0" diff --git a/apps/mobile/rust/src/android.rs b/apps/mobile/rust/src/android.rs new file mode 100644 index 000000000..3f51a5d49 --- /dev/null +++ b/apps/mobile/rust/src/android.rs @@ -0,0 +1,111 @@ +use crate::{CLIENT_CONTEXT, EVENT_SENDER, NODE, RUNTIME}; +use jni::objects::{JClass, JObject, JString}; +use jni::JNIEnv; +use rspc::Request; +use sdcore::Node; +use tokio::sync::mpsc::unbounded_channel; + +#[no_mangle] +pub extern "system" fn Java_com_spacedrive_app_SDCore_registerCoreEventListener( + env: JNIEnv, + class: JClass, +) { + let jvm = env.get_java_vm().unwrap(); + let class = env.new_global_ref(class).unwrap(); + let (tx, mut rx) = unbounded_channel(); + let _ = EVENT_SENDER.set(tx); + + RUNTIME.spawn(async move { + while let Some(event) = rx.recv().await { + let data = match serde_json::to_string(&event) { + Ok(json) => json, + Err(err) => { + println!("Failed to serialize event: {}", err); + continue; + }, + }; + + let env = jvm.attach_current_thread().unwrap(); + env.call_method( + &class, + "sendCoreEvent", + "(Ljava/lang/String;)V", + &[env + .new_string(data) + .expect("Couldn't create java string!") + .into()], + ) + .unwrap(); + } + }); +} + +#[no_mangle] +pub extern "system" fn Java_com_spacedrive_app_SDCore_handleCoreMsg( + env: JNIEnv, + class: JClass, + query: JString, + callback: JObject, +) { + let jvm = env.get_java_vm().unwrap(); + let query: String = env + .get_string(query) + .expect("Couldn't get java string!") + .into(); + let class = env.new_global_ref(class).unwrap(); + let callback = env.new_global_ref(callback).unwrap(); + + RUNTIME.spawn(async move { + let request: Request = serde_json::from_str(&query).unwrap(); + + let node = &mut *NODE.lock().await; + let (node, router) = match node { + Some(node) => node.clone(), + None => { + let data_dir: String = { + let env = jvm.attach_current_thread().unwrap(); + let data_dir = env + .call_method( + &class, + "getDataDirectory", + "()Ljava/lang/String;", + &[], + ) + .unwrap() + .l() + .unwrap(); + + env.get_string(data_dir.into()).unwrap().into() + }; + + let new_node = Node::new(data_dir).await; + node.replace(new_node.clone()); + new_node + }, + }; + + let resp = serde_json::to_string( + &request + .handle( + node.get_request_context(), + &router, + &CLIENT_CONTEXT, + EVENT_SENDER.get(), + ) + .await, + ) + .unwrap(); + + let env = jvm.attach_current_thread().unwrap(); + env.call_method( + &callback, + "resolve", + "(Ljava/lang/Object;)V", + &[env + .new_string(resp) + .expect("Couldn't create java string!") + .into()], + ) + .unwrap(); + }); +} diff --git a/apps/mobile/rust/src/ios.rs b/apps/mobile/rust/src/ios.rs new file mode 100644 index 000000000..9abcc7c87 --- /dev/null +++ b/apps/mobile/rust/src/ios.rs @@ -0,0 +1,96 @@ +use crate::{CLIENT_CONTEXT, EVENT_SENDER, NODE, RUNTIME}; +use std::{ + ffi::{CStr, CString}, + os::raw::{c_char, c_void}, +}; +use tokio::sync::mpsc::unbounded_channel; + +use objc::{class, msg_send, runtime::Object, sel, sel_impl}; +use objc_foundation::{INSString, NSString}; +use objc_id::Id; +use rspc::Request; +use sdcore::Node; + +extern "C" { + fn get_data_directory() -> *const c_char; + fn call_resolve(resolve: *const c_void, result: *const c_char); +} + +// This struct wraps the function pointer which represent a Javascript Promise. We wrap the +// function pointers in a struct so we can unsafely assert to Rust that they are `Send`. +// We know they are send as we have ensured Objective-C won't deallocate the function pointer +// until `call_resolve` is called. +struct RNPromise(*const c_void); + +unsafe impl Send for RNPromise {} + +impl RNPromise { + // resolve the promise + unsafe fn resolve(self, result: CString) { + call_resolve(self.0, result.as_ptr()); + } +} + +#[no_mangle] +pub unsafe extern "C" fn register_core_event_listener(id: *mut Object) { + let id = Id::::from_ptr(id); + + let (tx, mut rx) = unbounded_channel(); + let _ = EVENT_SENDER.set(tx); + + RUNTIME.spawn(async move { + while let Some(event) = rx.recv().await { + let data = match serde_json::to_string(&event) { + Ok(json) => json, + Err(err) => { + println!("Failed to serialize event: {}", err); + continue; + }, + }; + let data = NSString::from_str(&data); + let _: () = msg_send![id, sendCoreEvent: data]; + } + }); +} + +#[no_mangle] +pub unsafe extern "C" fn sd_core_msg(query: *const c_char, resolve: *const c_void) { + // This string is cloned to the Rust heap. This is important as Objective-C may remove the query once this function completions but prior to the async block finishing. + let query = CStr::from_ptr(query).to_str().unwrap().to_string(); + + let resolve = RNPromise(resolve); + RUNTIME.spawn(async move { + let request: Request = serde_json::from_str(&query).unwrap(); + + let node = &mut *NODE.lock().await; + let (node, router) = match node { + Some(node) => node.clone(), + None => { + let doc_dir = CStr::from_ptr(get_data_directory()) + .to_str() + .unwrap() + .to_string(); + let new_node = Node::new(doc_dir).await; + node.replace(new_node.clone()); + new_node + }, + }; + + resolve.resolve( + CString::new( + serde_json::to_vec( + &request + .handle( + node.get_request_context(), + &router, + &CLIENT_CONTEXT, + EVENT_SENDER.get(), + ) + .await, + ) + .unwrap(), + ) + .unwrap(), + ) + }); +} diff --git a/apps/mobile/rust/src/lib.rs b/apps/mobile/rust/src/lib.rs new file mode 100644 index 000000000..10c476149 --- /dev/null +++ b/apps/mobile/rust/src/lib.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; + +use once_cell::sync::{Lazy, OnceCell}; +use rspc::{ClientContext, Response}; +use sdcore::{api::Router, Node}; +use tokio::{ + runtime::Runtime, + sync::{mpsc::UnboundedSender, Mutex}, +}; + +#[allow(dead_code)] +pub(crate) static RUNTIME: Lazy = Lazy::new(|| Runtime::new().unwrap()); + +#[allow(dead_code)] +pub(crate) static NODE: Lazy, Arc)>>> = + Lazy::new(|| Mutex::new(None)); + +#[allow(dead_code)] +pub(crate) static CLIENT_CONTEXT: Lazy = Lazy::new(|| ClientContext { + subscriptions: Default::default(), +}); + +#[allow(dead_code)] +pub(crate) static EVENT_SENDER: OnceCell> = OnceCell::new(); + +#[cfg(target_os = "ios")] +mod ios; + +/// This is `not(ios)` instead of `android` because of https://github.com/mozilla/rust-android-gradle/issues/93 +#[cfg(not(target_os = "ios"))] +mod android; diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx new file mode 100644 index 000000000..14794ffd7 --- /dev/null +++ b/apps/mobile/src/App.tsx @@ -0,0 +1,75 @@ +import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; +import { DefaultTheme, NavigationContainer, Theme } from '@react-navigation/native'; +import { createClient } from '@rspc/client'; +import { StatusBar } from 'expo-status-bar'; +import React, { useEffect } from 'react'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { useDeviceContext } from 'twrnc'; + +import { GlobalModals } from './components/modals/GlobalModals'; +import { ReactNativeTransport, queryClient, rspc, useInvalidateQuery } from './hooks/rspc'; +import useCachedResources from './hooks/useCachedResources'; +import { getItemFromStorage } from './lib/storage'; +import tw from './lib/tailwind'; +import RootNavigator from './navigation'; +import OnboardingNavigator from './navigation/OnboardingNavigator'; +import { useOnboardingStore } from './stores/useOnboardingStore'; +import type { Operations } from './types/bindings'; + +const client = createClient({ + transport: new ReactNativeTransport() +}); + +const NavigatorTheme: Theme = { + ...DefaultTheme, + colors: { + ...DefaultTheme.colors, + background: '#08090D' + } +}; + +export default function App() { + // Enables dark mode, and screen size breakpoints, etc. for tailwind + useDeviceContext(tw, { withDeviceColorScheme: false }); + + const isLoadingComplete = useCachedResources(); + + const { showOnboarding, hideOnboarding } = useOnboardingStore(); + + // Runs when the app is launched + useEffect(() => { + getItemFromStorage('@onboarding').then((value) => { + value && hideOnboarding(); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (!isLoadingComplete) { + return null; + } else { + return ( + + <> + + + + + + + {showOnboarding ? : } + + + + + + + + ); + } +} + +function InvalidateQuery() { + useInvalidateQuery(); + return null; +} diff --git a/apps/mobile/src/assets/icons/file/index.ts b/apps/mobile/src/assets/icons/file/index.ts new file mode 100644 index 000000000..c67268de8 --- /dev/null +++ b/apps/mobile/src/assets/icons/file/index.ts @@ -0,0 +1,347 @@ +import ai from '@sd/assets/icons/ai.svg'; +import angular from '@sd/assets/icons/angular.svg'; +import audiomp3 from '@sd/assets/icons/audio-mp3.svg'; +import audioogg from '@sd/assets/icons/audio-ogg.svg'; +import audiowav from '@sd/assets/icons/audio-wav.svg'; +import audio from '@sd/assets/icons/audio.svg'; +import babel from '@sd/assets/icons/babel.svg'; +import bat from '@sd/assets/icons/bat.svg'; +import bicep from '@sd/assets/icons/bicep.svg'; +import binary from '@sd/assets/icons/binary.svg'; +import blade from '@sd/assets/icons/blade.svg'; +import browserslist from '@sd/assets/icons/browserslist.svg'; +import bsconfig from '@sd/assets/icons/bsconfig.svg'; +import bundler from '@sd/assets/icons/bundler.svg'; +import c from '@sd/assets/icons/c.svg'; +import cert from '@sd/assets/icons/cert.svg'; +import cheader from '@sd/assets/icons/cheader.svg'; +import cli from '@sd/assets/icons/cli.svg'; +import compodoc from '@sd/assets/icons/compodoc.svg'; +import composer from '@sd/assets/icons/composer.svg'; +import conf from '@sd/assets/icons/conf.svg'; +import cpp from '@sd/assets/icons/cpp.svg'; +import csharp from '@sd/assets/icons/csharp.svg'; +import cshtml from '@sd/assets/icons/cshtml.svg'; +import cssmap from '@sd/assets/icons/css-map.svg'; +import css from '@sd/assets/icons/css.svg'; +import csv from '@sd/assets/icons/csv.svg'; +import dartlang from '@sd/assets/icons/dartlang.svg'; +import dockerdebug from '@sd/assets/icons/docker-debug.svg'; +import dockerignore from '@sd/assets/icons/docker-ignore.svg'; +import docker from '@sd/assets/icons/docker.svg'; +import editorconfig from '@sd/assets/icons/editorconfig.svg'; +import eex from '@sd/assets/icons/eex.svg'; +import elixir from '@sd/assets/icons/elixir.svg'; +import elm from '@sd/assets/icons/elm.svg'; +import env from '@sd/assets/icons/env.svg'; +import erb from '@sd/assets/icons/erb.svg'; +import erlang from '@sd/assets/icons/erlang.svg'; +import eslint from '@sd/assets/icons/eslint.svg'; +import exs from '@sd/assets/icons/exs.svg'; +import exx from '@sd/assets/icons/exx.svg'; +import file from '@sd/assets/icons/file.svg'; +import folderlight from '@sd/assets/icons/folder-light.svg'; +import folderopen from '@sd/assets/icons/folder-open.svg'; +import folder from '@sd/assets/icons/folder.svg'; +import fontotf from '@sd/assets/icons/font-otf.svg'; +import fontttf from '@sd/assets/icons/font-ttf.svg'; +import fontwoff2 from '@sd/assets/icons/font-woff2.svg'; +import fontwoff from '@sd/assets/icons/font-woff.svg'; +import git from '@sd/assets/icons/git.svg'; +import gopackage from '@sd/assets/icons/go-package.svg'; +import go from '@sd/assets/icons/go.svg'; +import gradle from '@sd/assets/icons/gradle.svg'; +import graphql from '@sd/assets/icons/graphql.svg'; +import groovy from '@sd/assets/icons/groovy.svg'; +import grunt from '@sd/assets/icons/grunt.svg'; +import gulp from '@sd/assets/icons/gulp.svg'; +import haml from '@sd/assets/icons/haml.svg'; +import handlebars from '@sd/assets/icons/handlebars.svg'; +import haskell from '@sd/assets/icons/haskell.svg'; +import html from '@sd/assets/icons/html.svg'; +import imagegif from '@sd/assets/icons/image-gif.svg'; +import imageico from '@sd/assets/icons/image-ico.svg'; +import imagejpg from '@sd/assets/icons/image-jpg.svg'; +import imagepng from '@sd/assets/icons/image-png.svg'; +import imagewebp from '@sd/assets/icons/image-webp.svg'; +import image from '@sd/assets/icons/image.svg'; +import info from '@sd/assets/icons/info.svg'; +import ipynb from '@sd/assets/icons/ipynb.svg'; +import java from '@sd/assets/icons/java.svg'; +import jenkins from '@sd/assets/icons/jenkins.svg'; +import jest from '@sd/assets/icons/jest.svg'; +import jinja from '@sd/assets/icons/jinja.svg'; +import jsmap from '@sd/assets/icons/js-map.svg'; +import js from '@sd/assets/icons/js.svg'; +import json from '@sd/assets/icons/json.svg'; +import jsp from '@sd/assets/icons/jsp.svg'; +import julia from '@sd/assets/icons/julia.svg'; +import karma from '@sd/assets/icons/karma.svg'; +import key from '@sd/assets/icons/key.svg'; +import less from '@sd/assets/icons/less.svg'; +import license from '@sd/assets/icons/license.svg'; +import lighteditorconfig from '@sd/assets/icons/lighteditorconfig.svg'; +import liquid from '@sd/assets/icons/liquid.svg'; +import llvm from '@sd/assets/icons/llvm.svg'; +import log from '@sd/assets/icons/log.svg'; +import lua from '@sd/assets/icons/lua.svg'; +import m from '@sd/assets/icons/m.svg'; +import markdown from '@sd/assets/icons/markdown.svg'; +import mint from '@sd/assets/icons/mint.svg'; +import mov from '@sd/assets/icons/mov.svg'; +import mp4 from '@sd/assets/icons/mp4.svg'; +import nestjscontroller from '@sd/assets/icons/nestjs-controller.svg'; +import nestjsdecorator from '@sd/assets/icons/nestjs-decorator.svg'; +import nestjsfilter from '@sd/assets/icons/nestjs-filter.svg'; +import nestjsguard from '@sd/assets/icons/nestjs-guard.svg'; +import nestjsmodule from '@sd/assets/icons/nestjs-module.svg'; +import nestjsservice from '@sd/assets/icons/nestjs-service.svg'; +import nestjs from '@sd/assets/icons/nestjs.svg'; +import netlify from '@sd/assets/icons/netlify.svg'; +import nginx from '@sd/assets/icons/nginx.svg'; +import nim from '@sd/assets/icons/nim.svg'; +import njk from '@sd/assets/icons/njk.svg'; +import nodemon from '@sd/assets/icons/nodemon.svg'; +import npmlock from '@sd/assets/icons/npm-lock.svg'; +import npm from '@sd/assets/icons/npm.svg'; +import nuxt from '@sd/assets/icons/nuxt.svg'; +import nvm from '@sd/assets/icons/nvm.svg'; +import opengl from '@sd/assets/icons/opengl.svg'; +import pdf from '@sd/assets/icons/pdf.svg'; +import photoshop from '@sd/assets/icons/photoshop.svg'; +import php from '@sd/assets/icons/php.svg'; +import postcssconfig from '@sd/assets/icons/postcss-config.svg'; +import powershelldata from '@sd/assets/icons/powershell-data.svg'; +import powershellmodule from '@sd/assets/icons/powershell-module.svg'; +import powershell from '@sd/assets/icons/powershell.svg'; +import prettier from '@sd/assets/icons/prettier.svg'; +import prisma from '@sd/assets/icons/prisma.svg'; +import prolog from '@sd/assets/icons/prolog.svg'; +import pug from '@sd/assets/icons/pug.svg'; +import python from '@sd/assets/icons/python.svg'; +import qt from '@sd/assets/icons/qt.svg'; +import razor from '@sd/assets/icons/razor.svg'; +import reactjs from '@sd/assets/icons/react-js.svg'; +import reactts from '@sd/assets/icons/react-ts.svg'; +import readme from '@sd/assets/icons/readme.svg'; +import rescript from '@sd/assets/icons/rescript.svg'; +import rjson from '@sd/assets/icons/rjson.svg'; +import robots from '@sd/assets/icons/robots.svg'; +import rollup from '@sd/assets/icons/rollup.svg'; +import ruby from '@sd/assets/icons/ruby.svg'; +import rust from '@sd/assets/icons/rust.svg'; +import sass from '@sd/assets/icons/sass.svg'; +import scss from '@sd/assets/icons/scss.svg'; +import shell from '@sd/assets/icons/shell.svg'; +import smarty from '@sd/assets/icons/smarty.svg'; +import sol from '@sd/assets/icons/sol.svg'; +import sql from '@sd/assets/icons/sql.svg'; +import storybook from '@sd/assets/icons/storybook.svg'; +import stylelint from '@sd/assets/icons/stylelint.svg'; +import stylus from '@sd/assets/icons/stylus.svg'; +import svelte from '@sd/assets/icons/svelte.svg'; +import svg from '@sd/assets/icons/svg.svg'; +import swift from '@sd/assets/icons/swift.svg'; +import symfony from '@sd/assets/icons/symfony.svg'; +import tailwind from '@sd/assets/icons/tailwind.svg'; +import testjs from '@sd/assets/icons/test-js.svg'; +import testts from '@sd/assets/icons/test-ts.svg'; +import tmpl from '@sd/assets/icons/tmpl.svg'; +import toml from '@sd/assets/icons/toml.svg'; +import travis from '@sd/assets/icons/travis.svg'; +import tsconfig from '@sd/assets/icons/tsconfig.svg'; +import tsx from '@sd/assets/icons/tsx.svg'; +import twig from '@sd/assets/icons/twig.svg'; +import txt from '@sd/assets/icons/txt.svg'; +import typescriptdef from '@sd/assets/icons/typescript-def.svg'; +import typescript from '@sd/assets/icons/typescript.svg'; +import ui from '@sd/assets/icons/ui.svg'; +import user from '@sd/assets/icons/user.svg'; +import vercel from '@sd/assets/icons/vercel.svg'; +import video from '@sd/assets/icons/video.svg'; +import vite from '@sd/assets/icons/vite.svg'; +import vscode from '@sd/assets/icons/vscode.svg'; +import vue from '@sd/assets/icons/vue.svg'; +import wasm from '@sd/assets/icons/wasm.svg'; +import webpack from '@sd/assets/icons/webpack.svg'; +import windi from '@sd/assets/icons/windi.svg'; +import xml from '@sd/assets/icons/xml.svg'; +import yaml from '@sd/assets/icons/yaml.svg'; +import yarnerror from '@sd/assets/icons/yarn-error.svg'; +import yarn from '@sd/assets/icons/yarn.svg'; +import zip from '@sd/assets/icons/zip.svg'; + +export default { + ai, + angular, + audiomp3, + audioogg, + audiowav, + audio, + babel, + bat, + bicep, + binary, + blade, + browserslist, + bsconfig, + bundler, + c, + cert, + cheader, + cli, + compodoc, + composer, + conf, + cpp, + csharp, + cshtml, + cssmap, + css, + csv, + dartlang, + dockerdebug, + dockerignore, + docker, + editorconfig, + eex, + elixir, + elm, + env, + erb, + erlang, + eslint, + exs, + exx, + file, + folderlight, + folderopen, + folder, + fontotf, + fontttf, + fontwoff, + fontwoff2, + git, + gopackage, + go, + gradle, + graphql, + groovy, + grunt, + gulp, + haml, + handlebars, + haskell, + html, + imagegif, + imageico, + imagejpg, + imagepng, + imagewebp, + image, + info, + ipynb, + java, + jenkins, + jest, + jinja, + jsmap, + js, + json, + jsp, + julia, + karma, + key, + less, + license, + lighteditorconfig, + liquid, + llvm, + log, + lua, + m, + markdown, + mint, + mov, + mp4, + nestjscontroller, + nestjsdecorator, + nestjsfilter, + nestjsguard, + nestjsmodule, + nestjsservice, + nestjs, + netlify, + nginx, + nim, + njk, + nodemon, + npmlock, + npm, + nuxt, + nvm, + opengl, + pdf, + photoshop, + php, + postcssconfig, + powershelldata, + powershellmodule, + powershell, + prettier, + prisma, + prolog, + pug, + python, + qt, + razor, + reactjs, + reactts, + readme, + rescript, + rjson, + robots, + rollup, + ruby, + rust, + sass, + scss, + shell, + smarty, + sol, + sql, + storybook, + stylelint, + stylus, + svelte, + svg, + swift, + symfony, + tailwind, + testjs, + testts, + tmpl, + toml, + travis, + tsconfig, + tsx, + twig, + txt, + typescriptdef, + typescript, + ui, + user, + vercel, + video, + vite, + vscode, + vue, + wasm, + webpack, + windi, + xml, + yaml, + yarnerror, + yarn, + zip +}; diff --git a/apps/mobile/src/assets/temp/README.md b/apps/mobile/src/assets/temp/README.md new file mode 100644 index 000000000..f639a914b --- /dev/null +++ b/apps/mobile/src/assets/temp/README.md @@ -0,0 +1,3 @@ +Anything here is copied from some other app/package and should be moved to `assets` package for consistency. + +- Make sure sure to fix imports throughout the monorepo when you do such action. diff --git a/apps/mobile/src/components/animation/layout.tsx b/apps/mobile/src/components/animation/layout.tsx new file mode 100644 index 000000000..78253f6c4 --- /dev/null +++ b/apps/mobile/src/components/animation/layout.tsx @@ -0,0 +1,101 @@ +import { MotiView, useDynamicAnimation } from 'moti'; +import React from 'react'; +import { StyleSheet, View } from 'react-native'; +import { useDerivedValue, useSharedValue } from 'react-native-reanimated'; + +import Layout from '../../constants/Layout'; +import tw from '../../lib/tailwind'; + +// Anything wrapped with FadeIn will fade in on mount. +export const FadeInAnimation = ({ children, delay }: { children: any; delay?: number }) => ( + + {children} + +); + +export const FadeInUpAnimation = ({ children, delay }: { children: any; delay?: number }) => ( + + {children} + +); + +export const LogoAnimation = ({ children }: { children: any }) => ( + + {children} + +); + +type AnimatedHeightProps = { + children?: React.ReactNode; + /** + * If `true`, the height will automatically animate to 0. Default: `false`. + */ + hide?: boolean; + onHeightDidAnimate?: (height: number) => void; + initialHeight?: number; +} & React.ComponentProps; + +export function AnimatedHeight({ + children, + hide = false, + style, + delay = 0, + transition = { type: 'timing', delay }, + onHeightDidAnimate, + initialHeight = 0, + ...motiViewProps +}: AnimatedHeightProps) { + const measuredHeight = useSharedValue(initialHeight); + const state = useDynamicAnimation(() => { + return { + height: initialHeight, + opacity: !initialHeight || hide ? 0 : 1 + }; + }); + if ('state' in motiViewProps) { + console.warn('[AnimateHeight] state prop not supported'); + } + + useDerivedValue(() => { + let height = Math.ceil(measuredHeight.value); + if (hide) { + height = 0; + } + + state.animateTo({ + height, + opacity: !height || hide ? 0 : 1 + }); + }, [hide, measuredHeight]); + + return ( + + key === 'height' && onHeightDidAnimate(attemptedValue as number)) + } + style={[tw`overflow-hidden`, style]} + > + { + measuredHeight.value = nativeEvent.layout.height; + }} + > + {children} + + + ); +} diff --git a/apps/mobile/src/components/base/Button.tsx b/apps/mobile/src/components/base/Button.tsx new file mode 100644 index 000000000..3cc76eda4 --- /dev/null +++ b/apps/mobile/src/components/base/Button.tsx @@ -0,0 +1,58 @@ +import { VariantProps, cva } from 'class-variance-authority'; +import { MotiPressable, MotiPressableProps } from 'moti/interactions'; +import React, { useMemo } from 'react'; +import { Pressable, PressableProps } from 'react-native'; + +import tw from '../../lib/tailwind'; + +const button = cva(['border rounded-md items-center shadow-sm'], { + variants: { + variant: { + default: 'bg-gray-50 border-gray-100', + primary: ['bg-primary-600'], + gray: ['bg-gray-100 border-gray-200'] + }, + size: { + default: ['py-1', 'px-3'], + sm: ['py-1', 'px-2'], + lg: ['py-2', 'px-4'] + } + }, + defaultVariants: { + variant: 'default', + size: 'default' + } +}); + +type ButtonProps = VariantProps & PressableProps; + +export const Button: React.FC = ({ variant, size, ...props }) => { + const { style, ...otherProps } = props; + return ; +}; + +type AnimatedButtonProps = VariantProps & MotiPressableProps; + +export const AnimatedButton: React.FC = ({ variant, size, ...props }) => { + const { style, containerStyle, ...otherProps } = props; + return ( + + ({ hovered, pressed }) => { + 'worklet'; + + return { + opacity: hovered || pressed ? 0.7 : 1, + scale: hovered || pressed ? 0.97 : 1 + }; + }, + [] + )} + style={tw.style(button({ variant, size }), style as string)} + // MotiPressable acts differently than Pressable so containerStyle might need to used to achieve the same effect + containerStyle={containerStyle} + {...otherProps} + /> + ); +}; diff --git a/apps/mobile/src/components/base/Divider.tsx b/apps/mobile/src/components/base/Divider.tsx new file mode 100644 index 000000000..f936c2884 --- /dev/null +++ b/apps/mobile/src/components/base/Divider.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { StyleProp, Text, View, ViewStyle } from 'react-native'; + +import tw from '../../lib/tailwind'; + +type DividerProps = { + style?: StyleProp; +}; + +const Divider = ({ style }: DividerProps) => { + return ; +}; + +export default Divider; diff --git a/apps/mobile/src/components/browse/BrowseLocationItem.tsx b/apps/mobile/src/components/browse/BrowseLocationItem.tsx new file mode 100644 index 000000000..2298925c9 --- /dev/null +++ b/apps/mobile/src/components/browse/BrowseLocationItem.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Pressable, Text, View } from 'react-native'; + +import tw from '../../lib/tailwind'; +import FolderIcon from '../icons/FolderIcon'; + +interface BrowseLocationItemProps { + folderName: string; + onPress: () => void; +} + +const BrowseLocationItem: React.FC = (props) => { + const { folderName, onPress } = props; + + return ( + + + + + {folderName} + + + + ); +}; + +export default BrowseLocationItem; diff --git a/apps/mobile/src/components/browse/BrowseTagItem.tsx b/apps/mobile/src/components/browse/BrowseTagItem.tsx new file mode 100644 index 000000000..2f8508008 --- /dev/null +++ b/apps/mobile/src/components/browse/BrowseTagItem.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { ColorValue, Pressable, Text, View } from 'react-native'; + +import tw from '../../lib/tailwind'; + +type BrowseTagItemProps = { + tagName: string; + tagColor: ColorValue; + onPress: () => void; +}; + +const BrowseTagItem: React.FC = (props) => { + const { tagName, tagColor, onPress } = props; + return ( + + + + + {tagName} + + + + ); +}; + +export default BrowseTagItem; diff --git a/apps/mobile/src/components/device/Device.tsx b/apps/mobile/src/components/device/Device.tsx new file mode 100644 index 000000000..d8a24afad --- /dev/null +++ b/apps/mobile/src/components/device/Device.tsx @@ -0,0 +1,310 @@ +import { Cloud, Desktop, DeviceMobileCamera, Laptop } from 'phosphor-react-native'; +import React from 'react'; +import { FlatList, Text, View } from 'react-native'; +import { LockClosedIcon } from 'react-native-heroicons/solid'; + +import tw from '../../lib/tailwind'; +import { FilePath } from '../../types/bindings'; +import FileItem from '../file/FileItem'; + +const placeholderFileItems: FilePath[] = [ + { + is_dir: true, + date_created: '2020-01-01T00:00:00.000Z', + date_indexed: '2020-01-01T00:00:00.000Z', + date_modified: '2020-01-01T00:00:00.000Z', + extension: '', + file_id: 1, + id: 1, + key: null, + location_id: 1, + materialized_path: '', + name: 'Minecraft', + parent_id: 0, + key_id: null, + location: null, + file: { + id: 1, + key_id: 1, + albums: [], + comments: [], + key: { + algorithm: null, + checksum: '', + date_created: null, + file_paths: [], + files: [], + id: 1, + name: 'Hello world' + }, + labels: [], + media_data: null, + spaces: [], + tags: [], + cas_id: '', + ipfs_id: '', + has_thumbnail: false, + favorite: false, + has_thumbstrip: false, + has_video_preview: false, + hidden: false, + important: false, + integrity_checksum: '', + kind: 1, + note: '', + paths: [], + size_in_bytes: '555', + date_created: '', + date_indexed: '', + date_modified: '' + } + }, + { + is_dir: true, + date_created: '2020-01-01T00:00:00.000Z', + date_indexed: '2020-01-01T00:00:00.000Z', + date_modified: '2020-01-01T00:00:00.000Z', + extension: '', + file_id: 2, + id: 2, + key: null, + location_id: 2, + materialized_path: '', + name: 'Documents', + parent_id: 0, + key_id: null, + location: null, + file: { + id: 2, + key_id: 2, + albums: [], + comments: [], + key: { + algorithm: null, + checksum: '', + date_created: null, + file_paths: [], + files: [], + id: 1, + name: 'Hello world' + }, + labels: [], + media_data: null, + spaces: [], + tags: [], + cas_id: '', + ipfs_id: '', + has_thumbnail: false, + favorite: false, + has_thumbstrip: false, + has_video_preview: false, + hidden: false, + important: false, + integrity_checksum: '', + kind: 1, + note: '', + paths: [], + size_in_bytes: '555', + date_created: '', + date_indexed: '', + date_modified: '' + } + }, + { + is_dir: false, + date_created: '2020-01-01T00:00:00.000Z', + date_indexed: '2020-01-01T00:00:00.000Z', + date_modified: '2020-01-01T00:00:00.000Z', + extension: 'tsx', + file_id: 3, + id: 3, + key: null, + location_id: 3, + materialized_path: '', + name: 'App.tsx', + parent_id: 0, + key_id: null, + location: null, + file: { + id: 3, + key_id: 3, + albums: [], + comments: [], + key: { + algorithm: null, + checksum: '', + date_created: null, + file_paths: [], + files: [], + id: 1, + name: 'Hello world' + }, + labels: [], + media_data: null, + spaces: [], + tags: [], + cas_id: '', + ipfs_id: '', + has_thumbnail: false, + favorite: false, + has_thumbstrip: false, + has_video_preview: false, + hidden: false, + important: false, + integrity_checksum: '', + kind: 1, + note: '', + paths: [], + size_in_bytes: '555', + date_created: '', + date_indexed: '', + date_modified: '' + } + }, + { + is_dir: false, + date_created: '2020-01-01T00:00:00.000Z', + date_indexed: '2020-01-01T00:00:00.000Z', + date_modified: '2020-01-01T00:00:00.000Z', + extension: 'vite', + file_id: 4, + id: 4, + key: null, + location_id: 4, + materialized_path: '', + name: 'vite.config.js', + parent_id: 0, + key_id: null, + location: null, + file: { + id: 4, + key_id: 4, + albums: [], + comments: [], + key: { + algorithm: null, + checksum: '', + date_created: null, + file_paths: [], + files: [], + id: 1, + name: 'Hello world' + }, + labels: [], + media_data: null, + spaces: [], + tags: [], + cas_id: '', + ipfs_id: '', + has_thumbnail: false, + favorite: false, + has_thumbstrip: false, + has_video_preview: false, + hidden: false, + important: false, + integrity_checksum: '', + kind: 1, + note: '', + paths: [], + size_in_bytes: '555', + date_created: '', + date_indexed: '', + date_modified: '' + } + }, + { + is_dir: false, + date_created: '2020-01-01T00:00:00.000Z', + date_indexed: '2020-01-01T00:00:00.000Z', + date_modified: '2020-01-01T00:00:00.000Z', + extension: 'docker', + file_id: 5, + id: 5, + key: null, + location_id: 5, + materialized_path: '', + name: 'Dockerfile', + parent_id: 0, + key_id: null, + location: null, + file: { + id: 5, + key_id: 5, + albums: [], + comments: [], + key: { + algorithm: null, + checksum: '', + date_created: null, + file_paths: [], + files: [], + id: 1, + name: 'Hello world' + }, + labels: [], + media_data: null, + spaces: [], + tags: [], + cas_id: '', + ipfs_id: '', + has_thumbnail: false, + favorite: false, + has_thumbstrip: false, + has_video_preview: false, + hidden: false, + important: false, + integrity_checksum: '', + kind: 1, + note: '', + paths: [], + size_in_bytes: '555', + date_created: '', + date_indexed: '', + date_modified: '' + } + } +]; + +export interface DeviceProps { + name: string; + size: string; + type: 'laptop' | 'desktop' | 'phone' | 'server'; + locations: { name: string; folder?: boolean; format?: string; icon?: string }[]; + runningJob?: { amount: number; task: string }; +} + +const Device = ({ name, locations, size, type }: DeviceProps) => { + return ( + + + + {type === 'phone' && ( + + )} + {type === 'laptop' && } + {type === 'desktop' && } + {type === 'server' && } + {name || 'Unnamed Device'} + {/* P2P Lock */} + + + P2P + + + {/* Size */} + {size} + + {/* Locations/Files TODO: Maybe use FlashList? */} + } + keyExtractor={(item) => item.id.toString()} + horizontal + contentContainerStyle={tw`mt-3 mb-5`} + showsHorizontalScrollIndicator={false} + /> + + ); +}; + +export default Device; diff --git a/apps/mobile/src/components/drawer/DrawerContent.tsx b/apps/mobile/src/components/drawer/DrawerContent.tsx new file mode 100644 index 000000000..0a727600b --- /dev/null +++ b/apps/mobile/src/components/drawer/DrawerContent.tsx @@ -0,0 +1,118 @@ +import { DrawerContentScrollView } from '@react-navigation/drawer'; +import { DrawerContentComponentProps } from '@react-navigation/drawer/lib/typescript/src/types'; +import { getFocusedRouteNameFromRoute } from '@react-navigation/native'; +import React from 'react'; +import { ColorValue, Platform, Pressable, Text, View } from 'react-native'; +import { CogIcon } from 'react-native-heroicons/solid'; + +import Layout from '../../constants/Layout'; +import tw from '../../lib/tailwind'; +import CollapsibleView from '../layout/CollapsibleView'; +import DrawerLocationItem from './DrawerLocationItem'; +import DrawerLogo from './DrawerLogo'; +import DrawerTagItem from './DrawerTagItem'; + +const placeholderLocationData = [ + { + id: 1, + name: 'Spacedrive' + }, + { + id: 2, + name: 'Content' + } +]; +const placeholderTagsData = [ + { + id: 1, + name: 'Funny', + color: tw.color('blue-500') + }, + { + id: 2, + name: 'Twitch', + color: tw.color('purple-500') + }, + { + id: 3, + name: 'BlackMagic', + color: tw.color('red-500') + } +]; + +const drawerHeight = Platform.select({ + ios: Layout.window.height * 0.85, + android: Layout.window.height * 0.9 +}); + +const getActiveRouteState = function (state: any) { + if (!state.routes || state.routes.length === 0 || state.index >= state.routes.length) { + return state; + } + const childActiveRoute = state.routes[state.index]; + return getActiveRouteState(childActiveRoute); +}; + +const DrawerContent = ({ navigation, state }: DrawerContentComponentProps) => { + const stackName = getFocusedRouteNameFromRoute(getActiveRouteState(state)) ?? 'OverviewStack'; + + return ( + + + + + TODO: Library Selection + {/* Locations */} + + {placeholderLocationData.map((location) => ( + + navigation.navigate(stackName, { + screen: 'Location', + params: { id: location.id } + }) + } + /> + ))} + {/* Add Location */} + + + Add Location + + + + {/* Tags */} + + {placeholderTagsData.map((tag) => ( + + navigation.navigate(stackName, { + screen: 'Tag', + params: { id: tag.id } + }) + } + tagColor={tag.color as ColorValue} + /> + ))} + + + {/* Settings */} + navigation.navigate('Settings')}> + + + + + ); +}; + +export default DrawerContent; diff --git a/apps/mobile/src/components/drawer/DrawerLocationItem.tsx b/apps/mobile/src/components/drawer/DrawerLocationItem.tsx new file mode 100644 index 000000000..a9f74ae74 --- /dev/null +++ b/apps/mobile/src/components/drawer/DrawerLocationItem.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Pressable, Text, View } from 'react-native'; + +import tw from '../../lib/tailwind'; +import FolderIcon from '../icons/FolderIcon'; + +interface DrawerLocationItemProps { + folderName: string; + onPress: () => void; +} + +const DrawerLocationItem: React.FC = (props) => { + const { folderName, onPress } = props; + return ( + + + + + {folderName} + + + + ); +}; + +export default DrawerLocationItem; diff --git a/apps/mobile/src/components/drawer/DrawerLogo.tsx b/apps/mobile/src/components/drawer/DrawerLogo.tsx new file mode 100644 index 000000000..afe1ce54c --- /dev/null +++ b/apps/mobile/src/components/drawer/DrawerLogo.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Image, Text, View } from 'react-native'; + +import tw from '../../lib/tailwind'; +import Divider from '../base/Divider'; + +const DrawerLogo = () => { + return ( + <> + + + Spacedrive + + + + ); +}; + +export default DrawerLogo; diff --git a/apps/mobile/src/components/drawer/DrawerTagItem.tsx b/apps/mobile/src/components/drawer/DrawerTagItem.tsx new file mode 100644 index 000000000..210b04876 --- /dev/null +++ b/apps/mobile/src/components/drawer/DrawerTagItem.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { ColorValue, Pressable, Text, View } from 'react-native'; + +import tw from '../../lib/tailwind'; + +type DrawerTagItemProps = { + tagName: string; + tagColor: ColorValue; + onPress: () => void; +}; + +const DrawerTagItem: React.FC = (props) => { + const { tagName, tagColor, onPress } = props; + return ( + + + + + {tagName} + + + + ); +}; + +export default DrawerTagItem; diff --git a/apps/mobile/src/components/file/FileIcon.tsx b/apps/mobile/src/components/file/FileIcon.tsx new file mode 100644 index 000000000..101f78dc9 --- /dev/null +++ b/apps/mobile/src/components/file/FileIcon.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { Text, View } from 'react-native'; +import Svg, { Path } from 'react-native-svg'; + +import icons from '../../assets/icons/file'; +import tw from '../../lib/tailwind'; +import { FilePath } from '../../types/bindings'; +import FolderIcon from '../icons/FolderIcon'; + +type FileIconProps = { + file?: FilePath | null; + /** + * This is multiplier for calculating icon size + * default: `1` + */ + size?: number; +}; + +const FileIcon = ({ file, size = 1 }: FileIconProps) => { + return ( + + {file?.is_dir ? ( + + + + ) : file?.file?.has_thumbnail ? ( + <>{/* TODO */} + ) : ( + + + + + + + + {/* File Icon & Extension */} + + {file?.extension && + icons[file.extension] && + (() => { + const Icon = icons[file.extension]; + return ; + })()} + + {file?.extension} + + + + )} + + ); +}; + +export default FileIcon; diff --git a/apps/mobile/src/components/file/FileItem.tsx b/apps/mobile/src/components/file/FileItem.tsx new file mode 100644 index 000000000..5bc5d8a03 --- /dev/null +++ b/apps/mobile/src/components/file/FileItem.tsx @@ -0,0 +1,49 @@ +import { useNavigation } from '@react-navigation/native'; +import React from 'react'; +import { Pressable, Text, View } from 'react-native'; + +import tw from '../../lib/tailwind'; +import { SharedScreenProps } from '../../navigation/SharedScreens'; +import { useFileModalStore } from '../../stores/useModalStore'; +import { FilePath } from '../../types/bindings'; +import FileIcon from './FileIcon'; + +type FileItemProps = { + file?: FilePath | null; +}; + +// TODO: Menu for file actions (File details, Share etc.) + +const FileItem = ({ file }: FileItemProps) => { + const fileRef = useFileModalStore((state) => state.fileRef); + const setData = useFileModalStore((state) => state.setData); + + const navigation = useNavigation['navigation']>(); + + function handlePress() { + if (!file) return; + + if (file.is_dir) { + navigation.navigate('Location', { id: file.location_id }); + } else { + setData(file); + fileRef.current.present(); + } + } + + return ( + + + {/* Folder Icons/Thumbnail etc. */} + + + + {file?.name} + + + + + ); +}; + +export default FileItem; diff --git a/apps/mobile/src/components/header/Header.tsx b/apps/mobile/src/components/header/Header.tsx new file mode 100644 index 000000000..8838ade88 --- /dev/null +++ b/apps/mobile/src/components/header/Header.tsx @@ -0,0 +1,41 @@ +import { useDrawerStatus } from '@react-navigation/drawer'; +import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript/src/types'; +import { useNavigation } from '@react-navigation/native'; +import { MotiView } from 'moti'; +import { List } from 'phosphor-react-native'; +import React from 'react'; +import { Pressable, Text, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import tw from '../../lib/tailwind'; + +const Header = () => { + const navigation = useNavigation(); + + const { top } = useSafeAreaInsets(); + + const isDrawerOpen = useDrawerStatus() === 'open'; + + return ( + + + navigation.openDrawer()}> + + + + + navigation.navigate('Search')} + > + Search + + + + ); +}; + +export default Header; diff --git a/apps/mobile/src/components/icons/FolderIcon.tsx b/apps/mobile/src/components/icons/FolderIcon.tsx new file mode 100644 index 000000000..311544248 --- /dev/null +++ b/apps/mobile/src/components/icons/FolderIcon.tsx @@ -0,0 +1,26 @@ +import FolderWhite from '@sd/assets/svgs/folder-white.svg'; +import Folder from '@sd/assets/svgs/folder.svg'; +import React from 'react'; +import { SvgProps } from 'react-native-svg'; + +type FolderProps = { + /** + * Render a white folder icon + */ + isWhite?: boolean; + + /** + * The size of the icon to show -- uniform width and height + */ + size?: number; +} & SvgProps; + +const FolderIcon: React.FC = ({ size = 24, isWhite, ...svgProps }) => { + return isWhite ? ( + + ) : ( + + ); +}; + +export default FolderIcon; diff --git a/packages/interface/src/components/dialog/DemoDialog.tsx b/apps/mobile/src/components/layout/Card.tsx similarity index 100% rename from packages/interface/src/components/dialog/DemoDialog.tsx rename to apps/mobile/src/components/layout/Card.tsx diff --git a/apps/mobile/src/components/layout/CollapsibleView.tsx b/apps/mobile/src/components/layout/CollapsibleView.tsx new file mode 100644 index 000000000..abdf28a68 --- /dev/null +++ b/apps/mobile/src/components/layout/CollapsibleView.tsx @@ -0,0 +1,46 @@ +import { Ionicons } from '@expo/vector-icons'; +import { MotiView } from 'moti'; +import React, { useReducer } from 'react'; +import { Pressable, StyleProp, Text, TextStyle, View, ViewStyle } from 'react-native'; + +import tw from '../../lib/tailwind'; +import { AnimatedHeight } from '../animation/layout'; + +type CollapsibleViewProps = { + title: string; + titleStyle?: StyleProp; + children: React.ReactNode; + containerStyle?: StyleProp; +}; + +const CollapsibleView = ({ title, titleStyle, containerStyle, children }: CollapsibleViewProps) => { + const [hide, toggle] = useReducer((hide) => !hide, false); + + return ( + + + + {title} + + + + + + {children} + + ); +}; + +export default CollapsibleView; diff --git a/apps/mobile/src/components/layout/VirtualizedListWrapper.tsx b/apps/mobile/src/components/layout/VirtualizedListWrapper.tsx new file mode 100644 index 000000000..6a110b358 --- /dev/null +++ b/apps/mobile/src/components/layout/VirtualizedListWrapper.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { FlatList } from 'react-native'; + +export default function VirtualizedListWrapper({ children }) { + return ( + 'key'} + showsHorizontalScrollIndicator={false} + showsVerticalScrollIndicator={false} + renderItem={null} + ListHeaderComponent={<>{children}} + /> + ); +} diff --git a/apps/mobile/src/components/modals/FileModal.tsx b/apps/mobile/src/components/modals/FileModal.tsx new file mode 100644 index 000000000..e5658ee1d --- /dev/null +++ b/apps/mobile/src/components/modals/FileModal.tsx @@ -0,0 +1,128 @@ +import { BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet'; +import { format } from 'date-fns'; +import React, { useRef } from 'react'; +import { Button, Pressable, Text, View } from 'react-native'; +import { ChevronLeftIcon } from 'react-native-heroicons/outline'; + +import tw from '../../lib/tailwind'; +import { useFileModalStore } from '../../stores/useModalStore'; +import Divider from '../base/Divider'; +import FileIcon from '../file/FileIcon'; +import ModalBackdrop from './layout/ModalBackdrop'; +import ModalHandle from './layout/ModalHandle'; + +/* +https://github.com/software-mansion/react-native-reanimated/issues/3296 +https://github.com/gorhom/react-native-bottom-sheet/issues/925 +https://github.com/gorhom/react-native-bottom-sheet/issues/1036 + +Reanimated has a bug where it sometimes doesn't animate on mount (IOS only?), doing a console.log() seems to do a re-render and fix the issue. +We can't do this for production obvs but until then they might fix it so, let's not try weird hacks for now and live with the logs. +*/ + +interface MetaItemProps { + title: string; + value: string; +} + +function MetaItem({ title, value }: MetaItemProps) { + return ( + + {title} + {value} + + ); +} + +export const FileModal = () => { + const { fileRef, data } = useFileModalStore(); + + const fileDetailsRef = useRef(null); + + return ( + <> + console.log(from, to)} + > + {data && ( + + {/* File Icon / Name */} + + + {/* File Name, Details etc. */} + + {data?.name} + + 5 MB, + + {data?.extension.toUpperCase()}, + + 15 Aug + + fileDetailsRef.current.present()}> + More + + + + {/* Divider */} + + {/* Buttons */} + + + + + ); +}; + +export default SearchScreen; diff --git a/apps/mobile/src/screens/modals/settings/Settings.tsx b/apps/mobile/src/screens/modals/settings/Settings.tsx new file mode 100644 index 000000000..73fd7d643 --- /dev/null +++ b/apps/mobile/src/screens/modals/settings/Settings.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Text, View } from 'react-native'; + +import tw from '../../../lib/tailwind'; +import { RootStackScreenProps } from '../../../navigation'; + +export default function SettingsScreen({ navigation }: RootStackScreenProps<'Settings'>) { + return ( + + Settings + + + ); +} diff --git a/apps/mobile/src/screens/onboarding/Onboarding.tsx b/apps/mobile/src/screens/onboarding/Onboarding.tsx new file mode 100644 index 000000000..a56eed859 --- /dev/null +++ b/apps/mobile/src/screens/onboarding/Onboarding.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Image, Text, View } from 'react-native'; + +import { FadeInUpAnimation, LogoAnimation } from '../../components/animation/layout'; +import { AnimatedButton } from '../../components/base/Button'; +import { setItemToStorage } from '../../lib/storage'; +import tw from '../../lib/tailwind'; +import type { OnboardingStackScreenProps } from '../../navigation/OnboardingNavigator'; +import { useOnboardingStore } from '../../stores/useOnboardingStore'; + +const OnboardingScreen = ({ navigation }: OnboardingStackScreenProps<'Onboarding'>) => { + const { hideOnboarding } = useOnboardingStore(); + + function onButtonPress() { + setItemToStorage('@onboarding', '1'); + // TODO: Add a loading indicator to button as this takes a second or so. + hideOnboarding(); + } + + return ( + + {/* Logo */} + + + + + + {/* Text */} + + + + A file explorer from the future. + + + + + Combine your drives and clouds into one database that you can organize and explore from + any device. + + + + {/* Get Started Button */} + + + + Get Started + + + + + ); +}; + +export default OnboardingScreen; diff --git a/apps/mobile/src/stores/useLibraryStore.ts b/apps/mobile/src/stores/useLibraryStore.ts new file mode 100644 index 000000000..77294c4cc --- /dev/null +++ b/apps/mobile/src/stores/useLibraryStore.ts @@ -0,0 +1,13 @@ +import create from 'zustand'; + +interface LibraryStore { + currentLibraryUuid: string | null; + switchLibrary: (id: string) => void; +} + +export const useLibraryStore = create()((set) => ({ + currentLibraryUuid: null, + switchLibrary: (uuid) => { + set((state) => ({ currentLibraryUuid: uuid })); + } +})); diff --git a/apps/mobile/src/stores/useModalStore.ts b/apps/mobile/src/stores/useModalStore.ts new file mode 100644 index 000000000..3c3c3f6bd --- /dev/null +++ b/apps/mobile/src/stores/useModalStore.ts @@ -0,0 +1,19 @@ +import { BottomSheetModal } from '@gorhom/bottom-sheet'; +import React from 'react'; +import create from 'zustand'; + +import { FilePath } from '../types/bindings'; + +interface FileModalState { + fileRef: React.RefObject; + data: FilePath | null; + setData: (data: FilePath) => void; + clearData: () => void; +} + +export const useFileModalStore = create((set) => ({ + fileRef: React.createRef(), + data: null, + setData: (data: FilePath) => set((_) => ({ data })), + clearData: () => set((_) => ({ data: null })) +})); diff --git a/apps/mobile/src/stores/useOnboardingStore.ts b/apps/mobile/src/stores/useOnboardingStore.ts new file mode 100644 index 000000000..e76e5f1ce --- /dev/null +++ b/apps/mobile/src/stores/useOnboardingStore.ts @@ -0,0 +1,11 @@ +import create from 'zustand'; + +interface OnboardingState { + showOnboarding: boolean; + hideOnboarding: () => void; +} + +export const useOnboardingStore = create((set) => ({ + showOnboarding: true, + hideOnboarding: () => set((state) => ({ showOnboarding: false })) +})); diff --git a/apps/mobile/src/types/bindings.ts b/apps/mobile/src/types/bindings.ts new file mode 100644 index 000000000..7114e01b6 --- /dev/null +++ b/apps/mobile/src/types/bindings.ts @@ -0,0 +1,117 @@ +// This file was generated by [rspc](https://github.com/oscartbeaumont/rspc). Do not edit this file manually. + +export type Operations = { + queries: + { key: ["library.getStatistics", LibraryArgs], result: Statistics } | + { key: ["jobs.getRunning", LibraryArgs], result: Array } | + { key: ["version"], result: string } | + { key: ["files.readMetadata", LibraryArgs], result: null } | + { key: ["locations.getExplorerDir", LibraryArgs], result: DirectoryWithContents } | + { key: ["jobs.getHistory", LibraryArgs], result: Array } | + { key: ["library.get"], result: Array } | + { key: ["volumes.get"], result: Array } | + { key: ["tags.getFilesForTag", LibraryArgs], result: Tag | null } | + { key: ["locations.get", LibraryArgs], result: Array } | + { key: ["locations.getById", LibraryArgs], result: Location | null } | + { key: ["tags.get", LibraryArgs], result: Array } | + { key: ["getNode"], result: NodeState }, + mutations: + { key: ["tags.create", LibraryArgs], result: Tag } | + { key: ["files.setFavorite", LibraryArgs], result: null } | + { key: ["jobs.identifyUniqueFiles", LibraryArgs], result: null } | + { key: ["files.delete", LibraryArgs], result: null } | + { key: ["library.edit", EditLibraryArgs], result: null } | + { key: ["library.delete", string], result: null } | + { key: ["jobs.generateThumbsForLocation", LibraryArgs], result: null } | + { key: ["files.setNote", LibraryArgs], result: null } | + { key: ["library.create", string], result: null } | + { key: ["locations.quickRescan", LibraryArgs], result: null } | + { key: ["locations.delete", LibraryArgs], result: null } | + { key: ["tags.update", LibraryArgs], result: null } | + { key: ["tags.assign", LibraryArgs], result: null } | + { key: ["locations.create", LibraryArgs], result: Location } | + { key: ["locations.update", LibraryArgs], result: null } | + { key: ["locations.fullRescan", LibraryArgs], result: null } | + { key: ["tags.delete", LibraryArgs], result: null }, + subscriptions: + { key: ["jobs.newThumbnail", LibraryArgs], result: string } | + { key: ["invalidateQuery"], result: InvalidateOperationEvent } +}; + +export interface TagCreateArgs { name: string, color: string } + +export interface Location { id: number, pub_id: Array, node_id: number | null, name: string | null, local_path: string | null, total_capacity: number | null, available_capacity: number | null, filesystem: string | null, disk_type: number | null, is_removable: boolean | null, is_online: boolean, date_created: string, node: Node | null | null, file_paths: Array | null } + +export interface File { id: number, cas_id: string, integrity_checksum: string | null, kind: number, size_in_bytes: string, key_id: number | null, hidden: boolean, favorite: boolean, important: boolean, has_thumbnail: boolean, has_thumbstrip: boolean, has_video_preview: boolean, ipfs_id: string | null, note: string | null, date_created: string, date_modified: string, date_indexed: string, tags: Array | null, labels: Array | null, albums: Array | null, spaces: Array | null, paths: Array | null, comments: Array | null, media_data: MediaData | null | null, key: Key | null | null } + +export interface EditLibraryArgs { id: string, name: string | null, description: string | null } + +export interface LibraryArgs { library_id: string, arg: T } + +export interface NodeConfig { version: string | null, id: string, name: string, p2p_port: number | null } + +export interface MediaData { id: number, pixel_width: number | null, pixel_height: number | null, longitude: number | null, latitude: number | null, fps: number | null, capture_device_make: string | null, capture_device_model: string | null, capture_device_software: string | null, duration_seconds: number | null, codecs: string | null, streams: number | null, files: File | null | null } + +export interface Space { id: number, pub_id: Array, name: string | null, description: string | null, date_created: string, date_modified: string, files: Array | null } + +export interface Album { id: number, pub_id: Array, name: string, is_hidden: boolean, date_created: string, date_modified: string, files: Array | null } + +export interface LabelOnFile { date_created: string, label_id: number, label: Label | null, file_id: number, file: File | null } + +export interface JobReport { id: string, name: string, data: Array | null, date_created: string, date_modified: string, status: JobStatus, task_count: number, completed_task_count: number, message: string, seconds_elapsed: number } + +export interface FilePath { id: number, is_dir: boolean, location_id: number | null, materialized_path: string, name: string, extension: string | null, file_id: number | null, parent_id: number | null, key_id: number | null, date_created: string, date_modified: string, date_indexed: string, file: File | null | null, location: Location | null | null, key: Key | null | null } + +export interface LocationUpdateArgs { id: number, name: string | null } + +export type JobStatus = "Queued" | "Running" | "Completed" | "Canceled" | "Failed" | "Paused" + +export interface Node { id: number, pub_id: Array, name: string, platform: number, version: string | null, last_seen: string, timezone: string | null, date_created: string, sync_events: Array | null, jobs: Array | null, Location: Array | null } + +export interface Key { id: number, checksum: string, name: string | null, date_created: string | null, algorithm: number | null, files: Array | null, file_paths: Array | null } + +export interface SyncEvent { id: number, node_id: number, timestamp: string, record_id: Array, kind: number, column: string | null, value: string, node: Node | null } + +export interface SetFavoriteArgs { id: number, favorite: boolean } + +export interface InvalidateOperationEvent { key: string, arg: any } + +export interface NodeState { version: string | null, id: string, name: string, p2p_port: number | null, data_path: string } + +export interface ConfigMetadata { version: string | null } + +export interface Volume { name: string, mount_point: string, total_capacity: bigint, available_capacity: bigint, is_removable: boolean, disk_type: string | null, file_system: string | null, is_root_filesystem: boolean } + +export interface FileInAlbum { date_created: string, album_id: number, album: Album | null, file_id: number, file: File | null } + +export interface Comment { id: number, pub_id: Array, content: string, date_created: string, date_modified: string, file_id: number | null, file: File | null | null } + +export interface LibraryConfig { version: string | null, name: string, description: string } + +export interface TagUpdateArgs { id: number, name: string | null, color: string | null } + +export interface TagAssignArgs { file_id: number, tag_id: number } + +export interface Statistics { id: number, date_captured: string, total_file_count: number, library_db_size: string, total_bytes_used: string, total_bytes_capacity: string, total_unique_bytes: string, total_bytes_free: string, preview_media_bytes: string } + +export interface GenerateThumbsForLocationArgs { id: number, path: string } + +export interface SetNoteArgs { id: number, note: string | null } + +export interface Job { id: Array, name: string, node_id: number, action: number, status: number, data: Array | null, task_count: number, completed_task_count: number, date_created: string, date_modified: string, seconds_elapsed: number, nodes: Node | null } + +export interface GetExplorerDirArgs { location_id: number, path: string, limit: number } + +export interface Label { id: number, pub_id: Array, name: string | null, date_created: string, date_modified: string, label_files: Array | null } + +export interface LibraryConfigWrapped { uuid: string, config: LibraryConfig } + +export interface TagOnFile { date_created: string, tag_id: number, tag: Tag | null, file_id: number, file: File | null } + +export interface Tag { id: number, pub_id: Array, name: string | null, color: string | null, total_files: number | null, redundancy_goal: number | null, date_created: string, date_modified: string, tag_files: Array | null } + +export interface IdentifyUniqueFilesArgs { id: number, path: string } + +export interface DirectoryWithContents { directory: FilePath, contents: Array } + +export interface FileInSpace { date_created: string, space_id: number, space: Space | null, file_id: number, file: File | null } diff --git a/apps/mobile/src/types/declarations.d.ts b/apps/mobile/src/types/declarations.d.ts new file mode 100644 index 000000000..1eae33fe9 --- /dev/null +++ b/apps/mobile/src/types/declarations.d.ts @@ -0,0 +1,6 @@ +declare module '*.svg' { + import React from 'react'; + import { SvgProps } from 'react-native-svg'; + const content: React.FC; + export default content; +} diff --git a/apps/mobile/src/types/helper.ts b/apps/mobile/src/types/helper.ts new file mode 100644 index 000000000..16136471f --- /dev/null +++ b/apps/mobile/src/types/helper.ts @@ -0,0 +1 @@ +export type valueof = T[keyof T]; diff --git a/apps/mobile/tailwind.config.js b/apps/mobile/tailwind.config.js new file mode 100644 index 000000000..4865f47ef --- /dev/null +++ b/apps/mobile/tailwind.config.js @@ -0,0 +1,63 @@ +module.exports = { + content: ['./screens/**/*.{js,ts,jsx}', './components/**/*.{js,ts,jsx}', 'App.tsx'], + theme: { + fontSize: { + 'tiny': '.65rem', + 'xs': '.75rem', + 'sm': '.84rem', + 'base': '1rem', + 'lg': '1.125rem', + 'xl': '1.25rem', + '2xl': '1.5rem', + '3xl': '1.875rem', + '4xl': '2.25rem', + '5xl': '3rem', + '6xl': '4rem', + '7xl': '5rem' + }, + extend: { + colors: { + primary: { + DEFAULT: '#2599FF', + 50: '#FFFFFF', + 100: '#F1F8FF', + 200: '#BEE1FF', + 300: '#8BC9FF', + 400: '#58B1FF', + 500: '#2599FF', + 600: '#0081F1', + 700: '#0065BE', + 800: '#004A8B', + 900: '#002F58' + }, + gray: { + DEFAULT: '#505468', + 50: '#F1F1F4', + 100: '#E8E9ED', + 150: '#E0E1E6', + 200: '#D8DAE3', + 250: '#D2D4DC', + 300: '#C0C2CE', + 350: '#A6AABF', + 400: '#9196A8', + 450: '#71758A', + 500: '#303544', + 550: '#20222d', + 600: '#171720', + 650: '#121219', + 700: '#121317', + 750: '#0D0E11', + 800: '#0C0C0F', + 850: '#08090D', + 900: '#060609', + 950: '#030303' + } + }, + extend: {} + } + }, + variants: { + extend: {} + }, + plugins: [] +}; diff --git a/apps/mobile/tsconfig.json b/apps/mobile/tsconfig.json new file mode 100644 index 000000000..3e08e07e7 --- /dev/null +++ b/apps/mobile/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "expo/tsconfig.base", + "compilerOptions": {} +} diff --git a/apps/server/Cargo.toml b/apps/server/Cargo.toml index 4a242d8c9..4a8dc55cf 100644 --- a/apps/server/Cargo.toml +++ b/apps/server/Cargo.toml @@ -4,10 +4,9 @@ version = "0.1.0" edition = "2021" [dependencies] -actix = "0.13.0" -actix-web = "4.0.1" -actix-web-actors = "4.1.0" sdcore = { path = "../../core", features = [] } -serde = "1.0.136" -serde_json = "1.0.79" -tokio = { version = "1.17.0", features = ["sync", "rt"] } \ No newline at end of file +rspc = { version = "0.0.4", features = ["axum"] } +axum = "0.5.13" +tokio = { version = "1.17.0", features = ["sync", "rt-multi-thread", "signal"] } +tracing = "0.1.35" +ctrlc = "3.2.2" diff --git a/apps/server/k8s/infrastructure.yaml b/apps/server/k8s/infrastructure.yaml deleted file mode 100644 index a5e44b4ee..000000000 --- a/apps/server/k8s/infrastructure.yaml +++ /dev/null @@ -1,42 +0,0 @@ -# Infrastructure setups up the Kubernetes cluster for Spacedrive! -# -# To get the service account token use the following: -# ```bash -# TOKENNAME=`kubectl -n spacedrive get sa/spacedrive-ci -o jsonpath='{.secrets[0].name}'` -# kubectl -n spacedrive get secret $TOKENNAME -o jsonpath='{.data.token}' | base64 -d -# ``` - -apiVersion: v1 -kind: Namespace -metadata: - name: spacedrive ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: spacedrive-ci - namespace: spacedrive ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: spacedrive-ns-full - namespace: spacedrive -rules: - - apiGroups: ['apps'] - resources: ['deployments'] - verbs: ['get', 'patch'] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: spacedrive-ci-rb - namespace: spacedrive -subjects: - - kind: ServiceAccount - name: spacedrive-ci - namespace: spacedrive -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: spacedrive-ns-full diff --git a/apps/server/k8s/sdserver.yaml b/apps/server/k8s/sdserver.yaml deleted file mode 100644 index 00f02c1c1..000000000 --- a/apps/server/k8s/sdserver.yaml +++ /dev/null @@ -1,118 +0,0 @@ -# This will deploy the Spacedrive Server container to the `spacedrive`` namespace on Kubernetes. - -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: sdserver-ingress - namespace: spacedrive - labels: - app.kubernetes.io/name: sdserver - app.kubernetes.io/component: webserver - annotations: - traefik.ingress.kubernetes.io/router.tls.certresolver: le - traefik.ingress.kubernetes.io/router.middlewares: kube-system-antiseo@kubernetescrd -spec: - rules: - - host: spacedrive.otbeaumont.me - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: sdserver-service - port: - number: 8080 ---- -apiVersion: v1 -kind: Service -metadata: - name: sdserver-service - namespace: spacedrive - labels: - app.kubernetes.io/name: sdserver - app.kubernetes.io/component: webserver -spec: - ports: - - port: 8080 - targetPort: 8080 - protocol: TCP - selector: - app.kubernetes.io/name: sdserver - app.kubernetes.io/component: webserver ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: sdserver-pvc - namespace: spacedrive -spec: - accessModes: - - ReadWriteOnce - storageClassName: local-path - resources: - requests: - storage: 512M ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: sdserver-deployment - namespace: spacedrive - labels: - app.kubernetes.io/name: sdserver - app.kubernetes.io/component: webserver -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: sdserver - app.kubernetes.io/component: webserver - template: - metadata: - labels: - app.kubernetes.io/name: sdserver - app.kubernetes.io/component: webserver - spec: - restartPolicy: Always - # refer to Dockerfile to find securityContext values - securityContext: - runAsUser: 101 - runAsGroup: 101 - fsGroup: 101 - containers: - - name: sdserver - image: ghcr.io/oscartbeaumont/spacedrive/server:staging - imagePullPolicy: Always - ports: - - containerPort: 8080 - volumeMounts: - - name: data-volume - mountPath: /data - securityContext: - allowPrivilegeEscalation: false - resources: - limits: - memory: 100Mi - cpu: 100m - requests: - memory: 5Mi - cpu: 10m - readinessProbe: - httpGet: - path: /health - port: 8080 - initialDelaySeconds: 10 - failureThreshold: 4 - periodSeconds: 5 - livenessProbe: - httpGet: - path: /health - port: 8080 - initialDelaySeconds: 20 - failureThreshold: 3 - periodSeconds: 10 - volumes: - - name: data-volume - persistentVolumeClaim: - claimName: sdserver-pvc diff --git a/apps/server/src/main.rs b/apps/server/src/main.rs index 7e9c4683e..3711f815b 100644 --- a/apps/server/src/main.rs +++ b/apps/server/src/main.rs @@ -1,210 +1,61 @@ -use sdcore::{ClientCommand, ClientQuery, CoreController, CoreEvent, CoreResponse, Node}; -use std::{env, path::Path}; +use std::{env, net::SocketAddr, path::Path}; -use actix::{ - Actor, AsyncContext, ContextFutureSpawner, Handler, Message, StreamHandler, - WrapFuture, -}; -use actix_web::{ - get, http::StatusCode, web, App, Error, HttpRequest, HttpResponse, HttpServer, - Responder, -}; -use actix_web_actors::ws; -use serde::{Deserialize, Serialize}; +use axum::{handler::Handler, routing::get}; +use sdcore::Node; +use tracing::info; -use tokio::sync::mpsc; +mod utils; -const DATA_DIR_ENV_VAR: &'static str = "DATA_DIR"; - -/// Define HTTP actor -struct Socket { - _event_receiver: web::Data>, - core: web::Data, -} - -impl Actor for Socket { - type Context = ws::WebsocketContext; -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase", tag = "type", content = "data")] -enum SocketMessagePayload { - Command(ClientCommand), - Query(ClientQuery), -} - -#[derive(Serialize, Deserialize, Message)] -#[rtype(result = "()")] -#[serde(rename_all = "camelCase")] -struct SocketMessage { - id: String, - payload: SocketMessagePayload, -} - -impl StreamHandler> for Socket { - fn handle( - &mut self, - msg: Result, - ctx: &mut Self::Context, - ) { - // TODO: Add heartbeat and reconnect logic in the future. We can refer to https://github.com/actix/examples/blob/master/websockets/chat/src/session.rs for the heartbeat stuff. - - match msg { - Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), - Ok(ws::Message::Text(text)) => { - let msg: SocketMessage = serde_json::from_str(&text).unwrap(); - - let core = self.core.clone(); - - let recipient = ctx.address().recipient(); - - let fut = async move { - match msg.payload { - SocketMessagePayload::Query(query) => { - match core.query(query).await { - Ok(response) => recipient.do_send(SocketResponse { - id: msg.id.clone(), - payload: SocketResponsePayload::Query(response), - }), - Err(err) => { - println!("query error: {:?}", err); - // Err(err.to_string()) - }, - }; - }, - SocketMessagePayload::Command(command) => { - match core.command(command).await { - Ok(response) => recipient.do_send(SocketResponse { - id: msg.id.clone(), - payload: SocketResponsePayload::Query(response), - }), - Err(err) => { - println!("command error: {:?}", err); - // Err(err.to_string()) - }, - }; - }, - } - }; - - fut.into_actor(self).spawn(ctx); - - () - }, - _ => (), - } - } -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase", tag = "type", content = "data")] -pub enum SocketResponsePayload { - Query(CoreResponse), -} - -#[derive(Message, Serialize)] -#[rtype(result = "()")] -struct SocketResponse { - id: String, - payload: SocketResponsePayload, -} - -impl Handler for Socket { - type Result = (); - - fn handle(&mut self, msg: SocketResponse, ctx: &mut Self::Context) { - let string = serde_json::to_string(&msg).unwrap(); - ctx.text(string); - } -} - -#[get("/")] -async fn index() -> impl Responder { - format!("Spacedrive Server!") -} - -#[get("/health")] -async fn healthcheck() -> impl Responder { - format!("OK") -} - -#[get("/ws")] -async fn ws_handler( - req: HttpRequest, - stream: web::Payload, - event_receiver: web::Data>, - controller: web::Data, -) -> Result { - let resp = ws::start( - Socket { - _event_receiver: event_receiver, - core: controller, - }, - &req, - stream, - ); - resp -} - -#[get("/file/{file:.*}")] -async fn file() -> impl Responder { - // TODO - format!("OK") -} - -async fn not_found() -> impl Responder { - HttpResponse::build(StatusCode::OK).body("We're past the event horizon...") -} - -#[actix_web::main] -async fn main() -> std::io::Result<()> { - let (event_receiver, controller) = setup().await; - - println!("Listening http://localhost:8080"); - HttpServer::new(move || { - App::new() - .app_data(event_receiver.clone()) - .app_data(controller.clone()) - .service(index) - .service(healthcheck) - .service(ws_handler) - .service(file) - .default_service(web::route().to(not_found)) - }) - .bind(("0.0.0.0", 8080))? - .run() - .await -} - -async fn setup() -> ( - web::Data>, - web::Data, -) { - let data_dir_path = match env::var(DATA_DIR_ENV_VAR) { +#[tokio::main] +async fn main() { + let data_dir = match env::var("DATA_DIR") { Ok(path) => Path::new(&path).to_path_buf(), Err(_e) => { #[cfg(not(debug_assertions))] { - panic!("${} is not set ({})", DATA_DIR_ENV_VAR, _e) + panic!("'$DATA_DIR' is not set ({})", _e) } std::env::current_dir() .expect( - "Unable to get your currrent directory. Maybe try setting $DATA_DIR?", + "Unable to get your current directory. Maybe try setting $DATA_DIR?", ) .join("sdserver_data") }, }; - let (mut node, event_receiver) = Node::new(data_dir_path).await; + let port = env::var("PORT") + .map(|port| port.parse::().unwrap_or(8080)) + .unwrap_or(8080); - node.initializer().await; + let (node, router) = Node::new(data_dir).await; - let controller = node.get_controller(); + ctrlc::set_handler({ + let node = node.clone(); + move || { + node.shutdown(); + } + }) + .expect("Error setting Ctrl-C handler"); - tokio::spawn(async move { - node.start().await; - }); + let app = axum::Router::new() + .route("/", get(|| async { "Spacedrive Server!" })) + .route("/health", get(|| async { "OK" })) + .route( + "/rspcws", + router.axum_ws_handler(move || node.get_request_context()), + ) + .fallback( + (|| async { "404 Not Found: We're past the event horizon..." }) + .into_service(), + ); - (web::Data::new(event_receiver), web::Data::new(controller)) + let mut addr = "[::]:8080".parse::().unwrap(); // This listens on IPv6 and IPv4 + addr.set_port(port); + info!("Listening on http://localhost:{}", port); + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .with_graceful_shutdown(utils::axum_shutdown_signal()) + .await + .expect("Error with HTTP server!"); } diff --git a/apps/server/src/utils.rs b/apps/server/src/utils.rs new file mode 100644 index 000000000..f6437d365 --- /dev/null +++ b/apps/server/src/utils.rs @@ -0,0 +1,28 @@ +use tokio::signal; + +/// shutdown_signal will inform axum to gracefully shutdown when the process is asked to shutdown. +pub async fn axum_shutdown_signal() { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } + + println!("signal received, starting graceful shutdown"); +} diff --git a/apps/web/package.json b/apps/web/package.json index 2bba515ce..04c98aa0e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -8,24 +8,26 @@ "preview": "vite preview" }, "dependencies": { - "@fontsource/inter": "^4.5.10", + "@fontsource/inter": "^4.5.11", + "@rspc/client": "^0.0.5", "@sd/client": "workspace:*", "@sd/core": "workspace:*", "@sd/interface": "workspace:*", "@sd/ui": "workspace:*", - "react": "^18.1.0", - "react-dom": "^18.1.0" + "@tanstack/react-query": "^4.0.10", + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "devDependencies": { - "@types/react": "^18.0.9", - "@types/react-dom": "^18.0.5", - "@vitejs/plugin-react": "^1.3.2", + "@types/react": "^18.0.15", + "@types/react-dom": "^18.0.6", + "@vitejs/plugin-react": "^2.0.0", "autoprefixer": "^10.4.7", "postcss": "^8.4.14", "tailwind": "^4.0.0", - "typescript": "^4.7.2", - "vite": "^2.9.9", - "vite-plugin-svgr": "^2.1.0", - "vite-plugin-tsconfig-paths": "^1.0.5" + "typescript": "^4.7.4", + "vite": "^3.0.3", + "vite-plugin-svgr": "^2.2.1", + "vite-plugin-tsconfig-paths": "^1.1.0" } } diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 72560e3ff..a01b6386a 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,66 +1,13 @@ -import { BaseTransport } from '@sd/client'; -import { ClientCommand, ClientQuery, CoreEvent } from '@sd/core'; +import { WebsocketTransport, createClient } from '@rspc/client'; +import { Operations, queryClient, rspc } from '@sd/client'; import SpacedriveInterface from '@sd/interface'; import React, { useEffect } from 'react'; -const websocket = new WebSocket(import.meta.env.VITE_SDSERVER_BASE_URL || 'ws://localhost:8080/ws'); - -const randomId = () => Math.random().toString(36).slice(2); - -// bind state to core via Tauri -class Transport extends BaseTransport { - requestMap = new Map void>(); - - constructor() { - super(); - - websocket.addEventListener('message', (event) => { - if (!event.data) return; - - const { id, payload } = JSON.parse(event.data); - - const { type, data } = payload; - if (type === 'event') { - this.emit('core_event', data); - } else if (type === 'query' || type === 'command') { - if (this.requestMap.has(id)) { - this.requestMap.get(id)?.(data); - this.requestMap.delete(id); - } - } - }); - } - async query(query: ClientQuery) { - const id = randomId(); - let resolve: (data: any) => void; - - const promise = new Promise((res) => { - resolve = res; - }); - - // @ts-ignore - this.requestMap.set(id, resolve); - - websocket.send(JSON.stringify({ id, payload: { type: 'query', data: query } })); - - return await promise; - } - async command(command: ClientCommand) { - const id = randomId(); - let resolve: (data: any) => void; - - const promise = new Promise((res) => { - resolve = res; - }); - - // @ts-ignore - this.requestMap.set(id, resolve); - - websocket.send(JSON.stringify({ id, payload: { type: 'command', data: command } })); - - return await promise; - } -} +const client = createClient({ + transport: new WebsocketTransport( + import.meta.env.VITE_SDSERVER_BASE_URL || 'ws://localhost:8080/rspcws' + ) +}); function App() { useEffect(() => { @@ -69,20 +16,20 @@ function App() { return (
- {/*
*/} - { - return Promise.resolve([]); - }} - /> + + { + return Promise.resolve([]); + }} + /> +
); } diff --git a/core/.rustfmt.toml b/core/.rustfmt.toml index 411a5f052..89f5d09e3 100644 --- a/core/.rustfmt.toml +++ b/core/.rustfmt.toml @@ -10,5 +10,4 @@ merge_derives = true use_try_shorthand = false use_field_init_shorthand = false force_explicit_abi = true -# normalize_comments = true -normalize_doc_attributes = true \ No newline at end of file +# normalize_comments = true \ No newline at end of file diff --git a/core/Cargo.toml b/core/Cargo.toml index e748bfa2f..5a309048b 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -6,9 +6,13 @@ authors = ["Spacedrive Technology Inc."] license = "GNU GENERAL PUBLIC LICENSE" repository = "https://github.com/spacedriveapp/spacedrive" edition = "2021" +rust-version = "1.63.0" [features] +default = ["p2p"] p2p = [] # This feature controlls whether the Spacedrive Core contains the Peer to Peer syncing engine (It isn't required for the hosted core so we can disable it). +mobile = [] # This feature allows features to be disabled when the Core is running on mobile. +ffmpeg = ["dep:ffmpeg-next"] # This feature controls whether the Spacedrive Core contains functionality which requires FFmpeg. [dependencies] hostname = "0.3.1" @@ -16,29 +20,39 @@ hostname = "0.3.1" # Universal Dependencies base64 = "0.13.0" serde = { version = "1.0", features = ["derive"] } -chrono = { version = "0.4.0", features = ["serde"] } +chrono = { version = "0.4.19", features = ["serde"] } serde_json = "1.0" futures = "0.3" data-encoding = "2.3.2" ring = "0.17.0-alpha.10" int-enum = "0.4.0" +rmp = "^0.8.11" +rmp-serde = "^1.1.0" # Project dependencies -ts-rs = { version = "6.1", features = ["chrono-impl"] } -prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust.git", tag = "0.5.0" } +prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust.git", rev = "6a0119bce951c8d956542a59b2f783fc5a591fc7", features = [ + "rspc", + "sqlite-create-many", +] } +rspc = { version = "0.0.4", features = [ + "uuid", + "chrono", + "tracing", +] } walkdir = "^2.3.2" -lazy_static = "1.4.0" -uuid = "0.8" +uuid = { version = "1.1.2", features = ["v4", "serde"] } sysinfo = "0.23.9" thiserror = "1.0.30" -core-derive = { path = "./derive" } -tokio = { version = "1.17.0", features = ["sync", "rt"] } +tokio = { version = "1.17.0", features = ["sync", "rt-multi-thread"] } include_dir = { version = "0.7.2", features = ["glob"] } -async-trait = "0.1.52" +async-trait = "^0.1.52" image = "0.24.1" webp = "0.2.2" -ffmpeg-next = "5.0.3" +ffmpeg-next = { version = "5.0.3", optional = true, features = [] } fs_extra = "1.2.0" -log = { version = "0.4.17", features = ["max_level_trace"] } -env_logger = "0.9.0" +tracing = "0.1.35" +tracing-subscriber = { version = "0.3.14", features = ["env-filter"] } +async-stream = "0.3.3" +once_cell = "1.13.0" +ctor = "0.1.22" diff --git a/core/bindings/Client.ts b/core/bindings/Client.ts deleted file mode 100644 index 4660f7e8d..000000000 --- a/core/bindings/Client.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Platform } from './Platform'; - -export interface Client { - uuid: string; - name: string; - platform: Platform; - tcp_address: string; - last_seen: string; - last_synchronized: string; -} diff --git a/core/bindings/ClientCommand.ts b/core/bindings/ClientCommand.ts deleted file mode 100644 index 2677dd55f..000000000 --- a/core/bindings/ClientCommand.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ClientCommand = { key: "FileReadMetaData", params: { id: number, } } | { key: "FileSetNote", params: { id: number, note: string | null, } } | { key: "FileDelete", params: { id: number, } } | { key: "LibDelete", params: { id: number, } } | { key: "TagCreate", params: { name: string, color: string, } } | { key: "TagUpdate", params: { name: string, color: string, } } | { key: "TagAssign", params: { file_id: number, tag_id: number, } } | { key: "TagDelete", params: { id: number, } } | { key: "LocCreate", params: { path: string, } } | { key: "LocUpdate", params: { id: number, name: string | null, } } | { key: "LocDelete", params: { id: number, } } | { key: "LocRescan", params: { id: number, } } | { key: "SysVolumeUnmount", params: { id: number, } } | { key: "GenerateThumbsForLocation", params: { id: number, path: string, } } | { key: "IdentifyUniqueFiles", params: { id: number, path: string, } }; \ No newline at end of file diff --git a/core/bindings/ClientQuery.ts b/core/bindings/ClientQuery.ts deleted file mode 100644 index 9d4792a66..000000000 --- a/core/bindings/ClientQuery.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ClientQuery = { key: "NodeGetState" } | { key: "SysGetVolumes" } | { key: "LibGetTags" } | { key: "JobGetRunning" } | { key: "JobGetHistory" } | { key: "SysGetLocations" } | { key: "SysGetLocation", params: { id: number, } } | { key: "LibGetExplorerDir", params: { location_id: number, path: string, limit: number, } } | { key: "GetLibraryStatistics" } | { key: "GetNodes" }; \ No newline at end of file diff --git a/core/bindings/ClientState.ts b/core/bindings/ClientState.ts deleted file mode 100644 index ce5c2d7af..000000000 --- a/core/bindings/ClientState.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { LibraryState } from './LibraryState'; - -export interface ClientState { - client_uuid: string; - client_id: number; - client_name: string; - data_path: string; - tcp_port: number; - libraries: Array; - current_library_uuid: string; -} diff --git a/core/bindings/CoreEvent.ts b/core/bindings/CoreEvent.ts deleted file mode 100644 index 91a556227..000000000 --- a/core/bindings/CoreEvent.ts +++ /dev/null @@ -1,5 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ClientQuery } from "./ClientQuery"; -import type { CoreResource } from "./CoreResource"; - -export type CoreEvent = { key: "InvalidateQuery", data: ClientQuery } | { key: "InvalidateQueryDebounced", data: ClientQuery } | { key: "InvalidateResource", data: CoreResource } | { key: "NewThumbnail", data: { cas_id: string, } } | { key: "Log", data: { message: string, } } | { key: "DatabaseDisconnected", data: { reason: string | null, } }; \ No newline at end of file diff --git a/core/bindings/CoreResource.ts b/core/bindings/CoreResource.ts deleted file mode 100644 index 4ed7811b5..000000000 --- a/core/bindings/CoreResource.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { File } from "./File"; -import type { JobReport } from "./JobReport"; -import type { LocationResource } from "./LocationResource"; - -export type CoreResource = "Client" | "Library" | { Location: LocationResource } | { File: File } | { Job: JobReport } | "Tag"; \ No newline at end of file diff --git a/core/bindings/CoreResponse.ts b/core/bindings/CoreResponse.ts deleted file mode 100644 index 94dc0568c..000000000 --- a/core/bindings/CoreResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DirectoryWithContents } from "./DirectoryWithContents"; -import type { JobReport } from "./JobReport"; -import type { LocationResource } from "./LocationResource"; -import type { NodeState } from "./NodeState"; -import type { Statistics } from "./Statistics"; -import type { Volume } from "./Volume"; - -export type CoreResponse = { key: "Success", data: null } | { key: "SysGetVolumes", data: Array } | { key: "SysGetLocation", data: LocationResource } | { key: "SysGetLocations", data: Array } | { key: "LibGetExplorerDir", data: DirectoryWithContents } | { key: "NodeGetState", data: NodeState } | { key: "LocCreate", data: LocationResource } | { key: "JobGetRunning", data: Array } | { key: "JobGetHistory", data: Array } | { key: "GetLibraryStatistics", data: Statistics }; \ No newline at end of file diff --git a/core/bindings/DirectoryWithContents.ts b/core/bindings/DirectoryWithContents.ts deleted file mode 100644 index c41944db8..000000000 --- a/core/bindings/DirectoryWithContents.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FilePath } from "./FilePath"; - -export interface DirectoryWithContents { directory: FilePath, contents: Array, } \ No newline at end of file diff --git a/core/bindings/EncryptionAlgorithm.ts b/core/bindings/EncryptionAlgorithm.ts deleted file mode 100644 index d5b7d9889..000000000 --- a/core/bindings/EncryptionAlgorithm.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export type EncryptionAlgorithm = "None" | "AES128" | "AES192" | "AES256"; \ No newline at end of file diff --git a/core/bindings/File.ts b/core/bindings/File.ts deleted file mode 100644 index 8f8dd6aa6..000000000 --- a/core/bindings/File.ts +++ /dev/null @@ -1,5 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FileKind } from "./FileKind"; -import type { FilePath } from "./FilePath"; - -export interface File { id: number, cas_id: string, integrity_checksum: string | null, size_in_bytes: string, kind: FileKind, hidden: boolean, favorite: boolean, important: boolean, has_thumbnail: boolean, has_thumbstrip: boolean, has_video_preview: boolean, ipfs_id: string | null, note: string | null, date_created: string, date_modified: string, date_indexed: string, paths: Array, } \ No newline at end of file diff --git a/core/bindings/FileKind.ts b/core/bindings/FileKind.ts deleted file mode 100644 index 2b4f51fc7..000000000 --- a/core/bindings/FileKind.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FileKind = "Unknown" | "Directory" | "Package" | "Archive" | "Image" | "Video" | "Audio" | "Plaintext" | "Alias"; \ No newline at end of file diff --git a/core/bindings/FilePath.ts b/core/bindings/FilePath.ts deleted file mode 100644 index 6d45b049e..000000000 --- a/core/bindings/FilePath.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { File } from "./File"; - -export interface FilePath { id: number, is_dir: boolean, location_id: number, materialized_path: string, name: string, extension: string | null, file_id: number | null, parent_id: number | null, date_created: string, date_modified: string, date_indexed: string, file: File | null, } \ No newline at end of file diff --git a/core/bindings/JobReport.ts b/core/bindings/JobReport.ts deleted file mode 100644 index bd25c8d21..000000000 --- a/core/bindings/JobReport.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JobStatus } from "./JobStatus"; - -export interface JobReport { id: string, name: string, data: string | null, date_created: string, date_modified: string, status: JobStatus, task_count: number, completed_task_count: number, message: string, seconds_elapsed: string, } \ No newline at end of file diff --git a/core/bindings/JobStatus.ts b/core/bindings/JobStatus.ts deleted file mode 100644 index 58c3a06b3..000000000 --- a/core/bindings/JobStatus.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type JobStatus = "Queued" | "Running" | "Completed" | "Canceled" | "Failed"; \ No newline at end of file diff --git a/core/bindings/LibraryNode.ts b/core/bindings/LibraryNode.ts deleted file mode 100644 index f5c82dd96..000000000 --- a/core/bindings/LibraryNode.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Platform } from "./Platform"; - -export interface LibraryNode { uuid: string, name: string, platform: Platform, last_seen: string, } \ No newline at end of file diff --git a/core/bindings/LibraryState.ts b/core/bindings/LibraryState.ts deleted file mode 100644 index 9e77b1d4e..000000000 --- a/core/bindings/LibraryState.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export interface LibraryState { library_uuid: string, library_id: number, library_path: string, offline: boolean, } \ No newline at end of file diff --git a/core/bindings/LocationResource.ts b/core/bindings/LocationResource.ts deleted file mode 100644 index c9fbfa335..000000000 --- a/core/bindings/LocationResource.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { LibraryNode } from "./LibraryNode"; - -export interface LocationResource { id: number, name: string | null, path: string | null, total_capacity: number | null, available_capacity: number | null, is_removable: boolean | null, node: LibraryNode | null, is_online: boolean, date_created: string, } \ No newline at end of file diff --git a/core/bindings/NodeState.ts b/core/bindings/NodeState.ts deleted file mode 100644 index 6fc2d5c22..000000000 --- a/core/bindings/NodeState.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { LibraryState } from "./LibraryState"; - -export interface NodeState { node_pub_id: string, node_id: number, node_name: string, data_path: string, tcp_port: number, libraries: Array, current_library_uuid: string, } \ No newline at end of file diff --git a/core/bindings/Platform.ts b/core/bindings/Platform.ts deleted file mode 100644 index 30f10773a..000000000 --- a/core/bindings/Platform.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type Platform = "Unknown" | "Windows" | "MacOS" | "Linux" | "IOS" | "Android"; \ No newline at end of file diff --git a/core/bindings/Statistics.ts b/core/bindings/Statistics.ts deleted file mode 100644 index 503cd208c..000000000 --- a/core/bindings/Statistics.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export interface Statistics { total_file_count: number, total_bytes_used: string, total_bytes_capacity: string, total_bytes_free: string, total_unique_bytes: string, preview_media_bytes: string, library_db_size: string, } \ No newline at end of file diff --git a/core/bindings/Volume.ts b/core/bindings/Volume.ts deleted file mode 100644 index e40b28748..000000000 --- a/core/bindings/Volume.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export interface Volume { name: string, mount_point: string, total_capacity: bigint, available_capacity: bigint, is_removable: boolean, disk_type: string | null, file_system: string | null, is_root_filesystem: boolean, } \ No newline at end of file diff --git a/core/derive/Cargo.toml b/core/derive/Cargo.toml deleted file mode 100644 index dea7baf4a..000000000 --- a/core/derive/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "core-derive" -version = "0.1.0" -edition = "2021" - -[lib] -proc-macro = true - -[dependencies] -quote = "1.0.18" -syn = "1.0.91" \ No newline at end of file diff --git a/core/derive/src/lib.rs b/core/derive/src/lib.rs deleted file mode 100644 index 2d107dab2..000000000 --- a/core/derive/src/lib.rs +++ /dev/null @@ -1,42 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, Data, DeriveInput}; - -/// This macro must be executed in a file with `PropertyOperationCtx` defined and in the same package as the SyncContext is defined. -/// The creates: -/// ```rust -/// impl PropertyOperation { -/// fn apply(operation: PropertyOperationCtx, ctx: SyncContext) { -/// match operation.resource { -/// PropertyOperation::Tag(method) => method.apply(ctx), -/// }; -/// } -/// } -/// ``` -#[proc_macro_derive(PropertyOperationApply)] -pub fn property_operation_apply(input: TokenStream) -> TokenStream { - let DeriveInput { ident, data, .. } = parse_macro_input!(input); - - if let Data::Enum(data) = data { - let impls = data.variants.iter().map(|variant| { - let variant_ident = &variant.ident; - quote! { - #ident::#variant_ident(method) => method.apply(ctx), - } - }); - - let expanded = quote! { - impl #ident { - fn apply(operation: CrdtCtx, ctx: self::engine::SyncContext) { - match operation.resource { - #(#impls)* - }; - } - } - }; - - TokenStream::from(expanded) - } else { - panic!("The 'PropertyOperationApply' macro can only be used on enums!"); - } -} diff --git a/core/index.ts b/core/index.ts index 85eee6629..7114e01b6 100644 --- a/core/index.ts +++ b/core/index.ts @@ -1,21 +1,117 @@ -export * from './bindings/Client'; -export * from './bindings/ClientCommand'; -export * from './bindings/ClientQuery'; -export * from './bindings/ClientState'; -export * from './bindings/CoreEvent'; -export * from './bindings/CoreResource'; -export * from './bindings/CoreResponse'; -export * from './bindings/DirectoryWithContents'; -export * from './bindings/EncryptionAlgorithm'; -export * from './bindings/File'; -export * from './bindings/FileKind'; -export * from './bindings/FilePath'; -export * from './bindings/JobReport'; -export * from './bindings/JobStatus'; -export * from './bindings/LibraryNode'; -export * from './bindings/LibraryState'; -export * from './bindings/LocationResource'; -export * from './bindings/NodeState'; -export * from './bindings/Platform'; -export * from './bindings/Statistics'; -export * from './bindings/Volume'; +// This file was generated by [rspc](https://github.com/oscartbeaumont/rspc). Do not edit this file manually. + +export type Operations = { + queries: + { key: ["library.getStatistics", LibraryArgs], result: Statistics } | + { key: ["jobs.getRunning", LibraryArgs], result: Array } | + { key: ["version"], result: string } | + { key: ["files.readMetadata", LibraryArgs], result: null } | + { key: ["locations.getExplorerDir", LibraryArgs], result: DirectoryWithContents } | + { key: ["jobs.getHistory", LibraryArgs], result: Array } | + { key: ["library.get"], result: Array } | + { key: ["volumes.get"], result: Array } | + { key: ["tags.getFilesForTag", LibraryArgs], result: Tag | null } | + { key: ["locations.get", LibraryArgs], result: Array } | + { key: ["locations.getById", LibraryArgs], result: Location | null } | + { key: ["tags.get", LibraryArgs], result: Array } | + { key: ["getNode"], result: NodeState }, + mutations: + { key: ["tags.create", LibraryArgs], result: Tag } | + { key: ["files.setFavorite", LibraryArgs], result: null } | + { key: ["jobs.identifyUniqueFiles", LibraryArgs], result: null } | + { key: ["files.delete", LibraryArgs], result: null } | + { key: ["library.edit", EditLibraryArgs], result: null } | + { key: ["library.delete", string], result: null } | + { key: ["jobs.generateThumbsForLocation", LibraryArgs], result: null } | + { key: ["files.setNote", LibraryArgs], result: null } | + { key: ["library.create", string], result: null } | + { key: ["locations.quickRescan", LibraryArgs], result: null } | + { key: ["locations.delete", LibraryArgs], result: null } | + { key: ["tags.update", LibraryArgs], result: null } | + { key: ["tags.assign", LibraryArgs], result: null } | + { key: ["locations.create", LibraryArgs], result: Location } | + { key: ["locations.update", LibraryArgs], result: null } | + { key: ["locations.fullRescan", LibraryArgs], result: null } | + { key: ["tags.delete", LibraryArgs], result: null }, + subscriptions: + { key: ["jobs.newThumbnail", LibraryArgs], result: string } | + { key: ["invalidateQuery"], result: InvalidateOperationEvent } +}; + +export interface TagCreateArgs { name: string, color: string } + +export interface Location { id: number, pub_id: Array, node_id: number | null, name: string | null, local_path: string | null, total_capacity: number | null, available_capacity: number | null, filesystem: string | null, disk_type: number | null, is_removable: boolean | null, is_online: boolean, date_created: string, node: Node | null | null, file_paths: Array | null } + +export interface File { id: number, cas_id: string, integrity_checksum: string | null, kind: number, size_in_bytes: string, key_id: number | null, hidden: boolean, favorite: boolean, important: boolean, has_thumbnail: boolean, has_thumbstrip: boolean, has_video_preview: boolean, ipfs_id: string | null, note: string | null, date_created: string, date_modified: string, date_indexed: string, tags: Array | null, labels: Array | null, albums: Array | null, spaces: Array | null, paths: Array | null, comments: Array | null, media_data: MediaData | null | null, key: Key | null | null } + +export interface EditLibraryArgs { id: string, name: string | null, description: string | null } + +export interface LibraryArgs { library_id: string, arg: T } + +export interface NodeConfig { version: string | null, id: string, name: string, p2p_port: number | null } + +export interface MediaData { id: number, pixel_width: number | null, pixel_height: number | null, longitude: number | null, latitude: number | null, fps: number | null, capture_device_make: string | null, capture_device_model: string | null, capture_device_software: string | null, duration_seconds: number | null, codecs: string | null, streams: number | null, files: File | null | null } + +export interface Space { id: number, pub_id: Array, name: string | null, description: string | null, date_created: string, date_modified: string, files: Array | null } + +export interface Album { id: number, pub_id: Array, name: string, is_hidden: boolean, date_created: string, date_modified: string, files: Array | null } + +export interface LabelOnFile { date_created: string, label_id: number, label: Label | null, file_id: number, file: File | null } + +export interface JobReport { id: string, name: string, data: Array | null, date_created: string, date_modified: string, status: JobStatus, task_count: number, completed_task_count: number, message: string, seconds_elapsed: number } + +export interface FilePath { id: number, is_dir: boolean, location_id: number | null, materialized_path: string, name: string, extension: string | null, file_id: number | null, parent_id: number | null, key_id: number | null, date_created: string, date_modified: string, date_indexed: string, file: File | null | null, location: Location | null | null, key: Key | null | null } + +export interface LocationUpdateArgs { id: number, name: string | null } + +export type JobStatus = "Queued" | "Running" | "Completed" | "Canceled" | "Failed" | "Paused" + +export interface Node { id: number, pub_id: Array, name: string, platform: number, version: string | null, last_seen: string, timezone: string | null, date_created: string, sync_events: Array | null, jobs: Array | null, Location: Array | null } + +export interface Key { id: number, checksum: string, name: string | null, date_created: string | null, algorithm: number | null, files: Array | null, file_paths: Array | null } + +export interface SyncEvent { id: number, node_id: number, timestamp: string, record_id: Array, kind: number, column: string | null, value: string, node: Node | null } + +export interface SetFavoriteArgs { id: number, favorite: boolean } + +export interface InvalidateOperationEvent { key: string, arg: any } + +export interface NodeState { version: string | null, id: string, name: string, p2p_port: number | null, data_path: string } + +export interface ConfigMetadata { version: string | null } + +export interface Volume { name: string, mount_point: string, total_capacity: bigint, available_capacity: bigint, is_removable: boolean, disk_type: string | null, file_system: string | null, is_root_filesystem: boolean } + +export interface FileInAlbum { date_created: string, album_id: number, album: Album | null, file_id: number, file: File | null } + +export interface Comment { id: number, pub_id: Array, content: string, date_created: string, date_modified: string, file_id: number | null, file: File | null | null } + +export interface LibraryConfig { version: string | null, name: string, description: string } + +export interface TagUpdateArgs { id: number, name: string | null, color: string | null } + +export interface TagAssignArgs { file_id: number, tag_id: number } + +export interface Statistics { id: number, date_captured: string, total_file_count: number, library_db_size: string, total_bytes_used: string, total_bytes_capacity: string, total_unique_bytes: string, total_bytes_free: string, preview_media_bytes: string } + +export interface GenerateThumbsForLocationArgs { id: number, path: string } + +export interface SetNoteArgs { id: number, note: string | null } + +export interface Job { id: Array, name: string, node_id: number, action: number, status: number, data: Array | null, task_count: number, completed_task_count: number, date_created: string, date_modified: string, seconds_elapsed: number, nodes: Node | null } + +export interface GetExplorerDirArgs { location_id: number, path: string, limit: number } + +export interface Label { id: number, pub_id: Array, name: string | null, date_created: string, date_modified: string, label_files: Array | null } + +export interface LibraryConfigWrapped { uuid: string, config: LibraryConfig } + +export interface TagOnFile { date_created: string, tag_id: number, tag: Tag | null, file_id: number, file: File | null } + +export interface Tag { id: number, pub_id: Array, name: string | null, color: string | null, total_files: number | null, redundancy_goal: number | null, date_created: string, date_modified: string, tag_files: Array | null } + +export interface IdentifyUniqueFilesArgs { id: number, path: string } + +export interface DirectoryWithContents { directory: FilePath, contents: Array } + +export interface FileInSpace { date_created: string, space_id: number, space: Space | null, file_id: number, file: File | null } diff --git a/core/package.json b/core/package.json index 433da6483..c05d412fe 100644 --- a/core/package.json +++ b/core/package.json @@ -4,15 +4,14 @@ "main": "index.js", "license": "GPL-3.0-only", "scripts": { - "codegen": "cargo test && ts-node ./scripts/bindingsIndex.ts", + "codegen": "cargo test", "build": "cargo build", "test": "cargo test", "test:log": "cargo test -- --nocapture", "prisma": "cargo prisma" }, "devDependencies": { - "@types/node": "^17.0.36", - "ts-node": "^10.8.0", - "typescript": "^4.7.2" + "@types/node": "^18.6.1", + "typescript": "^4.7.4" } } diff --git a/core/prisma/Cargo.toml b/core/prisma/Cargo.toml index 9ed9a2062..b24c05939 100644 --- a/core/prisma/Cargo.toml +++ b/core/prisma/Cargo.toml @@ -4,4 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.5.0" } \ No newline at end of file +prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust.git", rev = "6a0119bce951c8d956542a59b2f783fc5a591fc7", features = [ + "rspc", + "sqlite-create-many", +] } diff --git a/core/prisma/migrations/20220625180107_remove_library/migration.sql b/core/prisma/migrations/20220625180107_remove_library/migration.sql new file mode 100644 index 000000000..63e4f056f --- /dev/null +++ b/core/prisma/migrations/20220625180107_remove_library/migration.sql @@ -0,0 +1,29 @@ +/* + Warnings: + + - You are about to drop the `libraries` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `library_statistics` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "libraries"; +PRAGMA foreign_keys=on; + +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "library_statistics"; +PRAGMA foreign_keys=on; + +-- CreateTable +CREATE TABLE "statistics" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "date_captured" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "total_file_count" INTEGER NOT NULL DEFAULT 0, + "library_db_size" TEXT NOT NULL DEFAULT '0', + "total_bytes_used" TEXT NOT NULL DEFAULT '0', + "total_bytes_capacity" TEXT NOT NULL DEFAULT '0', + "total_unique_bytes" TEXT NOT NULL DEFAULT '0', + "total_bytes_free" TEXT NOT NULL DEFAULT '0', + "preview_media_bytes" TEXT NOT NULL DEFAULT '0' +); diff --git a/core/prisma/migrations/20220715031021_added_spaces/migration.sql b/core/prisma/migrations/20220715031021_added_spaces/migration.sql new file mode 100644 index 000000000..672e3d89d --- /dev/null +++ b/core/prisma/migrations/20220715031021_added_spaces/migration.sql @@ -0,0 +1,62 @@ +/* + Warnings: + + - You are about to drop the `label_on_files` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `tags_on_files` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "label_on_files"; +PRAGMA foreign_keys=on; + +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "tags_on_files"; +PRAGMA foreign_keys=on; + +-- CreateTable +CREATE TABLE "tags_on_file" ( + "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "tag_id" INTEGER NOT NULL, + "file_id" INTEGER NOT NULL, + + PRIMARY KEY ("tag_id", "file_id"), + CONSTRAINT "tags_on_file_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, + CONSTRAINT "tags_on_file_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "tags" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION +); + +-- CreateTable +CREATE TABLE "label_on_file" ( + "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "label_id" INTEGER NOT NULL, + "file_id" INTEGER NOT NULL, + + PRIMARY KEY ("label_id", "file_id"), + CONSTRAINT "label_on_file_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, + CONSTRAINT "label_on_file_label_id_fkey" FOREIGN KEY ("label_id") REFERENCES "labels" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION +); + +-- CreateTable +CREATE TABLE "spaces" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "pub_id" TEXT NOT NULL, + "name" TEXT, + "description" TEXT, + "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateTable +CREATE TABLE "file_in_space" ( + "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "space_id" INTEGER NOT NULL, + "file_id" INTEGER NOT NULL, + + PRIMARY KEY ("space_id", "file_id"), + CONSTRAINT "file_in_space_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, + CONSTRAINT "file_in_space_space_id_fkey" FOREIGN KEY ("space_id") REFERENCES "spaces" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION +); + +-- CreateIndex +CREATE UNIQUE INDEX "spaces_pub_id_key" ON "spaces"("pub_id"); diff --git a/core/prisma/migrations/20220716023638_tags_color/migration.sql b/core/prisma/migrations/20220716023638_tags_color/migration.sql new file mode 100644 index 000000000..72d3fb7b9 --- /dev/null +++ b/core/prisma/migrations/20220716023638_tags_color/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "tags" ADD COLUMN "color" TEXT; diff --git a/core/prisma/migrations/20220722181530_alter_uuids_to_bytes/migration.sql b/core/prisma/migrations/20220722181530_alter_uuids_to_bytes/migration.sql new file mode 100644 index 000000000..fb53d5d65 --- /dev/null +++ b/core/prisma/migrations/20220722181530_alter_uuids_to_bytes/migration.sql @@ -0,0 +1,145 @@ +/* + Warnings: + + - The primary key for the `jobs` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to alter the column `data` on the `jobs` table. The data in that column could be lost. The data in that column will be cast from `String` to `Binary`. + - You are about to alter the column `id` on the `jobs` table. The data in that column could be lost. The data in that column will be cast from `String` to `Binary`. + - You are about to alter the column `pub_id` on the `nodes` table. The data in that column could be lost. The data in that column will be cast from `String` to `Binary`. + - You are about to alter the column `pub_id` on the `tags` table. The data in that column could be lost. The data in that column will be cast from `String` to `Binary`. + - You are about to alter the column `pub_id` on the `labels` table. The data in that column could be lost. The data in that column will be cast from `String` to `Binary`. + - You are about to alter the column `pub_id` on the `spaces` table. The data in that column could be lost. The data in that column will be cast from `String` to `Binary`. + - You are about to alter the column `pub_id` on the `locations` table. The data in that column could be lost. The data in that column will be cast from `String` to `Binary`. + - You are about to alter the column `record_id` on the `sync_events` table. The data in that column could be lost. The data in that column will be cast from `String` to `Binary`. + - You are about to alter the column `pub_id` on the `albums` table. The data in that column could be lost. The data in that column will be cast from `String` to `Binary`. + - You are about to alter the column `pub_id` on the `comments` table. The data in that column could be lost. The data in that column will be cast from `String` to `Binary`. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_jobs" ( + "id" BLOB NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "node_id" INTEGER NOT NULL, + "action" INTEGER NOT NULL, + "status" INTEGER NOT NULL DEFAULT 0, + "data" BLOB, + "task_count" INTEGER NOT NULL DEFAULT 1, + "completed_task_count" INTEGER NOT NULL DEFAULT 0, + "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "seconds_elapsed" INTEGER NOT NULL DEFAULT 0, + CONSTRAINT "jobs_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "nodes" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_jobs" ("action", "completed_task_count", "data", "date_created", "date_modified", "id", "name", "node_id", "seconds_elapsed", "status", "task_count") SELECT "action", "completed_task_count", "data", "date_created", "date_modified", "id", "name", "node_id", "seconds_elapsed", "status", "task_count" FROM "jobs"; +DROP TABLE "jobs"; +ALTER TABLE "new_jobs" RENAME TO "jobs"; +CREATE TABLE "new_nodes" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "pub_id" BLOB NOT NULL, + "name" TEXT NOT NULL, + "platform" INTEGER NOT NULL DEFAULT 0, + "version" TEXT, + "last_seen" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "timezone" TEXT, + "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +INSERT INTO "new_nodes" ("date_created", "id", "last_seen", "name", "platform", "pub_id", "timezone", "version") SELECT "date_created", "id", "last_seen", "name", "platform", "pub_id", "timezone", "version" FROM "nodes"; +DROP TABLE "nodes"; +ALTER TABLE "new_nodes" RENAME TO "nodes"; +CREATE UNIQUE INDEX "nodes_pub_id_key" ON "nodes"("pub_id"); +CREATE TABLE "new_tags" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "pub_id" BLOB NOT NULL, + "name" TEXT, + "color" TEXT, + "total_files" INTEGER DEFAULT 0, + "redundancy_goal" INTEGER DEFAULT 1, + "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +INSERT INTO "new_tags" ("color", "date_created", "date_modified", "id", "name", "pub_id", "redundancy_goal", "total_files") SELECT "color", "date_created", "date_modified", "id", "name", "pub_id", "redundancy_goal", "total_files" FROM "tags"; +DROP TABLE "tags"; +ALTER TABLE "new_tags" RENAME TO "tags"; +CREATE UNIQUE INDEX "tags_pub_id_key" ON "tags"("pub_id"); +CREATE TABLE "new_labels" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "pub_id" BLOB NOT NULL, + "name" TEXT, + "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +INSERT INTO "new_labels" ("date_created", "date_modified", "id", "name", "pub_id") SELECT "date_created", "date_modified", "id", "name", "pub_id" FROM "labels"; +DROP TABLE "labels"; +ALTER TABLE "new_labels" RENAME TO "labels"; +CREATE UNIQUE INDEX "labels_pub_id_key" ON "labels"("pub_id"); +CREATE TABLE "new_spaces" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "pub_id" BLOB NOT NULL, + "name" TEXT, + "description" TEXT, + "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +INSERT INTO "new_spaces" ("date_created", "date_modified", "description", "id", "name", "pub_id") SELECT "date_created", "date_modified", "description", "id", "name", "pub_id" FROM "spaces"; +DROP TABLE "spaces"; +ALTER TABLE "new_spaces" RENAME TO "spaces"; +CREATE UNIQUE INDEX "spaces_pub_id_key" ON "spaces"("pub_id"); +CREATE TABLE "new_locations" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "pub_id" BLOB NOT NULL, + "node_id" INTEGER, + "name" TEXT, + "local_path" TEXT, + "total_capacity" INTEGER, + "available_capacity" INTEGER, + "filesystem" TEXT, + "disk_type" INTEGER, + "is_removable" BOOLEAN, + "is_online" BOOLEAN NOT NULL DEFAULT true, + "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "locations_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "nodes" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_locations" ("available_capacity", "date_created", "disk_type", "filesystem", "id", "is_online", "is_removable", "local_path", "name", "node_id", "pub_id", "total_capacity") SELECT "available_capacity", "date_created", "disk_type", "filesystem", "id", "is_online", "is_removable", "local_path", "name", "node_id", "pub_id", "total_capacity" FROM "locations"; +DROP TABLE "locations"; +ALTER TABLE "new_locations" RENAME TO "locations"; +CREATE UNIQUE INDEX "locations_pub_id_key" ON "locations"("pub_id"); +CREATE TABLE "new_sync_events" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "node_id" INTEGER NOT NULL, + "timestamp" TEXT NOT NULL, + "record_id" BLOB NOT NULL, + "kind" INTEGER NOT NULL, + "column" TEXT, + "value" TEXT NOT NULL, + CONSTRAINT "sync_events_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "nodes" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_sync_events" ("column", "id", "kind", "node_id", "record_id", "timestamp", "value") SELECT "column", "id", "kind", "node_id", "record_id", "timestamp", "value" FROM "sync_events"; +DROP TABLE "sync_events"; +ALTER TABLE "new_sync_events" RENAME TO "sync_events"; +CREATE TABLE "new_albums" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "pub_id" BLOB NOT NULL, + "name" TEXT NOT NULL, + "is_hidden" BOOLEAN NOT NULL DEFAULT false, + "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +INSERT INTO "new_albums" ("date_created", "date_modified", "id", "is_hidden", "name", "pub_id") SELECT "date_created", "date_modified", "id", "is_hidden", "name", "pub_id" FROM "albums"; +DROP TABLE "albums"; +ALTER TABLE "new_albums" RENAME TO "albums"; +CREATE UNIQUE INDEX "albums_pub_id_key" ON "albums"("pub_id"); +CREATE TABLE "new_comments" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "pub_id" BLOB NOT NULL, + "content" TEXT NOT NULL, + "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "file_id" INTEGER, + CONSTRAINT "comments_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_comments" ("content", "date_created", "date_modified", "file_id", "id", "pub_id") SELECT "content", "date_created", "date_modified", "file_id", "id", "pub_id" FROM "comments"; +DROP TABLE "comments"; +ALTER TABLE "new_comments" RENAME TO "comments"; +CREATE UNIQUE INDEX "comments_pub_id_key" ON "comments"("pub_id"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/core/prisma/schema.prisma b/core/prisma/schema.prisma index e8f911004..5808ff3e6 100644 --- a/core/prisma/schema.prisma +++ b/core/prisma/schema.prisma @@ -23,7 +23,7 @@ model SyncEvent { node_id Int timestamp String // individual record pub id OR compound many-to-many pub ids - record_id String + record_id Bytes // the type of operation, I.E: CREATE, UPDATE, DELETE as an enum kind Int // the column name for atomic update operations @@ -35,21 +35,9 @@ model SyncEvent { @@map("sync_events") } -model Library { - id Int @id @default(autoincrement()) - pub_id String @unique - name String - is_primary Boolean @default(true) - date_created DateTime @default(now()) - timezone String? - - @@map("libraries") -} - -model LibraryStatistics { +model Statistics { id Int @id @default(autoincrement()) date_captured DateTime @default(now()) - library_id Int @unique total_file_count Int @default(0) library_db_size String @default("0") total_bytes_used String @default("0") @@ -58,12 +46,12 @@ model LibraryStatistics { total_bytes_free String @default("0") preview_media_bytes String @default("0") - @@map("library_statistics") + @@map("statistics") } model Node { id Int @id @default(autoincrement()) - pub_id String @unique + pub_id Bytes @unique name String platform Int @default(0) version String? @@ -79,7 +67,7 @@ model Node { } model Volume { - id Int @id() @default(autoincrement()) + id Int @id @default(autoincrement()) node_id Int name String mount_point String @@ -96,7 +84,7 @@ model Volume { model Location { id Int @id @default(autoincrement()) - pub_id String @unique + pub_id Bytes @unique node_id Int? name String? local_path String? @@ -145,6 +133,7 @@ model File { tags TagOnFile[] labels LabelOnFile[] albums FileInAlbum[] + spaces FileInSpace[] paths FilePath[] comments Comment[] media_data MediaData? @@ -237,8 +226,9 @@ model MediaData { model Tag { id Int @id @default(autoincrement()) - pub_id String @unique + pub_id Bytes @unique name String? + color String? total_files Int? @default(0) redundancy_goal Int? @default(1) date_created DateTime @default(now()) @@ -258,12 +248,12 @@ model TagOnFile { file File @relation(fields: [file_id], references: [id], onDelete: NoAction, onUpdate: NoAction) @@id([tag_id, file_id]) - @@map("tags_on_files") + @@map("tags_on_file") } model Label { id Int @id @default(autoincrement()) - pub_id String @unique + pub_id Bytes @unique name String? date_created DateTime @default(now()) date_modified DateTime @default(now()) @@ -282,16 +272,41 @@ model LabelOnFile { file File @relation(fields: [file_id], references: [id], onDelete: NoAction, onUpdate: NoAction) @@id([label_id, file_id]) - @@map("label_on_files") + @@map("label_on_file") +} + +model Space { + id Int @id @default(autoincrement()) + pub_id Bytes @unique + name String? + description String? + date_created DateTime @default(now()) + date_modified DateTime @default(now()) + + files FileInSpace[] + @@map("spaces") +} + +model FileInSpace { + date_created DateTime @default(now()) + + space_id Int + space Space @relation(fields: [space_id], references: [id], onDelete: NoAction, onUpdate: NoAction) + + file_id Int + file File @relation(fields: [file_id], references: [id], onDelete: NoAction, onUpdate: NoAction) + + @@id([space_id, file_id]) + @@map("file_in_space") } model Job { - id String @id + id Bytes @id name String node_id Int action Int status Int @default(0) - data String? + data Bytes? task_count Int @default(1) completed_task_count Int @default(0) @@ -305,7 +320,7 @@ model Job { model Album { id Int @id @default(autoincrement()) - pub_id String @unique + pub_id Bytes @unique name String is_hidden Boolean @default(false) @@ -332,7 +347,7 @@ model FileInAlbum { model Comment { id Int @id @default(autoincrement()) - pub_id String @unique + pub_id Bytes @unique content String date_created DateTime @default(now()) date_modified DateTime @default(now()) diff --git a/core/src/api/files.rs b/core/src/api/files.rs new file mode 100644 index 000000000..e8329c6ac --- /dev/null +++ b/core/src/api/files.rs @@ -0,0 +1,105 @@ +use rspc::Type; +use serde::Deserialize; + +use crate::{api::locations::GetExplorerDirArgs, invalidate_query, prisma::file}; + +use super::{LibraryArgs, RouterBuilder}; + +#[derive(Type, Deserialize)] +pub struct SetNoteArgs { + pub id: i32, + pub note: Option, +} + +#[derive(Type, Deserialize)] +pub struct SetFavoriteArgs { + pub id: i32, + pub favorite: bool, +} + +pub(crate) fn mount() -> RouterBuilder { + ::new() + .query("readMetadata", |_ctx, _id: LibraryArgs| todo!()) + .mutation("setNote", |ctx, arg: LibraryArgs| async move { + let (args, library) = arg.get_library(&ctx).await?; + + library + .db + .file() + .update(file::id::equals(args.id), vec![file::note::set(args.note)]) + .exec() + .await?; + + invalidate_query!( + library, + "locations.getExplorerDir": LibraryArgs, + LibraryArgs { + library_id: library.id, + arg: GetExplorerDirArgs { + location_id: 0, // TODO: This should be the correct location_id + path: "".into(), + limit: 0, + } + } + ); + + Ok(()) + }) + .mutation( + "setFavorite", + |ctx, arg: LibraryArgs| async move { + let (args, library) = arg.get_library(&ctx).await?; + + library + .db + .file() + .update( + file::id::equals(args.id), + vec![file::favorite::set(args.favorite)], + ) + .exec() + .await?; + + invalidate_query!( + library, + "locations.getExplorerDir": LibraryArgs, + LibraryArgs { + library_id: library.id, + arg: GetExplorerDirArgs { + // TODO: Set these arguments to the correct type + location_id: 0, + path: "".into(), + limit: 0, + } + } + ); + + Ok(()) + }, + ) + .mutation("delete", |ctx, arg: LibraryArgs| async move { + let (id, library) = arg.get_library(&ctx).await?; + + library + .db + .file() + .delete(file::id::equals(id)) + .exec() + .await?; + + invalidate_query!( + library, + "locations.getExplorerDir": LibraryArgs, + LibraryArgs { + library_id: library.id, + arg: GetExplorerDirArgs { + // TODO: Set these arguments to the correct type + location_id: 0, + path: "".into(), + limit: 0, + } + } + ); + Ok(()) + }) +} diff --git a/core/src/api/jobs.rs b/core/src/api/jobs.rs new file mode 100644 index 000000000..675a7a686 --- /dev/null +++ b/core/src/api/jobs.rs @@ -0,0 +1,88 @@ +use std::path::PathBuf; + +use rspc::Type; +use serde::Deserialize; + +use crate::{ + encode::{ThumbnailJob, ThumbnailJobInit}, + file::cas::{FileIdentifierJob, FileIdentifierJobInit}, + job::{Job, JobManager}, +}; + +use super::{CoreEvent, LibraryArgs, RouterBuilder}; + +#[derive(Type, Deserialize)] +pub struct GenerateThumbsForLocationArgs { + pub id: i32, + pub path: PathBuf, +} + +#[derive(Type, Deserialize)] +pub struct IdentifyUniqueFilesArgs { + pub id: i32, + pub path: PathBuf, +} + +pub(crate) fn mount() -> RouterBuilder { + ::new() + .query("getRunning", |ctx, arg: LibraryArgs<()>| async move { + let (_, _) = arg.get_library(&ctx).await?; + + Ok(ctx.jobs.get_running().await) + }) + .query("getHistory", |ctx, arg: LibraryArgs<()>| async move { + let (_, library) = arg.get_library(&ctx).await?; + + Ok(JobManager::get_history(&library).await?) + }) + .mutation( + "generateThumbsForLocation", + |ctx, arg: LibraryArgs| async move { + let (args, library) = arg.get_library(&ctx).await?; + + library + .spawn_job(Job::new( + ThumbnailJobInit { + location_id: args.id, + path: args.path, + background: false, // fix + }, + Box::new(ThumbnailJob {}), + )) + .await; + + Ok(()) + }, + ) + .mutation( + "identifyUniqueFiles", + |ctx, arg: LibraryArgs| async move { + let (args, library) = arg.get_library(&ctx).await?; + + library + .spawn_job(Job::new( + FileIdentifierJobInit { + location_id: args.id, + path: args.path, + }, + Box::new(FileIdentifierJob {}), + )) + .await; + + Ok(()) + }, + ) + .subscription("newThumbnail", |ctx, _: LibraryArgs<()>| { + // TODO: Only return event for the library that was subscribed to + + let mut event_bus_rx = ctx.event_bus.subscribe(); + async_stream::stream! { + while let Ok(event) = event_bus_rx.recv().await { + match event { + CoreEvent::NewThumbnail { cas_id } => yield cas_id, + _ => {} + } + } + } + }) +} diff --git a/core/src/api/libraries.rs b/core/src/api/libraries.rs new file mode 100644 index 000000000..f2135b1fa --- /dev/null +++ b/core/src/api/libraries.rs @@ -0,0 +1,101 @@ +use chrono::Utc; +use fs_extra::dir::get_size; +use rspc::Type; +use serde::Deserialize; +use tokio::fs; +use uuid::Uuid; + +use crate::{ + library::LibraryConfig, + prisma::statistics, + sys::{get_volumes, save_volume}, +}; + +use super::{LibraryArgs, RouterBuilder}; + +#[derive(Type, Deserialize)] +pub struct EditLibraryArgs { + pub id: Uuid, + pub name: Option, + pub description: Option, +} + +pub(crate) fn mount() -> RouterBuilder { + ::new() + .query("get", |ctx, _: ()| async move { + ctx.library_manager.get_all_libraries_config().await + }) + .query("getStatistics", |ctx, arg: LibraryArgs<()>| async move { + let (_, library) = arg.get_library(&ctx).await?; + + let _statistics = library + .db + .statistics() + .find_unique(statistics::id::equals(library.node_local_id)) + .exec() + .await?; + + // TODO: get from database, not sys + let volumes = get_volumes(); + save_volume(&library).await?; + + let mut available_capacity: u64 = 0; + let mut total_capacity: u64 = 0; + if volumes.is_ok() { + for volume in volumes? { + total_capacity += volume.total_capacity; + available_capacity += volume.available_capacity; + } + } + + let library_db_size = match fs::metadata(library.config().data_directory()).await { + Ok(metadata) => metadata.len(), + Err(_) => 0, + }; + + let thumbnail_folder_size = + get_size(library.config().data_directory().join("thumbnails")); + + use statistics::*; + let params = vec![ + id::set(1), // Each library is a database so only one of these ever exists + date_captured::set(Utc::now().into()), + total_file_count::set(0), + library_db_size::set(library_db_size.to_string()), + total_bytes_used::set(0.to_string()), + total_bytes_capacity::set(total_capacity.to_string()), + total_unique_bytes::set(0.to_string()), + total_bytes_free::set(available_capacity.to_string()), + preview_media_bytes::set(thumbnail_folder_size.unwrap_or(0).to_string()), + ]; + + Ok(library + .db + .statistics() + .upsert( + statistics::id::equals(1), // Each library is a database so only one of these ever exists + params.clone(), + params, + ) + .exec() + .await?) + }) + .mutation("create", |ctx, name: String| async move { + Ok(ctx + .library_manager + .create(LibraryConfig { + name: name.to_string(), + ..Default::default() + }) + .await?) + }) + .mutation("edit", |ctx, args: EditLibraryArgs| async move { + Ok(ctx + .library_manager + .edit(args.id, args.name, args.description) + .await?) + }) + .mutation("delete", |ctx, id: Uuid| async move { + Ok(ctx.library_manager.delete_library(id).await?) + }) +} diff --git a/core/src/api/locations.rs b/core/src/api/locations.rs new file mode 100644 index 000000000..d690597f5 --- /dev/null +++ b/core/src/api/locations.rs @@ -0,0 +1,184 @@ +use std::path::PathBuf; + +use rspc::{Error, ErrorCode, Type}; +use serde::{Deserialize, Serialize}; +use tracing::info; + +use crate::{ + encode::THUMBNAIL_CACHE_DIR_NAME, + invalidate_query, + prisma::{file_path, location}, + sys::{self, create_location, scan_location}, +}; + +use super::{LibraryArgs, RouterBuilder}; + +#[derive(Serialize, Deserialize, Type, Debug)] +pub struct DirectoryWithContents { + pub directory: file_path::Data, + pub contents: Vec, +} + +#[derive(Type, Deserialize)] +pub struct LocationUpdateArgs { + pub id: i32, + pub name: Option, +} + +#[derive(Clone, Serialize, Deserialize, Type)] +pub struct GetExplorerDirArgs { + pub location_id: i32, + pub path: String, + pub limit: i32, +} + +pub(crate) fn mount() -> RouterBuilder { + ::new() + .query("get", |ctx, arg: LibraryArgs<()>| async move { + let (_, library) = arg.get_library(&ctx).await?; + + let locations = library + .db + .location() + .find_many(vec![]) + .with(location::node::fetch()) + .exec() + .await?; + + Ok(locations) + }) + .query("getById", |ctx, arg: LibraryArgs| async move { + let (location_id, library) = arg.get_library(&ctx).await?; + + Ok(library + .db + .location() + .find_unique(location::id::equals(location_id)) + .exec() + .await?) + }) + .query( + "getExplorerDir", + |ctx, arg: LibraryArgs| async move { + let (args, library) = arg.get_library(&ctx).await?; + + let location = library + .db + .location() + .find_unique(location::id::equals(args.location_id)) + .exec() + .await? + .unwrap(); + + let directory = library + .db + .file_path() + .find_first(vec![ + file_path::location_id::equals(Some(location.id)), + file_path::materialized_path::equals(args.path), + file_path::is_dir::equals(true), + ]) + .exec() + .await? + .ok_or_else(|| Error::new(ErrorCode::NotFound, "Directory not found".into()))?; + + let file_paths = library + .db + .file_path() + .find_many(vec![ + file_path::location_id::equals(Some(location.id)), + file_path::parent_id::equals(Some(directory.id)), + ]) + .with(file_path::file::fetch()) + .exec() + .await?; + + Ok(DirectoryWithContents { + directory, + contents: file_paths + .into_iter() + .map(|mut file_path| { + if let Some(file) = &mut file_path.file.as_mut().unwrap_or_else( + || /* Prisma relationship was not fetched */ unreachable!(), + ) { + // TODO: Use helper function to build this url as as the Rust file loading layer + let thumb_path = library + .config() + .data_directory() + .join(THUMBNAIL_CACHE_DIR_NAME) + .join(location.id.to_string()) + .join(&file.cas_id) + .with_extension("webp"); + + file.has_thumbnail = thumb_path.exists(); + } + + file_path + }) + .collect(), + }) + }, + ) + .mutation("create", |ctx, arg: LibraryArgs| async move { + let (path, library) = arg.get_library(&ctx).await?; + let location = create_location(&library, &path).await?; + scan_location(&library, location.id, path).await; + Ok(location) + }) + .mutation( + "update", + |ctx, arg: LibraryArgs| async move { + let (args, library) = arg.get_library(&ctx).await?; + + library + .db + .location() + .update( + location::id::equals(args.id), + vec![location::name::set(args.name)], + ) + .exec() + .await?; + + Ok(()) + }, + ) + .mutation("delete", |ctx, arg: LibraryArgs| async move { + let (location_id, library) = arg.get_library(&ctx).await?; + + library + .db + .file_path() + .delete_many(vec![file_path::location_id::equals(Some(location_id))]) + .exec() + .await?; + + library + .db + .location() + .delete(location::id::equals(location_id)) + .exec() + .await?; + + invalidate_query!( + library, + "locations.get": LibraryArgs<()>, + LibraryArgs { + library_id: library.id, + arg: () + } + ); + + info!("Location {} deleted", location_id); + + Ok(()) + }) + .mutation("fullRescan", |ctx, arg: LibraryArgs| async move { + let (id, library) = arg.get_library(&ctx).await?; + + sys::scan_location(&library, id, String::new()).await; + + Ok(()) + }) + .mutation("quickRescan", |_, _: LibraryArgs<()>| todo!()) +} diff --git a/core/src/api/mod.rs b/core/src/api/mod.rs new file mode 100644 index 000000000..0ffa98e5a --- /dev/null +++ b/core/src/api/mod.rs @@ -0,0 +1,141 @@ +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; + +use rspc::{Config, ErrorCode, Type}; +use serde::{Deserialize, Serialize}; +use tokio::sync::broadcast; +use uuid::Uuid; + +use crate::{ + job::JobManager, + library::{LibraryContext, LibraryManager}, + node::{NodeConfig, NodeConfigManager}, +}; + +use utils::{InvalidRequests, InvalidateOperationEvent}; + +pub type Router = rspc::Router; +pub(crate) type RouterBuilder = rspc::RouterBuilder; + +/// Represents an internal core event, these are exposed to client via a rspc subscription. +#[derive(Debug, Clone, Serialize, Type)] +pub enum CoreEvent { + NewThumbnail { cas_id: String }, + InvalidateOperation(InvalidateOperationEvent), + InvalidateOperationDebounced(InvalidateOperationEvent), +} + +/// Is provided when executing the router from the request. +pub struct Ctx { + pub library_manager: Arc, + pub config: Arc, + pub jobs: Arc, + pub event_bus: broadcast::Sender, +} + +/// Can wrap a query argument to require it to contain a `library_id` and provide helpers for working with libraries. +#[derive(Clone, Serialize, Deserialize, Type)] +pub struct LibraryArgs { + // If you want to make these public, your doing it wrong. + pub library_id: Uuid, + pub arg: T, +} + +impl LibraryArgs { + pub async fn get_library(self, ctx: &Ctx) -> Result<(T, LibraryContext), rspc::Error> { + match ctx.library_manager.get_ctx(self.library_id).await { + Some(library) => Ok((self.arg, library)), + None => Err(rspc::Error::new( + ErrorCode::BadRequest, + "You must specify a valid library to use this operation.".to_string(), + )), + } + } +} + +mod files; +mod jobs; +mod libraries; +mod locations; +mod tags; +pub mod utils; +mod volumes; + +pub use files::*; +pub use jobs::*; +pub use libraries::*; +pub use tags::*; + +#[derive(Serialize, Deserialize, Debug, Type)] +struct NodeState { + #[serde(flatten)] + config: NodeConfig, + data_path: String, +} + +pub(crate) fn mount() -> Arc { + let r = ::new() + .config( + Config::new() + // TODO: This messes with Tauri's hot reload so we can't use it until their is a solution upstream. https://github.com/tauri-apps/tauri/issues/4617 + // .export_ts_bindings(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./index.ts")), + .set_ts_bindings_header("/* eslint-disable */"), + ) + .query("version", |_, _: ()| env!("CARGO_PKG_VERSION")) + .query("getNode", |ctx, _: ()| async move { + Ok(NodeState { + config: ctx.config.get().await, + // We are taking the assumption here that this value is only used on the frontend for display purposes + data_path: ctx.config.data_directory().to_string_lossy().into_owned(), + }) + }) + .merge("library.", libraries::mount()) + .merge("volumes.", volumes::mount()) + .merge("tags.", tags::mount()) + .merge("locations.", locations::mount()) + .merge("files.", files::mount()) + .merge("jobs.", jobs::mount()) + // TODO: Scope the invalidate queries to a specific library (filtered server side) + .subscription("invalidateQuery", |ctx, _: ()| { + let mut event_bus_rx = ctx.event_bus.subscribe(); + let mut last = Instant::now(); + async_stream::stream! { + while let Ok(event) = event_bus_rx.recv().await { + match event { + CoreEvent::InvalidateOperation(op) => yield op, + CoreEvent::InvalidateOperationDebounced(op) => { + let current = Instant::now(); + if current.duration_since(last) > Duration::from_millis(1000 / 60) { + last = current; + yield op; + } + }, + _ => {} + } + } + } + }) + .build() + .arced(); + InvalidRequests::validate(r.clone()); // This validates all invalidation calls. + r +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + /// This test will ensure the rspc router and all calls to `invalidate_query` are valid and also export an updated version of the Typescript bindings. + #[test] + fn test_and_export_rspc_bindings() { + let r = super::mount(); + r.export_ts(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./index.ts")) + .expect("Error exporting rspc Typescript bindings!"); + r.export_ts( + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../apps/mobile/src/types/bindings.ts"), + ) + .expect("Error exporting rspc Typescript bindings!"); + } +} diff --git a/core/src/api/tags.rs b/core/src/api/tags.rs new file mode 100644 index 000000000..b7a7996d5 --- /dev/null +++ b/core/src/api/tags.rs @@ -0,0 +1,135 @@ +use rspc::Type; +use serde::Deserialize; +use uuid::Uuid; + +use crate::{ + invalidate_query, + prisma::{file, tag}, +}; + +use super::{LibraryArgs, RouterBuilder}; + +#[derive(Type, Deserialize)] +pub struct TagCreateArgs { + pub name: String, + pub color: String, +} + +#[derive(Type, Deserialize)] +pub struct TagAssignArgs { + pub file_id: i32, + pub tag_id: i32, +} + +#[derive(Type, Deserialize)] +pub struct TagUpdateArgs { + pub id: i32, + pub name: Option, + pub color: Option, +} + +pub(crate) fn mount() -> RouterBuilder { + RouterBuilder::new() + .query("get", |ctx, arg: LibraryArgs<()>| async move { + let (_, library) = arg.get_library(&ctx).await?; + + Ok(library.db.tag().find_many(vec![]).exec().await?) + }) + .query("getFilesForTag", |ctx, arg: LibraryArgs| async move { + let (tag_id, library) = arg.get_library(&ctx).await?; + + Ok(library + .db + .tag() + .find_unique(tag::id::equals(tag_id)) + .exec() + .await?) + }) + .mutation( + "create", + |ctx, arg: LibraryArgs| async move { + let (args, library) = arg.get_library(&ctx).await?; + + let created_tag = library + .db + .tag() + .create( + Uuid::new_v4().as_bytes().to_vec(), + vec![ + tag::name::set(Some(args.name)), + tag::color::set(Some(args.color)), + ], + ) + .exec() + .await?; + + invalidate_query!( + library, + "tags.get": LibraryArgs<()>, + LibraryArgs { + library_id: library.id, + arg: () + } + ); + + Ok(created_tag) + }, + ) + .mutation( + "assign", + |ctx, arg: LibraryArgs| async move { + let (args, library) = arg.get_library(&ctx).await?; + + library.db.tag_on_file().create( + tag::id::equals(args.tag_id), + file::id::equals(args.file_id), + vec![], + ); + + Ok(()) + }, + ) + .mutation( + "update", + |ctx, arg: LibraryArgs| async move { + let (args, library) = arg.get_library(&ctx).await?; + + library + .db + .tag() + .update( + tag::id::equals(args.id), + vec![tag::name::set(args.name), tag::color::set(args.color)], + ) + .exec() + .await?; + + invalidate_query!( + library, + "tags.get": LibraryArgs<()>, + LibraryArgs { + library_id: library.id, + arg: () + } + ); + + Ok(()) + }, + ) + .mutation("delete", |ctx, arg: LibraryArgs| async move { + let (id, library) = arg.get_library(&ctx).await?; + + library.db.tag().delete(tag::id::equals(id)).exec().await?; + + invalidate_query!( + library, + "tags.get": LibraryArgs<()>, + LibraryArgs { + library_id: library.id, + arg: () + } + ); + + Ok(()) + }) +} diff --git a/core/src/api/utils/invalidate.rs b/core/src/api/utils/invalidate.rs new file mode 100644 index 000000000..df397577d --- /dev/null +++ b/core/src/api/utils/invalidate.rs @@ -0,0 +1,125 @@ +use std::sync::Arc; + +#[cfg(debug_assertions)] +use std::sync::Mutex; + +#[cfg(debug_assertions)] +use once_cell::sync::OnceCell; +use rspc::{internal::specta::DataType, Type}; +use serde::Serialize; +use serde_json::Value; + +use crate::api::Router; + +/// holds information about all invalidation queries done with the [invalidate_query!] macro so we can check they are valid when building the router. +#[cfg(debug_assertions)] +pub(crate) static INVALIDATION_REQUESTS: OnceCell> = OnceCell::new(); + +#[derive(Debug, Clone, Serialize, Type)] +pub struct InvalidateOperationEvent { + /// This fields are intentionally private. + key: &'static str, + arg: Value, +} + +impl InvalidateOperationEvent { + /// If you are using this function, your doing it wrong. + pub fn dangerously_create(key: &'static str, arg: Value) -> Self { + Self { key, arg } + } +} + +/// a request to invalidate a specific resource +#[derive(Debug)] +#[allow(dead_code)] +pub(crate) struct InvalidationRequest { + pub key: &'static str, + pub arg_ty: DataType, + pub macro_src: &'static str, +} + +/// invalidation request for a specific resource +#[derive(Debug, Default)] +#[allow(dead_code)] +pub(crate) struct InvalidRequests { + pub queries: Vec, +} + +impl InvalidRequests { + #[allow(unused_variables)] + pub(crate) fn validate(r: Arc) { + #[cfg(debug_assertions)] + { + let invalidate_requests = crate::api::utils::INVALIDATION_REQUESTS + .get_or_init(Default::default) + .lock() + .unwrap(); + + let queries = r.queries(); + for req in &invalidate_requests.queries { + if let Some(query_ty) = queries.get(req.key) { + if query_ty.ty.arg_ty != req.arg_ty { + panic!( + "Error at '{}': Attempted to invalid query '{}' but the argument type does not match the type defined on the router.", + req.macro_src, req.key + ); + } + } else { + panic!( + "Error at '{}': Attempted to invalid query '{}' which was not found in the router", + req.macro_src, req.key + ); + } + } + } + } +} + +/// invalidate_query is a macro which stores a list of all of it's invocations so it can ensure all of the queries match the queries attached to the router. +/// This allows invalidate to be type-safe even when the router keys are stringly typed. +/// ```ignore +/// invalidate_query!( +/// library, // crate::library::LibraryContext +/// "version": (), // Name of the query and the type of it +/// () // The arguments +/// ); +/// ``` +#[macro_export] +#[allow(clippy::crate_in_macro_def)] +macro_rules! invalidate_query { + ($ctx:expr, $key:literal: $arg_ty:ty, $arg:expr) => {{ + let _: $arg_ty = $arg; // Assert the type the user provided is correct + let ctx: &crate::library::LibraryContext = &$ctx; // Assert the context is the correct type + + #[cfg(debug_assertions)] + { + #[ctor::ctor] + fn invalidate() { + crate::api::utils::INVALIDATION_REQUESTS + .get_or_init(|| Default::default()) + .lock() + .unwrap() + .queries + .push(crate::api::utils::InvalidationRequest { + key: $key, + arg_ty: <$arg_ty as rspc::internal::specta::Type>::reference(rspc::internal::specta::DefOpts { + parent_inline: false, + type_map: &mut rspc::internal::specta::TypeDefs::new(), + }, &[]), + macro_src: concat!(file!(), ":", line!()), + }) + } + } + + // The error are ignored here because they aren't mission critical. If they fail the UI might be outdated for a bit. + let _ = serde_json::to_value($arg) + .map(|v| + ctx.emit(crate::api::CoreEvent::InvalidateOperation( + crate::api::utils::InvalidateOperationEvent::dangerously_create($key, v), + )) + ) + .map_err(|_| { + tracing::warn!("Failed to serialize invalidate query event!"); + }); + }}; +} diff --git a/core/src/api/utils/mod.rs b/core/src/api/utils/mod.rs new file mode 100644 index 000000000..bd98fe854 --- /dev/null +++ b/core/src/api/utils/mod.rs @@ -0,0 +1,3 @@ +mod invalidate; + +pub use invalidate::*; diff --git a/core/src/api/volumes.rs b/core/src/api/volumes.rs new file mode 100644 index 000000000..63b8bb742 --- /dev/null +++ b/core/src/api/volumes.rs @@ -0,0 +1,7 @@ +use crate::sys::get_volumes; + +use super::{Router, RouterBuilder}; + +pub(crate) fn mount() -> RouterBuilder { + ::new().query("get", |_, _: ()| Ok(get_volumes()?)) +} diff --git a/core/src/encode/metadata.rs b/core/src/encode/metadata.rs index cde41fffe..d22fd5317 100644 --- a/core/src/encode/metadata.rs +++ b/core/src/encode/metadata.rs @@ -1,5 +1,5 @@ -extern crate ffmpeg_next as ffmpeg; -use ffmpeg::format; +#[cfg(feature = "ffmpeg")] +use ffmpeg_next::format; #[derive(Default, Debug)] pub struct MediaItem { @@ -22,9 +22,10 @@ pub struct Stream { } #[derive(Debug)] +#[allow(dead_code)] // TODO: Remove this when we start using ffmpeg pub enum StreamKind { - // Video(VideoStream), - // Audio(AudioStream), + Video(VideoStream), + Audio(AudioStream), } #[derive(Debug)] @@ -32,6 +33,7 @@ pub struct VideoStream { pub width: u32, pub height: u32, pub aspect_ratio: String, + #[cfg(feature = "ffmpeg")] pub format: format::Pixel, pub bitrate: usize, } @@ -39,6 +41,7 @@ pub struct VideoStream { #[derive(Debug)] pub struct AudioStream { pub channels: u16, + #[cfg(feature = "ffmpeg")] pub format: format::Sample, pub bitrate: usize, pub rate: u32, @@ -125,10 +128,10 @@ pub struct AudioStream { // } // media_item.steams.push(stream_item); // } -// println!("{:#?}", media_item); +// info!("{:#?}", media_item); // } -// Err(error) => println!("error: {}", error), +// Err(error) => error!("error: {}", error), // } // Ok(()) // } diff --git a/core/src/encode/thumb.rs b/core/src/encode/thumb.rs index bb148d46b..6d47e009f 100644 --- a/core/src/encode/thumb.rs +++ b/core/src/encode/thumb.rs @@ -1,154 +1,216 @@ -use crate::job::JobReportUpdate; -use crate::node::get_nodestate; use crate::{ - job::{Job, WorkerContext}, - prisma::file_path, - CoreContext, + api::CoreEvent, + job::{JobError, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext}, + library::LibraryContext, + prisma::{file_path, location}, }; -use crate::{sys, CoreEvent}; -use futures::executor::block_on; -use image::*; -use log::{error, info}; -use std::fs; -use std::path::{Path, PathBuf}; -use webp::*; - -#[derive(Debug, Clone)] -pub struct ThumbnailJob { - pub location_id: i32, - pub path: String, - pub background: bool, -} +use image::{self, imageops, DynamicImage, GenericImageView}; +use serde::{Deserialize, Serialize}; +use std::{ + error::Error, + ops::Deref, + path::{Path, PathBuf}, +}; +use tokio::{fs, task::block_in_place}; +use tracing::{error, info, trace, warn}; +use webp::Encoder; static THUMBNAIL_SIZE_FACTOR: f32 = 0.2; static THUMBNAIL_QUALITY: f32 = 30.0; pub static THUMBNAIL_CACHE_DIR_NAME: &str = "thumbnails"; +pub const THUMBNAIL_JOB_NAME: &str = "thumbnailer"; + +pub struct ThumbnailJob {} + +#[derive(Serialize, Deserialize, Clone)] +pub struct ThumbnailJobInit { + pub location_id: i32, + pub path: PathBuf, + pub background: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ThumbnailJobState { + thumbnail_dir: PathBuf, + root_path: PathBuf, +} #[async_trait::async_trait] -impl Job for ThumbnailJob { - fn name(&self) -> &'static str { - "thumbnailer" - } - async fn run(&self, ctx: WorkerContext) -> Result<(), Box> { - let config = get_nodestate(); - let core_ctx = ctx.core_ctx.clone(); +impl StatefulJob for ThumbnailJob { + type Init = ThumbnailJobInit; + type Data = ThumbnailJobState; + type Step = file_path::Data; - let location = sys::get_location(&core_ctx, self.location_id).await?; + fn name(&self) -> &'static str { + THUMBNAIL_JOB_NAME + } + + async fn init( + &self, + ctx: WorkerContext, + state: &mut JobState, + ) -> JobResult { + let library_ctx = ctx.library_ctx(); + let thumbnail_dir = library_ctx + .config() + .data_directory() + .join(THUMBNAIL_CACHE_DIR_NAME) + .join(state.init.location_id.to_string()); + + let location = library_ctx + .db + .location() + .find_unique(location::id::equals(state.init.location_id)) + .exec() + .await? + .unwrap(); info!( - "Searching for images in location {} at path {}", - location.id, self.path + "Searching for images in location {} at path {:#?}", + location.id, state.init.path ); // create all necessary directories if they don't exist - fs::create_dir_all( - Path::new(&config.data_path) - .join(THUMBNAIL_CACHE_DIR_NAME) - .join(format!("{}", self.location_id)), - )?; - let root_path = location.path.unwrap(); + fs::create_dir_all(&thumbnail_dir).await?; + let root_path = location.local_path.map(PathBuf::from).unwrap(); // query database for all files in this location that need thumbnails - let image_files = get_images(&core_ctx, self.location_id, &self.path).await?; + let image_files = + get_images(&library_ctx, state.init.location_id, &state.init.path).await?; info!("Found {:?} files", image_files.len()); - let is_background = self.background.clone(); + ctx.progress(vec![ + JobReportUpdate::TaskCount(image_files.len()), + JobReportUpdate::Message(format!("Preparing to process {} files", image_files.len())), + ]); - tokio::task::spawn_blocking(move || { - ctx.progress(vec![ - JobReportUpdate::TaskCount(image_files.len()), - JobReportUpdate::Message(format!( - "Preparing to process {} files", - image_files.len() - )), - ]); + state.data = Some(ThumbnailJobState { + thumbnail_dir, + root_path, + }); + state.steps = image_files.into_iter().collect(); - for (i, image_file) in image_files.iter().enumerate() { - ctx.progress(vec![JobReportUpdate::Message(format!( - "Processing {}", - image_file.materialized_path.clone() - ))]); + Ok(()) + } - // assemble the file path - let path = Path::new(&root_path).join(&image_file.materialized_path); - error!("image_file {:?}", image_file); + async fn execute_step( + &self, + ctx: WorkerContext, + state: &mut JobState, + ) -> JobResult { + let step = &state.steps[0]; + ctx.progress(vec![JobReportUpdate::Message(format!( + "Processing {}", + step.materialized_path + ))]); - // get cas_id, if none found skip - let cas_id = match image_file.file() { - Ok(file) => { - if let Some(f) = file { - f.cas_id.clone() - } else { - continue; - } - } - Err(_) => { - error!("Error getting cas_id {:?}", image_file.materialized_path); - continue; - } - }; + let data = state + .data + .as_ref() + .expect("critical error: missing data on job state"); - // Define and write the WebP-encoded file to a given path - let output_path = Path::new(&config.data_path) - .join(THUMBNAIL_CACHE_DIR_NAME) - .join(format!("{}", location.id)) - .join(&cas_id) - .with_extension("webp"); + // assemble the file path + let path = data.root_path.join(&step.materialized_path); + trace!("image_file {:?}", step); - // check if file exists at output path - if !output_path.exists() { - info!("Writing {:?} to {:?}", path, output_path); - generate_thumbnail(&path, &output_path) - .map_err(|e| { - info!("Error generating thumb {:?}", e); - }) - .unwrap_or(()); - - ctx.progress(vec![JobReportUpdate::CompletedTaskCount(i + 1)]); - - if !is_background { - block_on(ctx.core_ctx.emit(CoreEvent::NewThumbnail { cas_id })); - }; + // get cas_id, if none found skip + let cas_id = match step.file() { + Ok(file) => { + if let Some(f) = file { + f.cas_id.clone() } else { - info!("Thumb exists, skipping... {}", output_path.display()); + warn!( + "skipping thumbnail generation for {}", + step.materialized_path + ); + return Ok(()); } } - }) - .await?; + Err(_) => { + error!("Error getting cas_id {:?}", step.materialized_path); + return Ok(()); + } + }; + // Define and write the WebP-encoded file to a given path + let output_path = data.thumbnail_dir.join(&cas_id).with_extension("webp"); + + // check if file exists at output path + if !output_path.exists() { + info!("Writing {:?} to {:?}", path, output_path); + + if let Err(e) = generate_thumbnail(&path, &output_path).await { + error!("Error generating thumb {:?}", e); + } + + if !state.init.background { + ctx.library_ctx().emit(CoreEvent::NewThumbnail { cas_id }); + }; + } else { + info!("Thumb exists, skipping... {}", output_path.display()); + } + + ctx.progress(vec![JobReportUpdate::CompletedTaskCount( + state.step_number + 1, + )]); + + Ok(()) + } + + async fn finalize( + &self, + _ctx: WorkerContext, + state: &mut JobState, + ) -> Result<(), JobError> { + let data = state + .data + .as_ref() + .expect("critical error: missing data on job state"); + info!( + "Finished thumbnail generation for location {} at {}", + state.init.location_id, + data.root_path.display() + ); Ok(()) } } -pub fn generate_thumbnail( - file_path: &PathBuf, - output_path: &PathBuf, -) -> Result<(), Box> { - // Using `image` crate, open the included .jpg file - let img = image::open(file_path)?; - let (w, h) = img.dimensions(); - // Optionally, resize the existing photo and convert back into DynamicImage - let img: DynamicImage = image::DynamicImage::ImageRgba8(imageops::resize( - &img, - (w as f32 * THUMBNAIL_SIZE_FACTOR) as u32, - (h as f32 * THUMBNAIL_SIZE_FACTOR) as u32, - imageops::FilterType::Triangle, - )); - // Create the WebP encoder for the above image - let encoder: Encoder = Encoder::from_image(&img)?; +pub async fn generate_thumbnail>( + file_path: P, + output_path: P, +) -> Result<(), Box> { + // Webp creation has blocking code + let webp = block_in_place(|| -> Result, Box> { + // Using `image` crate, open the included .jpg file + let img = image::open(file_path)?; + let (w, h) = img.dimensions(); + // Optionally, resize the existing photo and convert back into DynamicImage + let img = DynamicImage::ImageRgba8(imageops::resize( + &img, + (w as f32 * THUMBNAIL_SIZE_FACTOR) as u32, + (h as f32 * THUMBNAIL_SIZE_FACTOR) as u32, + imageops::FilterType::Triangle, + )); + // Create the WebP encoder for the above image + let encoder = Encoder::from_image(&img)?; - // Encode the image at a specified quality 0-100 - let webp: WebPMemory = encoder.encode(THUMBNAIL_QUALITY); + // Encode the image at a specified quality 0-100 - std::fs::write(&output_path, &*webp)?; + // Type WebPMemory is !Send, which makes the Future in this function !Send, + // this make us `deref` to have a `&[u8]` and then `to_owned` to make a Vec + // which implies on a unwanted clone... + Ok(encoder.encode(THUMBNAIL_QUALITY).deref().to_owned()) + })?; + + fs::write(output_path, &webp).await?; Ok(()) } pub async fn get_images( - ctx: &CoreContext, + ctx: &LibraryContext, location_id: i32, - path: &str, + path: impl AsRef, ) -> Result, std::io::Error> { let mut params = vec![ file_path::location_id::equals(Some(location_id)), @@ -161,12 +223,14 @@ pub async fn get_images( ]), ]; - if !path.is_empty() { - params.push(file_path::materialized_path::starts_with(path.to_string())) + let path_str = path.as_ref().as_os_str().to_str().unwrap().to_string(); + + if !path_str.is_empty() { + params.push(file_path::materialized_path::starts_with(path_str)) } let image_files = ctx - .database + .db .file_path() .find_many(params) .with(file_path::file::fetch()) diff --git a/core/src/file/cas/checksum.rs b/core/src/file/cas/checksum.rs index 78bd09aca..08c12675c 100644 --- a/core/src/file/cas/checksum.rs +++ b/core/src/file/cas/checksum.rs @@ -1,36 +1,27 @@ use data_encoding::HEXLOWER; use ring::digest::{Context, SHA256}; -use std::convert::TryInto; -use std::fs::File; - -use std::io; use std::path::PathBuf; +use tokio::{ + fs::File, + io::{self, AsyncReadExt, AsyncSeekExt, SeekFrom}, +}; static SAMPLE_COUNT: u64 = 4; static SAMPLE_SIZE: u64 = 10000; -fn read_at(file: &File, offset: u64, size: u64) -> Result, io::Error> { +async fn read_at(file: &mut File, offset: u64, size: u64) -> Result, io::Error> { let mut buf = vec![0u8; size as usize]; - #[cfg(target_family = "unix")] - { - use std::os::unix::prelude::*; - file.read_exact_at(&mut buf, offset)?; - } - - #[cfg(target_family = "windows")] - { - use std::os::windows::prelude::*; - file.seek_read(&mut buf, offset)?; - } + file.seek(SeekFrom::Start(offset)).await?; + file.read_exact(&mut buf).await?; Ok(buf) } -pub fn generate_cas_id(path: PathBuf, size: u64) -> Result { +pub async fn generate_cas_id(path: PathBuf, size: u64) -> Result { // open file reference - let file = File::open(path)?; + let mut file = File::open(path).await?; let mut context = Context::new(&SHA256); @@ -39,20 +30,16 @@ pub fn generate_cas_id(path: PathBuf, size: u64) -> Result { // if size is small enough, just read the whole thing if SAMPLE_COUNT * SAMPLE_SIZE > size { - let buf = read_at(&file, 0, size.try_into().unwrap())?; + let buf = read_at(&mut file, 0, size).await?; context.update(&buf); } else { // loop over samples for i in 0..SAMPLE_COUNT { - let buf = read_at( - &file, - (size / SAMPLE_COUNT) * i, - SAMPLE_SIZE.try_into().unwrap(), - )?; + let buf = read_at(&mut file, (size / SAMPLE_COUNT) * i, SAMPLE_SIZE).await?; context.update(&buf); } // sample end of file - let buf = read_at(&file, size - SAMPLE_SIZE, SAMPLE_SIZE.try_into().unwrap())?; + let buf = read_at(&mut file, size - SAMPLE_SIZE, SAMPLE_SIZE).await?; context.update(&buf); } diff --git a/core/src/file/cas/identifier.rs b/core/src/file/cas/identifier.rs index 94e5c5aaf..ac13c7b92 100644 --- a/core/src/file/cas/identifier.rs +++ b/core/src/file/cas/identifier.rs @@ -1,45 +1,78 @@ use super::checksum::generate_cas_id; + use crate::{ - file::FileError, - job::JobReportUpdate, - job::{Job, WorkerContext}, - prisma::{file, file_path}, - sys::get_location, - CoreContext, + job::{JobError, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext}, + library::LibraryContext, + prisma::{file, file_path, location}, }; use chrono::{DateTime, FixedOffset}; -use futures::executor::block_on; -use log::info; use prisma_client_rust::{prisma_models::PrismaValue, raw, raw::Raw, Direction}; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::path::Path; -use std::{fs, io}; - -// FileIdentifierJob takes file_paths without a file_id and uniquely identifies them -// first: generating the cas_id and extracting metadata -// finally: creating unique file records, and linking them to their file_paths -#[derive(Debug)] -pub struct FileIdentifierJob { - pub location_id: i32, - pub path: String, -} +use std::{ + collections::{HashMap, HashSet}, + path::{Path, PathBuf}, +}; +use tokio::{fs, io}; +use tracing::{error, info}; // we break this job into chunks of 100 to improve performance static CHUNK_SIZE: usize = 100; +pub const IDENTIFIER_JOB_NAME: &str = "file_identifier"; + +pub struct FileIdentifierJob {} + +// FileIdentifierJobInit takes file_paths without a file_id and uniquely identifies them +// first: generating the cas_id and extracting metadata +// finally: creating unique file records, and linking them to their file_paths +#[derive(Serialize, Deserialize, Clone)] +pub struct FileIdentifierJobInit { + pub location_id: i32, + pub path: PathBuf, +} + +#[derive(Serialize, Deserialize)] +pub struct FileIdentifierJobState { + total_count: usize, + task_count: usize, + location: location::Data, + location_path: PathBuf, + cursor: i32, +} #[async_trait::async_trait] -impl Job for FileIdentifierJob { +impl StatefulJob for FileIdentifierJob { + type Init = FileIdentifierJobInit; + type Data = FileIdentifierJobState; + type Step = (); + fn name(&self) -> &'static str { - "file_identifier" + IDENTIFIER_JOB_NAME } - async fn run(&self, ctx: WorkerContext) -> Result<(), Box> { + + async fn init( + &self, + ctx: WorkerContext, + state: &mut JobState, + ) -> JobResult { info!("Identifying orphan file paths..."); - let location = get_location(&ctx.core_ctx, self.location_id).await?; - let location_path = location.path.unwrap_or("".to_string()); + let library = ctx.library_ctx(); - let total_count = count_orphan_file_paths(&ctx.core_ctx, location.id.into()).await?; + let location = library + .db + .location() + .find_unique(location::id::equals(state.init.location_id)) + .exec() + .await? + .unwrap(); + + let location_path = location + .local_path + .as_ref() + .map(PathBuf::from) + .unwrap_or_default(); + + let total_count = count_orphan_file_paths(&library, location.id.into()).await?; info!("Found {} orphan file paths", total_count); let task_count = (total_count as f64 / CHUNK_SIZE as f64).ceil() as usize; @@ -48,142 +81,193 @@ impl Job for FileIdentifierJob { // update job with total task count based on orphan file_paths count ctx.progress(vec![JobReportUpdate::TaskCount(task_count)]); - let db = ctx.core_ctx.database.clone(); - // dedicated tokio thread for task - let _ctx = tokio::task::spawn_blocking(move || { - let mut completed: usize = 0; - let mut cursor: i32 = 1; - // loop until task count is complete - while completed < task_count { - // link file_path ids to a CreateFile struct containing unique file data - let mut chunk: HashMap = HashMap::new(); - let mut cas_lookup: HashMap = HashMap::new(); + state.data = Some(FileIdentifierJobState { + total_count, + task_count, + location, + location_path, + cursor: 1, + }); - // get chunk of orphans to process - let file_paths = match block_on(get_orphan_file_paths(&ctx.core_ctx, cursor)) { - Ok(file_paths) => file_paths, - Err(e) => { - info!("Error getting orphan file paths: {}", e); - continue; - } - }; - info!( - "Processing {:?} orphan files. ({} completed of {})", - file_paths.len(), - completed, - task_count - ); + state.steps = (0..task_count).map(|_| ()).collect(); - // analyze each file_path - for file_path in file_paths.iter() { - // get the cas_id and extract metadata - match prepare_file(&location_path, file_path) { - Ok(file) => { - let cas_id = file.cas_id.clone(); - // create entry into chunks for created file data - chunk.insert(file_path.id, file); - cas_lookup.insert(cas_id, file_path.id); - } - Err(e) => { - info!("Error processing file: {}", e); - continue; - } - }; - } + Ok(()) + } - // find all existing files by cas id - let generated_cas_ids = chunk.values().map(|c| c.cas_id.clone()).collect(); - let existing_files: Vec = block_on( - db.file() - .find_many(vec![file::cas_id::in_vec(generated_cas_ids)]) - .exec(), - ) - .unwrap(); - info!("Found {} existing files", existing_files.len()); + async fn execute_step( + &self, + ctx: WorkerContext, + state: &mut JobState, + ) -> JobResult { + // link file_path ids to a CreateFile struct containing unique file data + let mut chunk: HashMap = HashMap::new(); + let mut cas_lookup: HashMap = HashMap::new(); - // link those existing files to their file paths - for file in existing_files.iter() { - let file_path_id = cas_lookup.get(&file.cas_id).unwrap(); - block_on( - db.file_path() - .find_unique(file_path::id::equals(file_path_id.clone())) - .update(vec![file_path::file_id::set(Some(file.id.clone()))]) - .exec(), - ) - .unwrap(); - } + let data = state + .data + .as_mut() + .expect("critical error: missing data on job state"); - // extract files that don't already exist in the database - let new_files: Vec<&CreateFile> = chunk - .iter() - .map(|(_, c)| c) - .filter(|c| !existing_files.iter().any(|d| d.cas_id == c.cas_id)) - .collect(); - - // assemble prisma values for new unique files - let mut values: Vec = Vec::new(); - for file in new_files.iter() { - values.extend([ - PrismaValue::String(file.cas_id.clone()), - PrismaValue::Int(file.size_in_bytes.clone()), - PrismaValue::DateTime(file.date_created.clone()), - ]); - } - - // create new file records with assembled values - let created_files: Vec = block_on(db._query_raw(Raw::new( - &format!( - "INSERT INTO files (cas_id, size_in_bytes, date_created) VALUES {} - ON CONFLICT (cas_id) DO NOTHING RETURNING id, cas_id", - vec!["({}, {}, {})"; new_files.len()].join(",") - ), - values, - ))) - .unwrap_or_else(|e| { - info!("Error inserting files: {}", e); - Vec::new() - }); - - // associate newly created files with their respective file_paths - for file in created_files.iter() { - // TODO: this is potentially bottle necking the chunk system, individually linking file_path to file, 100 queries per chunk - // - insert many could work, but I couldn't find a good way to do this in a single SQL query - let file_path_id = cas_lookup.get(&file.cas_id).unwrap(); - block_on( - db.file_path() - .find_unique(file_path::id::equals(file_path_id.clone())) - .update(vec![file_path::file_id::set(Some(file.id.clone()))]) - .exec(), - ) - .unwrap(); - } - - // handle loop end - let last_row = match file_paths.last() { - Some(l) => l, - None => { - break; - } - }; - cursor = last_row.id; - completed += 1; - - ctx.progress(vec![ - JobReportUpdate::CompletedTaskCount(completed), - JobReportUpdate::Message(format!( - "Processed {} of {} orphan files", - completed * CHUNK_SIZE, - total_count - )), - ]); + // get chunk of orphans to process + let file_paths = match get_orphan_file_paths(&ctx.library_ctx(), data.cursor).await { + Ok(file_paths) => file_paths, + Err(e) => { + info!("Error getting orphan file paths: {:#?}", e); + return Ok(()); } - ctx - }) - .await?; + }; + info!( + "Processing {:?} orphan files. ({} completed of {})", + file_paths.len(), + state.step_number, + data.task_count + ); + + // analyze each file_path + for file_path in &file_paths { + // get the cas_id and extract metadata + match prepare_file(&data.location_path, file_path).await { + Ok(file) => { + let cas_id = file.cas_id.clone(); + // create entry into chunks for created file data + chunk.insert(file_path.id, file); + cas_lookup.insert(cas_id, file_path.id); + } + Err(e) => { + info!("Error processing file: {:#?}", e); + continue; + } + }; + } + + // find all existing files by cas id + let generated_cas_ids = chunk.values().map(|c| c.cas_id.clone()).collect(); + let existing_files = ctx + .library_ctx() + .db + .file() + .find_many(vec![file::cas_id::in_vec(generated_cas_ids)]) + .exec() + .await?; + + info!("Found {} existing files", existing_files.len()); + + // link those existing files to their file paths + // Had to put the file_path in a variable outside of the closure, to satisfy the borrow checker + let library_ctx = ctx.library_ctx(); + + for existing_file in &existing_files { + if let Err(e) = library_ctx + .db + .file_path() + .update( + file_path::id::equals(*cas_lookup.get(&existing_file.cas_id).unwrap()), + vec![file_path::file_id::set(Some(existing_file.id))], + ) + .exec() + .await + { + info!("Error updating file_id: {:#?}", e); + } + } + + let existing_files_cas_ids = existing_files + .iter() + .map(|file| file.cas_id.clone()) + .collect::>(); + + // extract files that don't already exist in the database + let new_files = chunk + .iter() + .map(|(_id, create_file)| create_file) + .filter(|create_file| !existing_files_cas_ids.contains(&create_file.cas_id)) + .collect::>(); + + // assemble prisma values for new unique files + let mut values = Vec::with_capacity(new_files.len() * 3); + for file in &new_files { + values.extend([ + PrismaValue::String(file.cas_id.clone()), + PrismaValue::Int(file.size_in_bytes), + PrismaValue::DateTime(file.date_created), + ]); + } + + // create new file records with assembled values + let created_files: Vec = ctx + .library_ctx() + .db + ._query_raw(Raw::new( + &format!( + "INSERT INTO files (cas_id, size_in_bytes, date_created) VALUES {} + ON CONFLICT (cas_id) DO NOTHING RETURNING id, cas_id", + vec!["({}, {}, {})"; new_files.len()].join(",") + ), + values, + )) + .exec() + .await + .unwrap_or_else(|e| { + error!("Error inserting files: {:#?}", e); + Vec::new() + }); + + for created_file in created_files { + // associate newly created files with their respective file_paths + // TODO: this is potentially bottle necking the chunk system, individually linking file_path to file, 100 queries per chunk + // - insert many could work, but I couldn't find a good way to do this in a single SQL query + if let Err(e) = ctx + .library_ctx() + .db + .file_path() + .update( + file_path::id::equals(*cas_lookup.get(&created_file.cas_id).unwrap()), + vec![file_path::file_id::set(Some(created_file.id))], + ) + .exec() + .await + { + info!("Error updating file_id: {:#?}", e); + } + } + + // handle last step + if let Some(last_row) = file_paths.last() { + data.cursor = last_row.id; + } else { + return Ok(()); + } + + ctx.progress(vec![ + JobReportUpdate::CompletedTaskCount(state.step_number), + JobReportUpdate::Message(format!( + "Processed {} of {} orphan files", + state.step_number * CHUNK_SIZE, + data.total_count + )), + ]); // let _remaining = count_orphan_file_paths(&ctx.core_ctx, location.id.into()).await?; Ok(()) } + + async fn finalize( + &self, + _ctx: WorkerContext, + state: &mut JobState, + ) -> Result<(), JobError> { + let data = state + .data + .as_ref() + .expect("critical error: missing data on job state"); + info!( + "Finalizing identifier job at {}, total of {} tasks", + data.location_path.display(), + data.task_count + ); + + Ok(()) + } } #[derive(Deserialize, Serialize, Debug)] @@ -192,40 +276,38 @@ struct CountRes { } pub async fn count_orphan_file_paths( - ctx: &CoreContext, + ctx: &LibraryContext, location_id: i64, -) -> Result { - let db = &ctx.database; - let files_count = db +) -> Result { + let files_count = ctx.db ._query_raw::(raw!( "SELECT COUNT(*) AS count FROM file_paths WHERE file_id IS NULL AND is_dir IS FALSE AND location_id = {}", PrismaValue::Int(location_id) )) + .exec() .await?; Ok(files_count[0].count.unwrap_or(0)) } pub async fn get_orphan_file_paths( - ctx: &CoreContext, + ctx: &LibraryContext, cursor: i32, -) -> Result, FileError> { - let db = &ctx.database; +) -> Result, prisma_client_rust::QueryError> { info!( "discovering {} orphan file paths at cursor: {:?}", CHUNK_SIZE, cursor ); - let files = db + ctx.db .file_path() .find_many(vec![ file_path::file_id::equals(None), file_path::is_dir::equals(false), ]) .order_by(file_path::id::order(Direction::Asc)) - .cursor(file_path::id::cursor(cursor)) + .cursor(file_path::id::equals(cursor)) .take(CHUNK_SIZE as i64) .exec() - .await?; - Ok(files) + .await } #[derive(Deserialize, Serialize, Debug)] @@ -241,13 +323,15 @@ pub struct FileCreated { pub cas_id: String, } -pub fn prepare_file( - location_path: &str, +pub async fn prepare_file( + location_path: impl AsRef, file_path: &file_path::Data, ) -> Result { - let path = Path::new(&location_path).join(Path::new(file_path.materialized_path.as_str())); + let path = location_path + .as_ref() + .join(file_path.materialized_path.as_str()); - let metadata = fs::metadata(&path)?; + let metadata = fs::metadata(&path).await?; // let date_created: DateTime = metadata.created().unwrap().into(); @@ -255,7 +339,7 @@ pub fn prepare_file( let cas_id = { if !file_path.is_dir { - let mut ret = generate_cas_id(path.clone(), size.clone()).unwrap(); + let mut ret = generate_cas_id(path, size).await?; ret.truncate(16); ret } else { diff --git a/core/src/file/explorer/mod.rs b/core/src/file/explorer/mod.rs deleted file mode 100644 index dd44d4a91..000000000 --- a/core/src/file/explorer/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod open; - -pub use open::*; diff --git a/core/src/file/explorer/open.rs b/core/src/file/explorer/open.rs deleted file mode 100644 index bedfba5a0..000000000 --- a/core/src/file/explorer/open.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::{ - encode::THUMBNAIL_CACHE_DIR_NAME, - file::{DirectoryWithContents, FileError, FilePath}, - node::get_nodestate, - prisma::file_path, - sys::get_location, - CoreContext, -}; -use std::path::Path; - -pub async fn open_dir( - ctx: &CoreContext, - location_id: &i32, - path: &str, -) -> Result { - let db = &ctx.database; - let config = get_nodestate(); - - // get location - let location = get_location(ctx, location_id.clone()).await?; - - let directory = db - .file_path() - .find_first(vec![ - file_path::location_id::equals(Some(location.id)), - file_path::materialized_path::equals(path.into()), - file_path::is_dir::equals(true), - ]) - .exec() - .await? - .ok_or(FileError::DirectoryNotFound(path.to_string()))?; - - println!("DIRECTORY: {:?}", directory); - - let mut file_paths: Vec = db - .file_path() - .find_many(vec![ - file_path::location_id::equals(Some(location.id)), - file_path::parent_id::equals(Some(directory.id)), - ]) - .with(file_path::file::fetch()) - .exec() - .await? - .into_iter() - .map(Into::into) - .collect(); - - for file_path in &mut file_paths { - if let Some(file) = &mut file_path.file { - let thumb_path = Path::new(&config.data_path) - .join(THUMBNAIL_CACHE_DIR_NAME) - .join(format!("{}", location.id)) - .join(file.cas_id.clone()) - .with_extension("webp"); - - file.has_thumbnail = thumb_path.exists(); - } - } - - Ok(DirectoryWithContents { - directory: directory.into(), - contents: file_paths, - }) -} diff --git a/core/src/file/indexer.rs b/core/src/file/indexer.rs new file mode 100644 index 000000000..36c75b470 --- /dev/null +++ b/core/src/file/indexer.rs @@ -0,0 +1,386 @@ +use crate::{ + job::{JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext}, + prisma::location, + sys::create_location, +}; +use chrono::{DateTime, Utc}; +use prisma_client_rust::{raw, raw::Raw, PrismaValue}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + ffi::OsStr, + path::{Path, PathBuf}, + time::Duration, +}; +use tokio::{fs, time::Instant}; +use tracing::{error, info}; +use walkdir::{DirEntry, WalkDir}; + +static BATCH_SIZE: usize = 100; +pub const INDEXER_JOB_NAME: &str = "indexer"; + +#[derive(Clone)] +pub enum ScanProgress { + ChunkCount(usize), + SavedChunks(usize), + Message(String), +} + +pub struct IndexerJob {} + +#[derive(Serialize, Deserialize, Clone)] +pub struct IndexerJobInit { + pub path: PathBuf, +} + +#[derive(Serialize, Deserialize)] +pub struct IndexerJobData { + location: location::Data, + db_write_start: DateTime, + scan_read_time: Duration, + total_paths: usize, +} + +pub(crate) type IndexerJobStep = Vec<(PathBuf, i32, Option, bool)>; + +impl IndexerJobData { + fn on_scan_progress(ctx: WorkerContext, progress: Vec) { + ctx.progress( + progress + .iter() + .map(|p| match p.clone() { + ScanProgress::ChunkCount(c) => JobReportUpdate::TaskCount(c), + ScanProgress::SavedChunks(p) => JobReportUpdate::CompletedTaskCount(p), + ScanProgress::Message(m) => JobReportUpdate::Message(m), + }) + .collect(), + ) + } +} + +#[async_trait::async_trait] +impl StatefulJob for IndexerJob { + type Init = IndexerJobInit; + type Data = IndexerJobData; + type Step = IndexerJobStep; + + fn name(&self) -> &'static str { + INDEXER_JOB_NAME + } + + // creates a vector of valid path buffers from a directory + async fn init( + &self, + ctx: WorkerContext, + state: &mut JobState, + ) -> JobResult { + let location = create_location(&ctx.library_ctx(), &state.init.path).await?; + + // query db to highers id, so we can increment it for the new files indexed + #[derive(Deserialize, Serialize, Debug)] + struct QueryRes { + id: Option, + } + // grab the next id so we can increment in memory for batch inserting + let first_file_id = match ctx + .library_ctx() + .db + ._query_raw::(raw!("SELECT MAX(id) id FROM file_paths")) + .exec() + .await + { + Ok(rows) => rows[0].id.unwrap_or(0), + Err(e) => panic!("Error querying for next file id: {:#?}", e), + }; + + //check is path is a directory + if !state.init.path.is_dir() { + // return Err(anyhow::anyhow!("{} is not a directory", &path)); + panic!("{:#?} is not a directory", state.init.path); + } + + // spawn a dedicated thread to scan the directory for performance + let path = state.init.path.clone(); + let inner_ctx = ctx.clone(); + let (paths, scan_start) = tokio::task::spawn_blocking(move || { + // store every valid path discovered + let mut paths: Vec<(PathBuf, i32, Option, bool)> = Vec::new(); + // store a hashmap of directories to their file ids for fast lookup + let mut dirs = HashMap::new(); + // begin timer for logging purposes + let scan_start = Instant::now(); + + let mut next_file_id = first_file_id; + let mut get_id = || { + next_file_id += 1; + next_file_id + }; + // walk through directory recursively + for entry in WalkDir::new(&path).into_iter().filter_entry(|dir| { + // check if entry is approved + !is_hidden(dir) && !is_app_bundle(dir) && !is_node_modules(dir) && !is_library(dir) + }) { + // extract directory entry or log and continue if failed + let entry = match entry { + Ok(entry) => entry, + Err(e) => { + error!("Error reading file {}", e); + continue; + } + }; + let path = entry.path(); + + info!("Found filesystem path: {:?}", path); + + let parent_path = path + .parent() + .unwrap_or_else(|| Path::new("")) + .to_str() + .unwrap_or(""); + let parent_dir_id = dirs.get(parent_path); + + let path_str = match path.as_os_str().to_str() { + Some(path_str) => path_str, + None => { + error!("Error reading file {}", &path.display()); + continue; + } + }; + + IndexerJobData::on_scan_progress( + inner_ctx.clone(), + vec![ + ScanProgress::Message(format!("Scanning {}", path_str)), + ScanProgress::ChunkCount(paths.len() / BATCH_SIZE), + ], + ); + + let file_id = get_id(); + let file_type = entry.file_type(); + let is_dir = file_type.is_dir(); + + if is_dir || file_type.is_file() { + paths.push((path.to_owned(), file_id, parent_dir_id.cloned(), is_dir)); + } + + if is_dir { + let _path = match path.to_str() { + Some(path) => path.to_owned(), + None => continue, + }; + dirs.insert(_path, file_id); + } + } + (paths, scan_start) + }) + .await?; + + state.data = Some(IndexerJobData { + location, + db_write_start: Utc::now(), + scan_read_time: scan_start.elapsed(), + total_paths: paths.len(), + }); + + state.steps = paths + .chunks(BATCH_SIZE) + .enumerate() + .map(|(i, chunk)| { + IndexerJobData::on_scan_progress( + ctx.clone(), + vec![ + ScanProgress::SavedChunks(i as usize), + ScanProgress::Message(format!( + "Writing {} of {} to db", + i * chunk.len(), + paths.len(), + )), + ], + ); + chunk.to_vec() + }) + .collect(); + + Ok(()) + } + + async fn execute_step( + &self, + ctx: WorkerContext, + state: &mut JobState, + ) -> JobResult { + // vector to store active models + let mut files = Vec::new(); + let step = &state.steps[0]; + + let data = state + .data + .as_ref() + .expect("critical error: missing data on job state"); + + for (file_path, file_id, parent_dir_id, is_dir) in step { + files.extend( + match prepare_values(file_path, *file_id, &data.location, parent_dir_id, *is_dir) + .await + { + Ok(values) => values.to_vec(), + Err(e) => { + error!("Error creating file model from path {:?}: {}", file_path, e); + continue; + } + }, + ); + } + + let raw = Raw::new( + &format!(" + INSERT INTO file_paths (id, is_dir, location_id, materialized_path, name, extension, parent_id, date_created) + VALUES {} + ", + vec!["({}, {}, {}, {}, {}, {}, {}, {})"; step.len()].join(", ") + ), + files + ); + + let count = ctx.library_ctx().db._execute_raw(raw).exec().await; + + info!("Inserted {:?} records", count); + + Ok(()) + } + + async fn finalize( + &self, + _ctx: WorkerContext, + state: &mut JobState, + ) -> JobResult { + let data = state + .data + .as_ref() + .expect("critical error: missing data on job state"); + info!( + "scan of {:?} completed in {:?}. {:?} files found. db write completed in {:?}", + state.init.path, + data.scan_read_time, + data.total_paths, + Utc::now() - data.db_write_start, + ); + + Ok(()) + } +} + +// // PathContext provides the indexer with instruction to handle particular directory structures and identify rich context. +// pub struct PathContext { +// // an app specific key "com.github.repo" +// pub key: String, +// pub name: String, +// pub is_dir: bool, +// // possible file extensions for this path +// pub extensions: Vec, +// // sub-paths that must be found +// pub must_contain_sub_paths: Vec, +// // sub-paths that are ignored +// pub always_ignored_sub_paths: Option, +// } + +// reads a file at a path and creates an ActiveModel with metadata +async fn prepare_values( + file_path: impl AsRef, + id: i32, + location: &location::Data, + parent_id: &Option, + is_dir: bool, +) -> Result<[PrismaValue; 8], std::io::Error> { + let file_path = file_path.as_ref(); + + let metadata = fs::metadata(file_path).await?; + let location_path = location.local_path.as_ref().map(PathBuf::from).unwrap(); + // let size = metadata.len(); + let name; + let extension; + let date_created: DateTime = metadata.created().unwrap().into(); + + // if the 'file_path' is not a directory, then get the extension and name. + + // if 'file_path' is a directory, set extension to an empty string to avoid periods in folder names + // - being interpreted as file extensions + if is_dir { + extension = "".to_string(); + name = extract_name(file_path.file_name()); + } else { + extension = extract_name(file_path.extension()); + name = extract_name(file_path.file_stem()); + } + + let materialized_path = file_path.strip_prefix(location_path).unwrap(); + let materialized_path_as_string = materialized_path.to_str().unwrap_or("").to_owned(); + + let values = [ + PrismaValue::Int(id as i64), + PrismaValue::Boolean(metadata.is_dir()), + PrismaValue::Int(location.id as i64), + PrismaValue::String(materialized_path_as_string), + PrismaValue::String(name), + PrismaValue::String(extension.to_lowercase()), + parent_id + .map(|id| PrismaValue::Int(id as i64)) + .unwrap_or(PrismaValue::Null), + PrismaValue::DateTime(date_created.into()), + ]; + + Ok(values) +} + +// extract name from OsStr returned by PathBuff +fn extract_name(os_string: Option<&OsStr>) -> String { + os_string + .unwrap_or_default() + .to_str() + .unwrap_or_default() + .to_owned() +} + +fn is_hidden(entry: &DirEntry) -> bool { + entry + .file_name() + .to_str() + .map(|s| s.starts_with('.')) + .unwrap_or(false) +} + +fn is_library(entry: &DirEntry) -> bool { + entry + .path() + .to_str() + // make better this is shit + .map(|s| s.contains("/Library/")) + .unwrap_or(false) +} + +fn is_node_modules(entry: &DirEntry) -> bool { + entry + .file_name() + .to_str() + .map(|s| s.contains("node_modules")) + .unwrap_or(false) +} + +fn is_app_bundle(entry: &DirEntry) -> bool { + let is_dir = entry.metadata().unwrap().is_dir(); + let contains_dot = entry + .file_name() + .to_str() + .map(|s| s.contains(".app") | s.contains(".bundle")) + .unwrap_or(false); + + // let is_app_bundle = is_dir && contains_dot; + // if is_app_bundle { + // let path_buff = entry.path(); + // let path = path_buff.to_str().unwrap(); + + // self::path(&path, ); + // } + + is_dir && contains_dot +} diff --git a/core/src/file/indexer/mod.rs b/core/src/file/indexer/mod.rs deleted file mode 100644 index 4415b252c..000000000 --- a/core/src/file/indexer/mod.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::job::{Job, JobReportUpdate, WorkerContext}; - -use self::scan::ScanProgress; -mod scan; - -pub use scan::*; - -pub use scan::scan_path; - -#[derive(Debug)] -pub struct IndexerJob { - pub path: String, -} - -#[async_trait::async_trait] -impl Job for IndexerJob { - fn name(&self) -> &'static str { - "indexer" - } - async fn run(&self, ctx: WorkerContext) -> Result<(), Box> { - let core_ctx = ctx.core_ctx.clone(); - scan_path(&core_ctx, self.path.as_str(), move |p| { - ctx.progress( - p.iter() - .map(|p| match p.clone() { - ScanProgress::ChunkCount(c) => JobReportUpdate::TaskCount(c), - ScanProgress::SavedChunks(p) => JobReportUpdate::CompletedTaskCount(p), - ScanProgress::Message(m) => JobReportUpdate::Message(m), - }) - .collect(), - ) - }) - .await - } -} - -// // PathContext provides the indexer with instruction to handle particular directory structures and identify rich context. -// pub struct PathContext { -// // an app specific key "com.github.repo" -// pub key: String, -// pub name: String, -// pub is_dir: bool, -// // possible file extensions for this path -// pub extensions: Vec, -// // sub-paths that must be found -// pub must_contain_sub_paths: Vec, -// // sub-paths that are ignored -// pub always_ignored_sub_paths: Option, -// } diff --git a/core/src/file/indexer/scan.rs b/core/src/file/indexer/scan.rs deleted file mode 100644 index 19c3ee53d..000000000 --- a/core/src/file/indexer/scan.rs +++ /dev/null @@ -1,277 +0,0 @@ -use crate::sys::{create_location, LocationResource}; -use crate::CoreContext; -use chrono::{DateTime, FixedOffset, Utc}; -use log::{error, info}; -use prisma_client_rust::prisma_models::PrismaValue; -use prisma_client_rust::raw; -use prisma_client_rust::raw::Raw; -use serde::{Deserialize, Serialize}; -use std::ffi::OsStr; -use std::{collections::HashMap, fs, path::Path, path::PathBuf, time::Instant}; -use walkdir::{DirEntry, WalkDir}; - -#[derive(Clone)] -pub enum ScanProgress { - ChunkCount(usize), - SavedChunks(usize), - Message(String), -} - -static BATCH_SIZE: usize = 100; - -// creates a vector of valid path buffers from a directory -pub async fn scan_path( - ctx: &CoreContext, - path: &str, - on_progress: impl Fn(Vec) + Send + Sync + 'static, -) -> Result<(), Box> { - let db = &ctx.database; - let path = path.to_string(); - - let location = create_location(&ctx, &path).await?; - - // query db to highers id, so we can increment it for the new files indexed - #[derive(Deserialize, Serialize, Debug)] - struct QueryRes { - id: Option, - } - // grab the next id so we can increment in memory for batch inserting - let first_file_id = match db - ._query_raw::(raw!("SELECT MAX(id) id FROM file_paths")) - .await - { - Ok(rows) => rows[0].id.unwrap_or(0), - Err(e) => panic!("Error querying for next file id: {}", e), - }; - - //check is path is a directory - if !PathBuf::from(&path).is_dir() { - // return Err(anyhow::anyhow!("{} is not a directory", &path)); - panic!("{} is not a directory", &path); - } - let dir_path = path.clone(); - - // spawn a dedicated thread to scan the directory for performance - let (paths, scan_start, on_progress) = tokio::task::spawn_blocking(move || { - // store every valid path discovered - let mut paths: Vec<(PathBuf, i32, Option, bool)> = Vec::new(); - // store a hashmap of directories to their file ids for fast lookup - let mut dirs: HashMap = HashMap::new(); - // begin timer for logging purposes - let scan_start = Instant::now(); - - let mut next_file_id = first_file_id; - let mut get_id = || { - next_file_id += 1; - next_file_id - }; - // walk through directory recursively - for entry in WalkDir::new(&dir_path).into_iter().filter_entry(|dir| { - let approved = - !is_hidden(dir) && !is_app_bundle(dir) && !is_node_modules(dir) && !is_library(dir); - approved - }) { - // extract directory entry or log and continue if failed - let entry = match entry { - Ok(entry) => entry, - Err(e) => { - error!("Error reading file {}", e); - continue; - } - }; - let path = entry.path(); - - info!("Found filesystem path: {:?}", path); - - let parent_path = path - .parent() - .unwrap_or(Path::new("")) - .to_str() - .unwrap_or(""); - let parent_dir_id = dirs.get(&*parent_path); - - let path_str = match path.as_os_str().to_str() { - Some(path_str) => path_str, - None => { - error!("Error reading file {}", &path.display()); - continue; - } - }; - - on_progress(vec![ - ScanProgress::Message(format!("{}", path_str)), - ScanProgress::ChunkCount(paths.len() / BATCH_SIZE), - ]); - - let file_id = get_id(); - let file_type = entry.file_type(); - let is_dir = file_type.is_dir(); - - if is_dir || file_type.is_file() { - paths.push((path.to_owned(), file_id, parent_dir_id.cloned(), is_dir)); - } - - if is_dir { - let _path = match path.to_str() { - Some(path) => path.to_owned(), - None => continue, - }; - dirs.insert(_path, file_id); - } - } - (paths, scan_start, on_progress) - }) - .await - .unwrap(); - - let db_write_start = Instant::now(); - let scan_read_time = scan_start.elapsed(); - - for (i, chunk) in paths.chunks(BATCH_SIZE).enumerate() { - on_progress(vec![ - ScanProgress::SavedChunks(i as usize), - ScanProgress::Message(format!( - "Writing {} of {} to db", - i * chunk.len(), - paths.len(), - )), - ]); - - // vector to store active models - let mut files: Vec = Vec::new(); - - for (file_path, file_id, parent_dir_id, is_dir) in chunk { - files.extend( - match prepare_values(&file_path, *file_id, &location, parent_dir_id, *is_dir) { - Ok(values) => values.to_vec(), - Err(e) => { - error!("Error creating file model from path {:?}: {}", file_path, e); - continue; - } - }, - ); - } - - let raw = Raw::new( - &format!(" - INSERT INTO file_paths (id, is_dir, location_id, materialized_path, name, extension, parent_id, date_created) - VALUES {} - ", - vec!["({}, {}, {}, {}, {}, {}, {}, {})"; chunk.len()].join(", ") - ), - files - ); - - let count = db._execute_raw(raw).await; - - info!("Inserted {:?} records", count); - } - info!( - "scan of {:?} completed in {:?}. {:?} files found. db write completed in {:?}", - &path, - scan_read_time, - paths.len(), - db_write_start.elapsed() - ); - Ok(()) -} - -// reads a file at a path and creates an ActiveModel with metadata -fn prepare_values( - file_path: &PathBuf, - id: i32, - location: &LocationResource, - parent_id: &Option, - is_dir: bool, -) -> Result<[PrismaValue; 8], std::io::Error> { - let metadata = fs::metadata(&file_path)?; - let location_path = Path::new(location.path.as_ref().unwrap().as_str()); - // let size = metadata.len(); - let name; - let extension; - let date_created: DateTime = metadata.created().unwrap().into(); - - // if the 'file_path' is not a directory, then get the extension and name. - - // if 'file_path' is a directory, set extension to an empty string to avoid periods in folder names - // - being interpreted as file extensions - if is_dir { - extension = "".to_string(); - name = extract_name(file_path.file_name()); - } else { - extension = extract_name(file_path.extension()); - name = extract_name(file_path.file_stem()); - } - - let materialized_path = file_path.strip_prefix(location_path).unwrap(); - let materialized_path_as_string = materialized_path.to_str().unwrap_or("").to_owned(); - - let values = [ - PrismaValue::Int(id as i64), - PrismaValue::Boolean(metadata.is_dir()), - PrismaValue::Int(location.id as i64), - PrismaValue::String(materialized_path_as_string), - PrismaValue::String(name), - PrismaValue::String(extension.to_lowercase()), - parent_id - .clone() - .map(|id| PrismaValue::Int(id as i64)) - .unwrap_or(PrismaValue::Null), - PrismaValue::DateTime(date_created.into()), - ]; - - Ok(values) -} - -// extract name from OsStr returned by PathBuff -fn extract_name(os_string: Option<&OsStr>) -> String { - os_string - .unwrap_or_default() - .to_str() - .unwrap_or_default() - .to_owned() -} - -fn is_hidden(entry: &DirEntry) -> bool { - entry - .file_name() - .to_str() - .map(|s| s.starts_with(".")) - .unwrap_or(false) -} - -fn is_library(entry: &DirEntry) -> bool { - entry - .path() - .to_str() - // make better this is shit - .map(|s| s.contains("/Library/")) - .unwrap_or(false) -} - -fn is_node_modules(entry: &DirEntry) -> bool { - entry - .file_name() - .to_str() - .map(|s| s.contains("node_modules")) - .unwrap_or(false) -} - -fn is_app_bundle(entry: &DirEntry) -> bool { - let is_dir = entry.metadata().unwrap().is_dir(); - let contains_dot = entry - .file_name() - .to_str() - .map(|s| s.contains(".app") | s.contains(".bundle")) - .unwrap_or(false); - - let is_app_bundle = is_dir && contains_dot; - // if is_app_bundle { - // let path_buff = entry.path(); - // let path = path_buff.to_str().unwrap(); - - // self::path(&path, ); - // } - - is_app_bundle -} diff --git a/core/src/file/mod.rs b/core/src/file/mod.rs index bc632ecec..fc2fd30f5 100644 --- a/core/src/file/mod.rs +++ b/core/src/file/mod.rs @@ -1,165 +1,2 @@ -use int_enum::IntEnum; -use serde::{Deserialize, Serialize}; -use thiserror::Error; -use ts_rs::TS; - -use crate::{ - prisma::{self, file, file_path}, - sys::SysError, - ClientQuery, CoreContext, CoreError, CoreEvent, CoreResponse, -}; pub mod cas; -pub mod explorer; pub mod indexer; - -// A unique file -#[derive(Debug, Clone, Serialize, Deserialize, TS)] -#[ts(export)] -pub struct File { - pub id: i32, - pub cas_id: String, - pub integrity_checksum: Option, - pub size_in_bytes: String, - pub kind: FileKind, - - pub hidden: bool, - pub favorite: bool, - pub important: bool, - pub has_thumbnail: bool, - pub has_thumbstrip: bool, - pub has_video_preview: bool, - // pub encryption: EncryptionAlgorithm, - pub ipfs_id: Option, - pub note: Option, - - pub date_created: chrono::DateTime, - pub date_modified: chrono::DateTime, - pub date_indexed: chrono::DateTime, - - pub paths: Vec, - // pub media_data: Option, - // pub tags: Vec, - // pub label: Vec