diff --git a/.cargo/config.toml b/.cargo/config.toml index f31443890..2c98975a0 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,6 @@ [alias] prisma = "run --package prisma-cli --" -seed = "run --bin seed --" +seed = "run --features=seed-binary --bin seed --" # this caused me such an unbearable headache... [target.x86_64-apple-darwin] diff --git a/.dockerignore b/.dockerignore index 1f1c03308..8b2ada971 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,3 +4,4 @@ static target *.lock *.log +*.db \ No newline at end of file diff --git a/.github/images/logo.png b/.github/images/logo.png index f3d35c972..8f9f642d6 100644 Binary files a/.github/images/logo.png and b/.github/images/logo.png differ diff --git a/.github/images/logo_old.png b/.github/images/logo_old.png new file mode 100644 index 000000000..f3d35c972 Binary files /dev/null and b/.github/images/logo_old.png differ diff --git a/.github/images/stump-logo--circle.png b/.github/images/stump-logo--circle.png new file mode 100644 index 000000000..acd53294f Binary files /dev/null and b/.github/images/stump-logo--circle.png differ diff --git a/.github/images/stump-logo--irregular-lg.png b/.github/images/stump-logo--irregular-lg.png new file mode 100644 index 000000000..0ea4f365b Binary files /dev/null and b/.github/images/stump-logo--irregular-lg.png differ diff --git a/.github/images/stump-logo--irregular-sm.png b/.github/images/stump-logo--irregular-sm.png new file mode 100644 index 000000000..d1ca9d91f Binary files /dev/null and b/.github/images/stump-logo--irregular-sm.png differ diff --git a/.github/images/stump-logo--square.png b/.github/images/stump-logo--square.png new file mode 100644 index 000000000..8fdc6fd09 Binary files /dev/null and b/.github/images/stump-logo--square.png differ diff --git a/.github/scripts/setup.ps1 b/.github/scripts/setup.ps1 new file mode 100644 index 000000000..24df6c9a2 --- /dev/null +++ b/.github/scripts/setup.ps1 @@ -0,0 +1,24 @@ +write-host "Running Stump pre-setup script! This will install the necessary development tools/dependencies required to develop Stump." + +# admin privileges (unsure if needed, prolly not) + +# if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { +# write-Warning "This setup needs admin permissions. Please run this file as admin." +# break +# } + +# check if user has cargo installed +if (-NOT (Get-Command cargo -errorAction SilentlyContinue)) { + write-Warning "Rust could not be found on your system. Please ensure the 'rustc' and 'cargo' binaries are in your \$PATH." + break +} + +# check if user has pnpm installed +if (-NOT (Get-Command pnpm -errorAction SilentlyContinue)) { + write-Warning "PNPM could not be found on your system. Ensure the 'pnpm' command is in your \$PATH." + break; +} + +# TODO: check if anything else is needed + +write-host "Pre-setup completed! Be sure to run 'pnpm run setup' to finish the setup." \ No newline at end of file diff --git a/.github/scripts/setup.sh b/.github/scripts/setup.sh new file mode 100755 index 000000000..25424ba51 --- /dev/null +++ b/.github/scripts/setup.sh @@ -0,0 +1,106 @@ +#!/bin/bash + +echo "Running Stump pre-setup script! This will install the necessary development tools/dependencies required to develop Stump." +echo + +which cargo &> /dev/null +if [ $? -eq 1 ]; then + echo "Rust could not be found on your system. Please ensure the 'rustc' and 'cargo' binaries are in your \$PATH." + exit 1 +else + echo "Rust found on your system." +fi + +which node &> /dev/null +if [ $? -eq 1 ]; then + echo "Node could not be found on your system. Please ensure the 'node'command is in your \$PATH." + exit 1 +else + echo "Node found on your system." +fi + +echo + + +which pnpm &> /dev/null +if [ $? -eq 1 ]; then + echo "pnpm could not be found on your system. Would you like for this script to attempt to install 'pnpm'? (y/n)" + + can_continue=false + until [ $can_continue = true ]; do + read -p "Choice: " choice + + case $choice in + y) + echo "Attempting to install 'pnpm'..." + npm install -g pnpm + if [ $? -eq 0 ]; then + echo "pnpm installed successfully." + can_continue=true + else + echo "pnpm could not be installed. Please ensure you have node and npm installed." + can_continue=false + exit 1 + fi + ;; + n) + echo "Skipping 'pnpm' installation. Exiting." + can_continue=false + exit 1 + ;; + *) + echo "Invalid choice. Please enter 'y' or 'n'." + can_continue=false + ;; + esac + + echo + echo "Would you like for this script to attempt to install 'pnpm'? (y/n)" + done +else + echo "pnpm found on your system." +fi + +echo + +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + if which apt-get &> /dev/null; then + echo "Detected 'apt' based distro!" + sudo apt-get -y update + sudo apt-get -y install "pkg-config libssl-dev" + elif which pacman &> /dev/null; then + echo "Detected 'pacman' based distro!" + sudo pacman -Syu + sudo pacman -S --needed base-devel openssl libssl-dev + elif which dnf &> /dev/null; then + echo "Detected 'dnf' based distro!" + sudo dnf check-update + sudo dnf install "openssl-devel" + sudo dnf group install "C Development Tools and Libraries" # GCC C/C++ compilers, autoconf, automake, make, etc.,. + else + echo "Your Linux distro '$(lsb_release -s -d)' is not supported by the pre-setup script. Please consider adding support for it: https://github.com/aaronleopold/stump/issues" + exit 1 + fi + + echo "Running 'pnpm core setup':" + echo + + cargo install cargo-watch + pnpm run setup + + echo + echo "Pre-setup completed! Be sure to run 'pnpm core seed' to finish the setup." +elif [[ "$OSTYPE" == "darwin"* ]]; then + echo + echo "Running 'pnpm core setup':" + echo + + cargo install cargo-watch + pnpm run setup + + echo + echo "Pre-setup completed! Be sure to run 'pnpm core seed' to finish the setup." +else + echo "Your OS '$OSTYPE' is not supported by the pre-setup script. Please consider adding support for it: https://github.com/aaronleopold/stump/issues" + exit 1 +fi \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..ad0e8a533 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "Prisma.prisma", + "matklad.rust-analyzer", + "bradlc.vscode-tailwindcss", + "Gruntfuggly.todo-tree" + ] +} diff --git a/Cargo.lock b/Cargo.lock index 292f41d1b..a0b9ed6d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -461,6 +461,45 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "3.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "lazy_static", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -838,6 +877,15 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-sys" version = "0.3.7" @@ -2413,6 +2461,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" + [[package]] name = "parking_lot" version = "0.10.2" @@ -2789,6 +2843,30 @@ dependencies = [ "uuid", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -3229,7 +3307,8 @@ dependencies = [ [[package]] name = "rocket-session-store" version = "0.2.0" -source = "git+https://github.com/Aurora2500/rocket-session-store.git#a64386dfdc5fdcc726c4c24d21011fc9db689a19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d8391c9df8d2b21eaa16ff3787b5d6927a77ee1d6d5b086968918e37c7131a" dependencies = [ "rand 0.8.5", "rocket", @@ -3785,6 +3864,8 @@ dependencies = [ "async-trait", "base64 0.13.0", "bcrypt", + "clap", + "dirs", "dotenv", "epub", "infer", @@ -3856,6 +3937,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + [[package]] name = "thiserror" version = "1.0.31" diff --git a/Dockerfile b/Dockerfile index 1e0007da3..d241b7211 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,4 +55,8 @@ RUN chown stump:stump stump USER stump +ENV STUMP_CONFIG_DIR=/config +ENV STUMP_DATA_DIR=/data +ENV ROCKET_PROFILE=release + CMD ["./stump"] diff --git a/README.md b/README.md index 18303abf6..a077f197f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,15 @@

Stump logo +
+ + + + + +

-A free and open source comics server with OPDS support, **heavily** inspired by [Komga](https://github.com/gotson/komga), created with Rust, [Rocket](https://github.com/SergioBenitez/Rocket), [Prisma](https://github.com/Brendonovich/prisma-client-rust) and React. +Stump is a free and open source comics server with OPDS support, **heavily** inspired by [Komga](https://github.com/gotson/komga), created with Rust, [Rocket](https://github.com/SergioBenitez/Rocket), [Prisma](https://github.com/Brendonovich/prisma-client-rust) and React. I love Komga and use it at home, and I thought it would be cool to learn what goes into making something like this myself. I opted to develop this in Rust to hopefully, at the end of all this, create something just as if not almost as convenient but with a smaller footprint. _I also just want to practice Rust!_ @@ -23,6 +30,10 @@ I'll list the major target features below - I am very open to suggestions and id - Server management - Built-in webreader for media - Role-based access control (i.e. the server owner and authorized users) +- Language support (currently only English) + - Once more of the core features are implemented, I'll be prioritizing language support + +You can track the development of this project [here](https://github.com/users/aaronleopold/projects/2) ## Project Structure @@ -36,9 +47,9 @@ I am ommitting a lot of files and only focusing on the main directories, but the │   │   └── server # stump core implementation │   │   ├── prisma # prisma configuration │   │   ├── prisma-cli # prisma CLI configuration -│   │   ├── src # source code -│   │      ├── bin # bin rust files -│   │         ├── seed.rs # seed database with fake data +│   │   └── src # source code +│   │      └── bin # bin rust files +│   │         └── seed.rs # seed database with fake data │   └── website # the advertisement website code ├── README.md └── ... @@ -48,15 +59,58 @@ I am ommitting a lot of files and only focusing on the main directories, but the ## Development Setup -Clone the repository and run the setup script: +There is now a setup script that handles most of the initial configuration, however for simplicity I recommend ensuring you at least have the basics: [pnpm](https://pnpm.io/installation), [rust](https://www.rust-lang.org/tools/install) and [node](https://nodejs.org/en/download/). The script may ask to attempt installing `pnpm` using `npm` if it is not found in your $PATH. + +**Ensure you are on the `develop` branch before continuing.** + +### Setup Script + +If you are on a Windows machine, you'll need to run the following: + +``` +.\.github\scripts\setup.ps1 +``` + +Otherwise, you can run the following: ```bash -git clone https://github.com/aaronleopold/stump.git -cd stump -pnpm run setup +./.github/scripts/setup.sh ``` -This will install all the dependencies, build the frontend bundle (required for server to start), generate the prisma client, and seed the database with some fake data. +These scripts will run system checks for `cargo` and `pnpm`, and will install a few additional dependencies, depending on your system. It will then install all the direct, Stump development dependencies, build the frontend bundle (required for server to start), generate the prisma client and sqlite database. + +If you face any issues running these, or are using a system that is not supported by the setup scripts, please consider [adding/improving support](https://github.com/aaronleopold/stump/issues) for your system. + +### Running the Seed Script + +During the next step, a seed will be run to create basic data for testing. At some point, this will not be a requirement, but for now, it is. If you are running the seed script, you can run the following for instructions: + +```bash +cargo seed --help +``` + +In general, you should provide a `library_path` argument, which is the path to a Library directory on your system. This 'Library' should contain your folders that represent series. It will default to `$HOME/Documents/Stump`. You may provide a `user_name` argument, which will be the username of the server owner. Default will be 'oromei' with a password of 'oromei'. Specifiying a username will still yield an **equivalent** password. + +An example folder structure for a one-library collection might be: + +``` +/Users/aaronleopold/Documents +├── Stump +│   ├── Marvel Comics +│   │   ├── The Amazing Spider-Man (2018) +│   │   │ ├── The Amazing Spider-Man 001 (2018).cbz +│   │   │ ├── The Amazing Spider-Man 002 (2018).cbz +│   │   │ └── etc. +│   │   └── The Amazing Spider-Man (2022) +│   │   ├── The Amazing Spider-Man 001 (2022).cbz +│   │   ├── The Amazing Spider-Man 002 (2022).cbz +│   │   └── etc. +│   └── EBooks +│   ├── Martin, George R R - [Song of Ice and Fire 3] - A Storm of Swords (2003).epub +│   ├── Tolkien, J R R - [The Lord of the Rings] - Hobbit Or There and Back Again (1986).epub +│   └── etc. +└── ... +``` ## Running Stump @@ -77,7 +131,7 @@ pnpm core frontend:dev # start the frontend
- Note: This is currently non-functional. Migrating to Prisma from SeaORM bork this, but I am working on it. + Note: This is not currently configured properly. Migrating to Prisma from SeaORM bork this, but I am working on it. No images have been published to dockerhub yet, so you'll have to build it yourself: @@ -105,3 +159,16 @@ As of now, you'll need to make the `source` and `target` paths match. So if you ## Contributing Contributions are very **encouraged** and **welcome**! Please review the [CONTRIBUTING.md](./CONTRIBUTING.md) file beforehand. Thanks! + +#### Developer Resources + +A few useful resources for developers looking to contribute: + +- [Rocket documentation](https://rocket.rs/v0.5-rc/) +- [Prisma documentation](https://prisma.io/docs/prisma-client/introduction) + - [Prisma Client Rust Documentation](https://github.com/Brendonovich/prisma-client-rust/tree/main/docs) +- [Chakra UI Documentation](https://chakra-ui.com/docs) +- [OPDS specification](https://specs.opds.io/) +- [OPDS Page Streaming](https://vaemendis.net/opds-pse/#:~:text=The%20OPDS%20Page%20Streaming%20Extension,having%20to%20download%20it%20completely.) +- [Getting started with React](https://reactjs.org/docs/getting-started.html) +- [Rust Book](https://doc.rust-lang.org/book/) diff --git a/package.json b/package.json index adace8391..ba73f2887 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "setup": "pnpm website install && pnpm core run setup", "website": "pnpm --filter @stump/website --", "core": "pnpm --filter @stump/core --", + "prisma": "pnpm core prisma", "prepare": "husky install" }, "devDependencies": { @@ -22,6 +23,5 @@ ".rs": [ "cargo fmt --manifest-path=core/server/Cargo.toml --" ] - }, - "dependencies": {} + } } \ No newline at end of file diff --git a/packages/core/frontend/public/favicon.ico b/packages/core/frontend/public/favicon.ico index 2b249ca56..29e1498fe 100644 Binary files a/packages/core/frontend/public/favicon.ico and b/packages/core/frontend/public/favicon.ico differ diff --git a/packages/core/frontend/public/favicon.png b/packages/core/frontend/public/favicon.png index e93e8d74f..0ea4f365b 100644 Binary files a/packages/core/frontend/public/favicon.png and b/packages/core/frontend/public/favicon.png differ diff --git a/packages/core/frontend/public/stump-logo--irregular-lg.png b/packages/core/frontend/public/stump-logo--irregular-lg.png new file mode 100644 index 000000000..0ea4f365b Binary files /dev/null and b/packages/core/frontend/public/stump-logo--irregular-lg.png differ diff --git a/packages/core/frontend/public/stump-logo--irregular-sm.png b/packages/core/frontend/public/stump-logo--irregular-sm.png new file mode 100644 index 000000000..d1ca9d91f Binary files /dev/null and b/packages/core/frontend/public/stump-logo--irregular-sm.png differ diff --git a/packages/core/frontend/public/stump-logo--irregular-xs.png b/packages/core/frontend/public/stump-logo--irregular-xs.png new file mode 100644 index 000000000..12b158583 Binary files /dev/null and b/packages/core/frontend/public/stump-logo--irregular-xs.png differ diff --git a/packages/core/frontend/public/stump-logo--irregular.png b/packages/core/frontend/public/stump-logo--irregular.png new file mode 100644 index 000000000..0ea4f365b Binary files /dev/null and b/packages/core/frontend/public/stump-logo--irregular.png differ diff --git a/packages/core/frontend/public/stump-logo--square.png b/packages/core/frontend/public/stump-logo--square.png new file mode 100644 index 000000000..8fdc6fd09 Binary files /dev/null and b/packages/core/frontend/public/stump-logo--square.png differ diff --git a/packages/core/frontend/src/App.tsx b/packages/core/frontend/src/App.tsx index ed41bb570..4522339d0 100644 --- a/packages/core/frontend/src/App.tsx +++ b/packages/core/frontend/src/App.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { QueryClientProvider } from 'react-query'; -import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; import client from '~api/client'; import ErrorBoundary from '~components/ErrorBoundary'; import BaseLayout from '~components/Layouts/BaseLayout'; @@ -11,7 +11,7 @@ import StoreProvider from '~store/StoreProvider'; import theme from '~util/theme'; import { ChakraProvider } from '@chakra-ui/react'; -import JobOverlay from '~components/JobOverlay'; +// import JobOverlay from '~components/JobOverlay'; import { Helmet, HelmetTags } from 'react-helmet'; import { useStore } from '~store/store'; @@ -22,6 +22,8 @@ const BookOverview = React.lazy(() => import('~pages/Book/BookOverview')); const ReadBook = React.lazy(() => import('~pages/Book/ReadBook')); const Login = React.lazy(() => import('~pages/Auth/Login')); const Settings = React.lazy(() => import('~pages/Settings')); +const GeneralSettings = React.lazy(() => import('~pages/Settings/GeneralSettings')); +const ServerSettings = React.lazy(() => import('~pages/Settings/ServerSettings')); export default function Root() { return ( @@ -65,7 +67,11 @@ function App() { }> } /> - } /> + }> + } /> + } /> + } /> + } /> } /> } /> diff --git a/packages/core/frontend/src/api/mutation/library.ts b/packages/core/frontend/src/api/mutation/library.ts index 7f718c575..fda073b18 100644 --- a/packages/core/frontend/src/api/mutation/library.ts +++ b/packages/core/frontend/src/api/mutation/library.ts @@ -2,5 +2,5 @@ import API from '..'; // TODO: type this export function scanLibary(id: string): Promise { - return API.get(`/library/${id}/scan`); + return API.get(`/libraries/${id}/scan`); } diff --git a/packages/core/frontend/src/components/Settings/SettingsNav.tsx b/packages/core/frontend/src/components/Settings/SettingsNav.tsx index 0f08ccabf..7736b8804 100644 --- a/packages/core/frontend/src/components/Settings/SettingsNav.tsx +++ b/packages/core/frontend/src/components/Settings/SettingsNav.tsx @@ -1,21 +1,52 @@ -import { Stack, Text, useColorModeValue } from '@chakra-ui/react'; -import React from 'react'; +import { Box, Tab, TabList, Tabs } from '@chakra-ui/react'; +import React, { useMemo } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +const pages = [ + { + path: '/settings/general', + shortName: 'general', + index: 0, + }, + { + path: '/settings/server', + shortName: 'server', + index: 1, + }, +]; export default function SettingsNav() { + const navigate = useNavigate(); + + const location = useLocation(); + + const activeTab = useMemo( + () => pages.find((p) => p.path === location.pathname)?.index ?? 0, + [location], + ); + + function handleChange(index: number) { + const page = pages.find((p) => p.index === index); + + if (page && index !== activeTab) { + navigate(`/settings/${page.shortName}`); + } + } + return ( - - User Preferences - Server Settings - + + + + General Settings + Server Settings + + + ); } diff --git a/packages/core/frontend/src/components/Sidebar/Sidebar.tsx b/packages/core/frontend/src/components/Sidebar/Sidebar.tsx index 327e50829..6facc5f60 100644 --- a/packages/core/frontend/src/components/Sidebar/Sidebar.tsx +++ b/packages/core/frontend/src/components/Sidebar/Sidebar.tsx @@ -148,15 +148,23 @@ export function SidebarContent() { // This kinda makes me hate chakra return ( <> - - + + Stump diff --git a/packages/core/frontend/src/pages/Settings.tsx b/packages/core/frontend/src/pages/Settings.tsx deleted file mode 100644 index dbe5975e5..000000000 --- a/packages/core/frontend/src/pages/Settings.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Box, Flex, useColorModeValue } from '@chakra-ui/react'; -import React from 'react'; -import BaseLayout from '~components/Layouts/BaseLayout'; -import SettingsNav from '~components/Settings/SettingsNav'; - -export default function Settings() { - return ( - - {/* */} - - - f - - - ); -} diff --git a/packages/core/frontend/src/pages/Settings/GeneralSettings.tsx b/packages/core/frontend/src/pages/Settings/GeneralSettings.tsx new file mode 100644 index 000000000..6c37da2e5 --- /dev/null +++ b/packages/core/frontend/src/pages/Settings/GeneralSettings.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function GeneralSettings() { + return
General
; +} diff --git a/packages/core/frontend/src/pages/Settings/ServerSettings.tsx b/packages/core/frontend/src/pages/Settings/ServerSettings.tsx new file mode 100644 index 000000000..0348ad2ea --- /dev/null +++ b/packages/core/frontend/src/pages/Settings/ServerSettings.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export default function ServerSettings() { + return
Server
; +} \ No newline at end of file diff --git a/packages/core/frontend/src/pages/Settings/index.tsx b/packages/core/frontend/src/pages/Settings/index.tsx new file mode 100644 index 000000000..ff447bfba --- /dev/null +++ b/packages/core/frontend/src/pages/Settings/index.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Box, Flex, useColorModeValue } from '@chakra-ui/react'; +import SettingsNav from '~components/Settings/SettingsNav'; +import { Helmet } from 'react-helmet'; +import { Outlet } from 'react-router-dom'; + +export default function Settings() { + return ( + <> + + Stump | {'Settings'} + + + + + + + + + ); +} diff --git a/packages/core/frontend/src/util/theme.ts b/packages/core/frontend/src/util/theme.ts index 1bacc378e..bda29eea3 100644 --- a/packages/core/frontend/src/util/theme.ts +++ b/packages/core/frontend/src/util/theme.ts @@ -38,6 +38,10 @@ const colors = { 800: '#1A202C', 850: '#191D28', 900: '#171923', + 950: '#11121A', + 1000: '#0B0C11', + 1050: '#050507', + 1100: '#010101', }, }; diff --git a/packages/core/frontend/tailwind.config.js b/packages/core/frontend/tailwind.config.js index 2ed86c716..4febce9e2 100644 --- a/packages/core/frontend/tailwind.config.js +++ b/packages/core/frontend/tailwind.config.js @@ -42,6 +42,10 @@ module.exports = { 800: '#1A202C', 850: '#191D28', 900: '#171923', + 950: '#11121A', + 1000: '#0B0C11', + 1050: '#050507', + 1100: '#010101', }, brand, }, diff --git a/packages/core/package.json b/packages/core/package.json index 7e078bb8b..fda221df3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -5,13 +5,13 @@ "author": "Aaron Leopold ", "license": "MIT", "scripts": { - "start": "cd server && cargo run --release", + "start": "cd server && ROCKET_PROFILE=release cargo run --release", "setup": "pnpm install && pnpm frontend:install && pnpm frontend:build && pnpm server:setup", - "server:setup": "cargo install cargo-watch && pnpm prisma generate && pnpm prisma db push && pnpm seed", + "server:setup": "pnpm prisma generate && pnpm prisma db push", "dev": "concurrently -n server,frontend -c green.bold,blue.bold \"pnpm server:dev\" \"pnpm frontend:dev\"", "build:docker": "docker build -t stump .", - "server:start": "cd server && cargo run", - "server:dev": "cd server && cargo watch -x run", + "server:start": "cd server && ROCKET_PROFILE=debug cargo run", + "server:dev": "cd server && ROCKET_PROFILE=debug cargo watch -x run", "server:build": "cd server && cargo build --release", "server:check": "cd server && cargo check", "server:migrate-up": "cd server && sea-orm-cli migrate up", @@ -20,12 +20,13 @@ "frontend:install": "pnpm --filter frontend install", "frontend:start": "pnpm --filter frontend start", "frontend:dev": "pnpm --filter frontend dev", - "frontend:move-build": "rm -rf ./server/static && mv ./frontend/build ./server/static", + "frontend:move-build": "trash ./server/static && mv ./frontend/build ./server/static", "frontend:build": "pnpm --filter frontend build && pnpm frontend:move-build", "prisma": "cd server && cargo prisma", "seed": "cd server && cargo seed" }, "devDependencies": { - "concurrently": "^6.5.1" + "concurrently": "^6.5.1", + "trash-cli": "^5.0.0" } } \ No newline at end of file diff --git a/packages/core/server/Cargo.toml b/packages/core/server/Cargo.toml index ee0bd088d..3c093c2bb 100644 --- a/packages/core/server/Cargo.toml +++ b/packages/core/server/Cargo.toml @@ -4,15 +4,13 @@ version = "0.1.0" edition = "2021" default-run = "stump" -# [workspace] -# members = ["prisma-cli"] - [dependencies] prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.4.1" } serde = { version = "1.0", features = ["derive"] } dotenv = "0.15.0" -rocket = { version = "0.5.0-rc.1", features = ["json" ] } -rocket-session-store = { git = "https://github.com/Aurora2500/rocket-session-store.git" } +# env_logger = "0.9.0" +rocket = { version = "0.5.0-rc.2", features = ["json" ] } +rocket-session-store = "0.2.0" rocket_cors = "0.6.0-alpha1" bcrypt = "0.10.1" anyhow = "1.0.57" @@ -27,6 +25,16 @@ walkdir = "2.3.2" zip = "0.5.13" # unrar = "0.4.4" unrar = { git = "https://github.com/aaronleopold/unrar.rs", branch = "aleopold--read-bytes" } -# unrar = { path = "/Users/aaronleopold/Documents/sandbox/clones/unrar.rs" } async-trait = "0.1.53" infer = "0.7.0" +dirs = "4.0.0" + +# Using clap to make the seed more configurable +clap = { version = "3.1.18", features = ["derive"], optional = true } + +[features] +seed-binary = ["clap"] + +[[bin]] +name = "seed" +required-features = ["seed-binary"] diff --git a/packages/core/server/Rocket.toml b/packages/core/server/Rocket.toml index fa3b0b4c4..e2af1fce6 100644 --- a/packages/core/server/Rocket.toml +++ b/packages/core/server/Rocket.toml @@ -8,14 +8,6 @@ port = 6969 keep_alive = 5 log_level = "normal" - -[staging] -address = "0.0.0.0" -port = 6969 -keep_alive = 5 -log_level = "normal" - - [release] address = "0.0.0.0" port = 6969 diff --git a/packages/core/server/prisma/schema.prisma b/packages/core/server/prisma/schema.prisma index abd4dfe58..c5a539ad2 100644 --- a/packages/core/server/prisma/schema.prisma +++ b/packages/core/server/prisma/schema.prisma @@ -46,6 +46,8 @@ model User { @@map("users") } +// TODO: access control for individual libraries. E.g. a managed user account that may only +// access libraries a, b and c. model Library { id String @id @default(uuid()) // The name of the library. ex: "Marvel Comics" diff --git a/packages/core/server/src/bin/seed.rs b/packages/core/server/src/bin/seed.rs index 9d590f192..0436f2ea4 100644 --- a/packages/core/server/src/bin/seed.rs +++ b/packages/core/server/src/bin/seed.rs @@ -10,203 +10,96 @@ use prisma::{library, media, read_progress, series, user}; use prisma_client_rust::serde_json; use serde::{Deserialize, Serialize}; +use clap::Parser; + // this is not big brain solution but I am lazy fn join_path(base: &str, rest: &str) -> String { - let mut path = String::from(base); - - if !path.ends_with('/') { - path.push('/'); - } + let mut path = String::from(base); - path.push_str(rest); + if !path.ends_with('/') { + path.push('/'); + } - path -} + path.push_str(rest); -#[derive(Serialize, Deserialize)] -struct MockMedia { - name: String, - description: Option, - size: i32, - extension: String, - pages: i32, - path: String, + path } -// TODO: remove some of the code duplication here when creating media - -async fn create_series_media( - client: &prisma::PrismaClient, - media_json: Vec, - series_id: String, -) -> Result, prisma_client_rust::query::Error> { - let mut ret = vec![]; - - for m in media_json { - ret.push( - client - .media() - .create( - media::name::set(m.name), - media::size::set(m.size), - media::extension::set(m.extension), - media::pages::set(m.pages), - media::path::set(m.path), - vec![ - media::description::set(m.description), - media::series::link(series::id::equals(series_id.clone())), - ], - ) - .exec() - .await?, - ); - } - - Ok(ret) +/// Seed program for Stump development. +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Args { + /// Name of the library to seed. + #[clap(short, long)] + library_path: Option, + + /// Name of the managing user account to seed. + #[clap(short, long)] + user_name: Option, } #[tokio::main] async fn main() -> Result<(), Box> { - println!("Starting seed."); - - let client = prisma::new_client().await?; - - let oromei = client - .user() - .create( - user::username::set(String::from("oromei")), - user::hashed_password::set( - bcrypt::hash("oromei", 12).expect("Could not hash password"), - ), - vec![user::role::set(String::from("ServerOwner"))], - ) - .exec() - .await?; - - println!("Created user: {} - {}", &oromei.username, &oromei.id); - - let comics_library = client - .library() - .create( - library::name::set(String::from("Marvel Comics")), - library::path::set(String::from( - "/Users/aaronleopold/Documents/stump_tests/Marvel Comics", - )), - vec![], - ) - .exec() - .await?; - - println!( - "Created library: {} - {}", - &comics_library.name, &comics_library.id - ); - - let amazing_spiderman = client - .series() - .create( - series::name::set(String::from("Amazing Spiderman (2018)")), - series::path::set(String::from(join_path( - &comics_library.path, - "Amazing Spiderman (2018)", - ))), - vec![series::library::link(library::id::equals( - comics_library.id.clone(), - ))], - ) - .exec() - .await?; - - println!( - "Created series: {} - {}", - &amazing_spiderman.name, &amazing_spiderman.id - ); - - let amazing_spiderman_file = std::fs::File::open("src/bin/amazing-spiderman.json")?; - - let amazing_spiderman_json: Vec = - serde_json::from_reader(BufReader::new(amazing_spiderman_file))?; - - let amazing_spiderman_books = create_series_media( - &client, - amazing_spiderman_json, - amazing_spiderman.id.clone(), - ) - .await?; - - println!("Created media for series: {}", &amazing_spiderman.id); - - let spiderman_blue = client - .series() - .create( - series::name::set(String::from("Spider-Man - Blue")), - series::path::set(String::from(join_path( - &comics_library.path, - "Spider-Man - Blue", - ))), - vec![series::library::link(library::id::equals( - comics_library.id.clone(), - ))], - ) - .exec() - .await?; - - println!( - "Created series: {} - {}", - &spiderman_blue.name, &spiderman_blue.id - ); - - let spiderman_blue_file = std::fs::File::open("src/bin/spiderman-blue.json")?; - - let spiderman_blue_json: Vec = - serde_json::from_reader(BufReader::new(spiderman_blue_file))?; - - let _spiderman_blue_books = - create_series_media(&client, spiderman_blue_json, spiderman_blue.id.clone()).await?; - - println!("Created media for series: {}", &spiderman_blue.id); - - let venom = client - .series() - .create( - series::name::set(String::from("Venom")), - series::path::set(String::from(join_path(&comics_library.path, "Venom"))), - vec![series::library::link(library::id::equals( - comics_library.id.clone(), - ))], - ) - .exec() - .await?; - - println!("Created series: {} - {}", &venom.name, &venom.id); - - let venom_file = std::fs::File::open("src/bin/venom.json")?; - - let venom_json: Vec = serde_json::from_reader(BufReader::new(venom_file))?; - - let venom_books = create_series_media(&client, venom_json, venom.id.clone()).await?; - - println!("Created media for series: {}", &venom.id); - - client.read_progress().create( - read_progress::page::set(2), - read_progress::media::link(media::id::equals( - amazing_spiderman_books.get(0).unwrap().id.clone(), - )), - read_progress::user::link(user::id::equals(oromei.id.clone())), - vec![], - ); - - client.read_progress().create( - read_progress::page::set(6), - read_progress::media::link(media::id::equals(venom_books.get(0).unwrap().id.clone())), - read_progress::user::link(user::id::equals(oromei.id.clone())), - vec![], - ); - - println!("Marked two books as in progress."); - - println!("Seed completed."); - - Ok(()) + println!("Starting seed..."); + + let args = Args::parse(); + + let library_path = match args.library_path { + Some(path) => path, + None => { + let home = dirs::home_dir().expect("Could not find home directory"); + + // Default library will be what I use for testing + join_path(&home.to_str().unwrap(), "Documents/Stump/Marvel Comics") + }, + }; + + let library_name = library_path.split('/').last().unwrap(); + + let user_name = match args.user_name { + Some(name) => name, + None => "oromei".to_string(), + }; + + println!( + "Seed configured: library={}, user={}", + &library_path, &user_name + ); + + let client = prisma::new_client().await?; + + let user = client + .user() + .create( + user::username::set(user_name.clone()), + user::hashed_password::set( + bcrypt::hash(user_name.clone(), 12).expect("Could not hash password"), + ), + vec![user::role::set(String::from("SERVER_OWNER"))], + ) + .exec() + .await?; + + println!("Created user: {} - {}", &user.username, &user.id); + + let comics_library = client + .library() + .create( + library::name::set(String::from("Marvel Comics")), + library::path::set(library_path.clone()), + vec![], + ) + .exec() + .await?; + + println!( + "Created library: {} - {}", + &comics_library.name, &comics_library.id + ); + + println!("\nSeed completed."); + + println!("Be sure to spawn a 'ScannerJob' to populate the library: POST /api/libraries/{}/scan, or you may use the UI", comics_library.id); + + Ok(()) } diff --git a/packages/core/server/src/config/context.rs b/packages/core/server/src/config/context.rs index 35203c8d8..931bc9cfa 100644 --- a/packages/core/server/src/config/context.rs +++ b/packages/core/server/src/config/context.rs @@ -6,6 +6,7 @@ use rocket::tokio::sync::{ }; use crate::{ + db, job::Job, prisma, types::event::{InternalEvent, InternalTask, TaskResponder}, @@ -27,11 +28,7 @@ pub struct Context { impl Context { pub async fn new(event_sender: EventSender, task_sender: TaskSender) -> Context { Context { - db: Arc::new( - prisma::new_client() - .await - .expect("Failed to create Prisma client"), - ), + db: Arc::new(db::create_client().await), event_sender: Arc::new(event_sender), task_sender: Arc::new(task_sender), client_channel: Arc::new(channel::(1024)), diff --git a/packages/core/server/src/config/logging.rs b/packages/core/server/src/config/logging.rs new file mode 100644 index 000000000..c4623f0f5 --- /dev/null +++ b/packages/core/server/src/config/logging.rs @@ -0,0 +1,52 @@ +// extern crate log; + +use std::io::Write; + +pub fn init() { + // let mut builder = env_logger::Builder::new(); + + // let verbosity = match std::env::var("STUMP_LOG_LEVEL") { + // Ok(verbosity) => match verbosity.to_lowercase().as_str() { + // "verbose" => "VERBOSE", + // "internal" => "INTERNAL", + // "none" => "NONE", + // _ => "INTERNAL", + // }, + // Err(_) => "INTERNAL", + // }; + + // match verbosity { + // "INTERNAL" => { + // builder + // .filter_level(log::LevelFilter::Info) + // .filter_module("tracing", log::LevelFilter::Off) + // .filter_module("quaint", log::LevelFilter::Off) + // .filter_module("sql_query_connector", log::LevelFilter::Off) + // .filter_module("query_core", log::LevelFilter::Off) + // .filter_module("prisma-client-rust", log::LevelFilter::Off) + // .filter_module("prisma", log::LevelFilter::Off); + // }, + // "VERBOSE" => { + // builder.filter_level(log::LevelFilter::Info); + // }, + // "NONE" => { + // builder.filter_level(log::LevelFilter::Off); + // }, + // _ => { + // builder.filter_level(log::LevelFilter::Info); + // }, + // }; + + // if verbosity != "NONE" { + // builder.format(|buf, record| { + // let style = buf.style(); + // // style.set_bg(Color::Yellow).set_bold(true); + // // let timestamp = buf.timestamp(); + // writeln!(buf, ">> {}", style.value(record.args())) + // }); + // } + + // builder.init(); + + // env_logger::init(); +} diff --git a/packages/core/server/src/config/mod.rs b/packages/core/server/src/config/mod.rs index b378e5383..48eea7241 100644 --- a/packages/core/server/src/config/mod.rs +++ b/packages/core/server/src/config/mod.rs @@ -1,4 +1,8 @@ pub mod context; pub mod cors; pub mod helmet; +pub mod logging; pub mod session; + +// TODO: look into this +// https://api.rocket.rs/v0.5-rc/rocket/struct.Config.html#method.figment diff --git a/packages/core/server/src/db/migration.rs b/packages/core/server/src/db/migration.rs new file mode 100644 index 000000000..5166583ad --- /dev/null +++ b/packages/core/server/src/db/migration.rs @@ -0,0 +1,5 @@ +use crate::prisma; + +pub async fn run_migrations(_db: &prisma::PrismaClient) { + unimplemented!() +} diff --git a/packages/core/server/src/db/mod.rs b/packages/core/server/src/db/mod.rs new file mode 100644 index 000000000..78ef0ce85 --- /dev/null +++ b/packages/core/server/src/db/mod.rs @@ -0,0 +1,44 @@ +pub mod migration; + +use std::path::PathBuf; + +use crate::prisma; + +/// Creates the Stump data directory relative to the home directory of the host +/// OS. If the directory does not exist, it will be created. +fn create_config_dir() -> String { + let db_path = match std::env::var("STUMP_CONFIG_DIR") { + Ok(path) => PathBuf::from(&path), + // .join("stump.db"), + _ => dirs::home_dir() + .expect("Failed to get data dir") + .join(".stump"), // .join("stump.db"), + }; + + let path_str = db_path.to_str().unwrap(); + + std::fs::create_dir_all(&path_str).unwrap(); + + path_str.to_string() +} + +/// Creates the PrismaClient. Will call `create_data_dir` as well +pub async fn create_client() -> prisma::PrismaClient { + let rocket_env = + std::env::var("ROCKET_PROFILE").unwrap_or_else(|_| "debug".to_string()); + + // .expect("ROCKET_PROFILE not set"); + + if rocket_env == "release" { + let config_dir = create_config_dir(); + + prisma::new_client_with_url(&format!("file:{}/stump.db", &config_dir)) + .await + .expect("Failed to create Prisma client") + } else { + // Development database will live in the /prisma directory + prisma::new_client() + .await + .expect("Failed to create Prisma client") + } +} diff --git a/packages/core/server/src/fs/scanner.rs b/packages/core/server/src/fs/scanner.rs index 8bc1872cf..6a900b964 100644 --- a/packages/core/server/src/fs/scanner.rs +++ b/packages/core/server/src/fs/scanner.rs @@ -53,12 +53,14 @@ impl Job for ScannerJob { } }; - // TODO: remove this + // TODO: removed this ctx.emit_client_event(format!("Scanning library: {}", library.path.clone())); - let _scanner = Scanner::new(library, ctx.db.clone()); + let mut scanner = Scanner::new(library, ctx.db.clone()); - todo!("scanner.scan()"); + scanner.scan_library().await; + + Ok(()) } } @@ -91,7 +93,40 @@ impl ScannedFileTrait for Path { } fn should_ignore(&self) -> bool { - !self.is_visible_file() + if !self.is_visible_file() { + log::info!("Ignoring hidden file: {}", self.display()); + return true; + } + + let kind = infer::get_from_path(self); + + if kind.is_err() { + log::info!("Could not infer file type for {:?}: {:?}", self, kind); + return true; + } + + let kind = kind.unwrap(); + + match kind { + Some(k) => { + let mime = k.mime_type(); + + match mime { + "application/zip" => false, + "application/vnd.rar" => false, + "application/epub+zip" => false, + "application/pdf" => false, + _ => { + log::info!("Ignoring file {:?} with mime type {}", self, mime); + true + }, + } + }, + None => { + log::info!("Unable to infer file type: {:?}", self); + return true; + }, + } } fn dir_has_media(&self) -> bool { @@ -169,26 +204,6 @@ impl Scanner { .map(|(_, s)| s.id.clone()) } - // async fn create_series(&self, path: &Path, library_id: i32) -> Option { - // let series = generate_series_model(path, library_id); - - // match series.insert(self.db).await { - // Ok(m) => { - // log::info!("Created new series: {:?}", m); - // self.event_handler - // .emit_event(Event::series_created(m.clone())); - // Some(m) - // } - // Err(err) => { - // log::error!("Failed to create series: {:?}", err); - // self.event_handler.log_error(err.to_string()); - // None - // } - // } - // } - - // - fn process_entry(&self, entry: &DirEntry) -> ProcessResult { match entry.file_name().to_str() { Some(name) if name.ends_with("cbr") => process_rar(entry), @@ -262,7 +277,7 @@ impl Scanner { Ok(media) } - async fn insert_series(&self, entry: &DirEntry) { + async fn insert_series(&self, entry: &DirEntry) -> Result { let path = entry.path(); let metadata = match path.metadata() { @@ -270,32 +285,37 @@ impl Scanner { _ => None, }; - // // TODO: remove the unsafe unwraps throughout this file - // let name = path.file_name().unwrap().to_str().unwrap().to_string(); + // TODO: change error + let name = match path.file_name() { + Some(name) => match name.to_str() { + Some(name) => name.to_string(), + _ => return Err(ScanError::Unknown("Failed to get name".to_string())), + }, + _ => return Err(ScanError::Unknown("Failed to get name".to_string())), + }; + + let series = self + .db + .series() + .create( + series::name::set(name), + series::path::set(path.to_str().unwrap().to_string()), + vec![series::library::link(library::id::equals( + self.library.id.clone(), + ))], + ) + .exec() + .await?; - // let mut updated_at: Option = None; + let files_to_process = WalkDir::new(&self.library.path) + .into_iter() + .filter_map(|e| e.ok()) + .count(); - // if let Some(metadata) = metadata { - // // TODO: extract to fn somewhere - // updated_at = match metadata.modified() { - // Ok(st) => { - // let dt: DateTime = st.clone().into(); - // Some(dt.naive_utc()) - // } - // Err(_) => Some(Utc::now().naive_utc()), - // }; - // } + // TODO: send progress + // self.on_progress(vec![]) - // series::ActiveModel { - // library_id: Set(library_id), - // title: Set(name), - // updated_at: Set(updated_at), - // // TODO: do I want this to throw an error? - // path: Set(path.to_str().unwrap_or("").to_string()), - // // FIXME: this should be handled by default but isn't, see https://github.com/SeaQL/sea-orm/issues/420 ? - // status: Set(FileStatus::Ready), - // ..Default::default() - // } + Ok(series) } pub async fn scan_library(&mut self) { @@ -304,35 +324,43 @@ impl Scanner { let mut visited_series = HashMap::::new(); let mut visited_media = HashMap::::new(); - for entry in WalkDir::new(&self.library.path) + for (i, entry) in WalkDir::new(&self.library.path) .into_iter() .filter_map(|e| e.ok()) + .enumerate() { let entry_path = entry.path(); log::info!("Scanning: {:?}", entry_path); + // TODO: send progress (use i) + // self.on_progress(vec![]) + let series = self.get_series(&entry_path); let series_exists = series.is_some(); if entry_path.is_dir() && !series_exists { if !entry_path.dir_has_media() { + log::info!("Skipping empty directory: {:?}", entry_path); // TODO: send progress continue; } log::info!("Creating new series: {:?}", entry_path); - // match self.create_series(path, library.id).await { - // Some(s) => { - // visited_series.insert(s.id, true); - // self.series.insert(s.path.clone(), s); - // } - // // Error handled in the function call - // None => {} - // } + match self.insert_series(&entry).await { + Ok(series) => { + visited_series.insert(series.id.clone(), true); + self.series_map.insert(series.path.clone(), series); + }, + Err(e) => { + log::error!("Failed to insert series: {:?}", e); + // TODO: send progress + // self.on_progress(vec![]) + }, + } - // continue; + continue; } if series_exists { @@ -344,6 +372,7 @@ impl Scanner { continue; } else if entry_path.should_ignore() { + log::info!("Skipping ignored file: {:?}", entry_path); // TODO: send progress continue; } else if let Some(media) = self.get_media(&entry_path) { @@ -365,8 +394,7 @@ impl Scanner { match self.insert_media(&entry, series_id).await { Ok(media) => { visited_media.insert(media.id.clone(), true); - // FIXME: ruh roh, this won't work but *do I need it to??* - // self.media.insert(m.path.clone(), m); + self.media_map.insert(media.path.clone(), media); }, Err(e) => { log::error!("Failed to insert media: {:?}", e); @@ -413,176 +441,6 @@ impl Scanner { } } -// TODO: reimplement this, from before migration -// use std::{ -// collections::HashMap, -// path::{Path, PathBuf}, -// }; - -// use chrono::{DateTime, NaiveDateTime, Utc}; -// use entity::{ -// library, media, -// sea_orm::{self, ActiveModelTrait}, -// series, -// util::FileStatus, -// }; - -// use sea_orm::{DatabaseConnection, Set}; - -// use walkdir::{DirEntry, WalkDir}; - -// use crate::{ -// database::queries, -// event::{event::Event, handler::EventHandler}, -// fs::{ -// epub::process_epub, error::ProcessFileError, media_file::ProcessResult, rar::process_rar, -// zip::process_zip, -// }, -// types::{ -// comic::ComicInfo, -// dto::{GetMediaQuery, GetMediaQueryResult}, -// }, -// State, -// }; - -// // TODO: use ApiErrors here! - -// pub trait IgnoredFile { -// fn should_ignore(&self) -> bool; -// } - -// impl IgnoredFile for Path { -// fn should_ignore(&self) -> bool { -// let filename = self -// .file_name() -// .unwrap_or_default() -// .to_str() -// .expect(format!("Malformed filename: {:?}", self.as_os_str()).as_str()); - -// if self.is_dir() { -// return true; -// } else if filename.starts_with(".") { -// return true; -// } - -// false -// } -// } - -// // TODO: error handling / return result -// fn generate_series_model(path: &Path, library_id: i32) -> series::ActiveModel { - -// } - -// // TODO: result return to handle error downstream -// fn generate_media_model(entry: &DirEntry, series_id: i32) -> Option { -// let processed_info = process_entry(entry); - -// if let Err(e) = processed_info { -// // log::info!("{:?}", e); -// return None; -// } - -// let (info, pages) = processed_info.unwrap(); - -// let path = entry.path(); - -// let metadata = match entry.metadata() { -// Ok(metadata) => Some(metadata), -// _ => None, -// }; - -// let path_str = path.to_str().unwrap().to_string(); -// let name = entry.file_name().to_str().unwrap().to_string(); -// let ext = path.extension().unwrap().to_str().unwrap().to_string(); - -// let comic_info = match info { -// Some(info) => info, -// None => ComicInfo::default(), -// }; - -// let mut size: u64 = 0; -// let mut modified: Option = None; - -// if let Some(metadata) = metadata { -// size = metadata.len(); - -// modified = match metadata.modified() { -// Ok(st) => { -// let dt: DateTime = st.clone().into(); -// Some(dt.naive_utc()) -// } -// Err(_) => Some(Utc::now().naive_utc()), -// }; -// } - -// Some(media::ActiveModel { -// series_id: Set(series_id), -// name: Set(name), -// description: Set(comic_info.summary), -// size: Set(size as i64), -// extension: Set(ext), -// pages: Set(match comic_info.page_count { -// Some(count) => count as i32, -// None => pages.len() as i32, -// }), -// updated_at: Set(modified), -// path: Set(path_str), -// status: Set(FileStatus::Ready), -// ..Default::default() -// }) -// } - -// fn dir_has_files(path: &Path) -> bool { -// let items = std::fs::read_dir(path); - -// if items.is_err() { -// return false; -// } - -// let items = items.unwrap(); - -// items -// .filter_map(|item| item.ok()) -// .any(|f| !f.path().should_ignore()) -// } - -// struct Scanner<'a> { -// pub db: &'a DatabaseConnection, -// pub event_handler: &'a EventHandler, -// pub series: HashMap, -// pub media: HashMap, -// } - -// impl<'a> Scanner<'a> { -// pub fn new( -// db: &'a DatabaseConnection, -// event_handler: &'a EventHandler, -// series: Vec, -// media: GetMediaQueryResult, -// ) -> Self { -// let mut media_map = HashMap::new(); -// let mut series_map = HashMap::new(); - -// for m in media { -// // media_map.insert(media.checksum.clone(), media); -// media_map.insert(m.path.clone(), m); -// } - -// for s in series { -// series_map.insert(s.path.clone(), s); -// } - -// Self { -// db, -// event_handler, -// series: series_map, -// media: media_map, -// } -// } - -// // FIXME: pass in &Model?? -// // TODO: make me // async fn analyze_media(&self, key: String) { // let media = self.media.get(&key).unwrap(); @@ -599,29 +457,6 @@ impl Scanner { // // TODO: more checks?? // } -// fn get_series(&self, path: &Path) -> Option<&series::Model> { -// self.series.get(path.to_str().expect("Invalid key")) -// } - -// fn get_media(&self, key: &Path) -> Option<&GetMediaQuery> { -// self.media.get(key.to_str().expect("Invalid key")) -// } - -// // TODO: I'm not sure I love this solution. I could check if the Path.parent is the series path, but -// // not sure about that either -// /// Get the series id for the given path. Used to determine the series of a media -// /// file at a given path. -// fn get_series_id(&self, path: &Path) -> Option { -// self.series -// .iter() -// .find(|(_, s)| path.to_str().unwrap_or("").to_string().contains(&s.path)) -// .map(|(_, s)| s.id) -// } - -// fn series_exists(&self, path: &Path) -> bool { -// self.get_series(path).is_some() -// } - // async fn set_media_status(&self, id: i32, status: FileStatus, path: String) { // match queries::media::set_status(self.db, id, status).await { // Ok(_) => { @@ -652,117 +487,6 @@ impl Scanner { // } // } -// pub async fn scan_library(&mut self, library: &library::Model) { -// let library_path = PathBuf::from(&library.path); - -// let mut visited_series = HashMap::::new(); -// let mut visited_media = HashMap::::new(); - -// for entry in WalkDir::new(&library.path) -// .into_iter() -// .filter_map(|e| e.ok()) -// { -// let path = entry.path(); - -// log::info!("Current: {:?}", path); - -// let series = self.get_series(&path); -// let series_exists = series.is_some(); - -// if path.is_dir() && !series_exists { -// if path.to_path_buf().eq(&library_path) && !dir_has_files(path) { -// log::info!("Skipping library directory - contains no files."); -// continue; -// } - -// log::info!("Creating new series: {:?}", path); - -// match self.create_series(path, library.id).await { -// Some(s) => { -// visited_series.insert(s.id, true); -// self.series.insert(s.path.clone(), s); -// } -// // Error handled in the function call -// None => {} -// } - -// continue; -// } - -// if series_exists { -// let series = series.unwrap(); -// log::info!("Existing series: {:?}", series); -// visited_series.insert(series.id, true); -// continue; -// } else if path.should_ignore() { -// // log::info!("Ignoring: {:?}", path); -// continue; -// } - -// if let Some(media) = self.get_media(&path) { -// // log::info!("Existing media: {:?}", media); -// visited_media.insert(media.id, true); -// // self.analyze_media(media).await; -// continue; -// } - -// // TODO: don't do this :) -// let series_id = self.get_series_id(&path).expect(&format!( -// "Could not determine series for new media: {:?}", -// path -// )); - -// log::info!("New media at {:?} in series {:?}", &path, series_id); - -// match self.create_media(&entry, series_id).await { -// Some(m) => { -// visited_media.insert(m.id, true); -// // FIXME: ruh roh, this won't work but *do I need it to??* -// // self.media.insert(m.path.clone(), m); -// } -// // Error handled in the function call -// None => {} -// } -// } - -// for (_, s) in self.series.iter() { -// match visited_series.get(&s.id) { -// Some(true) => { -// if s.status == FileStatus::Missing { -// self.set_series_status(s.id, FileStatus::Ready, s.path.clone()) -// .await; -// } -// } -// _ => { -// if s.library_id == library.id { -// log::info!("MOVED/MISSING SERIES: {}", s.path); -// self.set_series_status(s.id, FileStatus::Missing, s.path.clone()) -// .await; -// } -// } -// } -// } - -// for media in self.media.values() { -// match visited_media.get(&media.id) { -// Some(true) => { -// if media.status == FileStatus::Missing { -// self.set_media_status(media.id, FileStatus::Ready, media.path.clone()) -// .await; -// } -// } -// _ => { -// if media.library_id == library.id { -// log::info!("MOVED/MISSING MEDIA: {}", media.path); -// self.set_media_status(media.id, FileStatus::Missing, media.path.clone()) -// .await; -// } -// } -// } -// } -// } -// } - // pub async fn scan(state: &State, library_id: Option) -> Result<(), String> { // let conn = state.get_connection(); // let event_handler = state.get_event_handler(); diff --git a/packages/core/server/src/main.rs b/packages/core/server/src/main.rs index f92a817e4..49d01c2c5 100644 --- a/packages/core/server/src/main.rs +++ b/packages/core/server/src/main.rs @@ -4,7 +4,7 @@ extern crate rocket; #[cfg(debug_assertions)] use dotenv::dotenv; -use config::{context::Context, cors, helmet::Helmet, session}; +use config::{context::Context, cors, helmet::Helmet, logging, session}; use rocket::{ fs::{FileServer, NamedFile}, tokio::{self, sync::mpsc::unbounded_channel}, @@ -17,6 +17,7 @@ use types::{ use utils::event::EventManager; pub mod config; +pub mod db; pub mod fs; pub mod guards; pub mod job; @@ -43,6 +44,8 @@ async fn rocket() -> _ { #[cfg(debug_assertions)] dotenv().ok(); + // logging::init(); + // Channel to handle internal events let event_channel = unbounded_channel::(); diff --git a/packages/core/server/src/routes/api/library.rs b/packages/core/server/src/routes/api/library.rs index 6b688574b..019cb9e8a 100644 --- a/packages/core/server/src/routes/api/library.rs +++ b/packages/core/server/src/routes/api/library.rs @@ -47,11 +47,11 @@ pub async fn get_library_by_id( } // TODO: write me -#[get("/library//scan")] +#[get("/libraries//scan")] pub async fn scan_library( id: String, ctx: &Context, - _auth: Auth, + // _auth: Auth, TODO: uncomment ) -> Result<(), ApiError> { let db = ctx.get_db(); @@ -87,7 +87,7 @@ pub struct CreateLibrary { } /// Create a new library. Will queue a ScannerJob to scan the library, and return the library -#[post("/library", data = "")] +#[post("/libraries", data = "")] pub async fn create_library( input: Json, ctx: &Context, @@ -121,7 +121,7 @@ pub struct UpdateLibrary { /// Update a library. // TODO: Scan? -#[put("/library/", data = "")] +#[put("/libraries/", data = "")] pub async fn update_library( id: String, input: Json, @@ -152,7 +152,7 @@ pub async fn update_library( } // TODO: check the deletion of a library properly cascades to all series and media within it. -#[delete("/library/")] +#[delete("/libraries/")] pub async fn delete_library( id: String, ctx: &Context, diff --git a/packages/website/@types/index.d.ts b/packages/website/@types/index.d.ts new file mode 100644 index 000000000..ee464739e --- /dev/null +++ b/packages/website/@types/index.d.ts @@ -0,0 +1 @@ +type PropsOf> = React.ComponentPropsWithoutRef; diff --git a/packages/website/package.json b/packages/website/package.json index 88f4c154b..9e3a84a29 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -3,7 +3,7 @@ "private": true, "version": "0.0.0", "scripts": { - "dev": "vite", + "dev": "vite --host", "build": "vite build", "preview": "vite preview" }, @@ -19,6 +19,7 @@ "react": "^18.0.0", "react-dom": "^18.0.0", "react-helmet": "^6.1.0", + "react-intersection-observer": "^9.1.0", "react-router-dom": "^6.3.0", "tailwind-scrollbar-hide": "^1.1.7" }, diff --git a/packages/website/public/favicon.ico b/packages/website/public/favicon.ico index 2b249ca56..29e1498fe 100644 Binary files a/packages/website/public/favicon.ico and b/packages/website/public/favicon.ico differ diff --git a/packages/website/public/favicon.png b/packages/website/public/favicon.png index e93e8d74f..0ea4f365b 100644 Binary files a/packages/website/public/favicon.png and b/packages/website/public/favicon.png differ diff --git a/packages/website/public/favicon_old.ico b/packages/website/public/favicon_old.ico new file mode 100644 index 000000000..2b249ca56 Binary files /dev/null and b/packages/website/public/favicon_old.ico differ diff --git a/packages/website/public/favicon_old.png b/packages/website/public/favicon_old.png new file mode 100644 index 000000000..e93e8d74f Binary files /dev/null and b/packages/website/public/favicon_old.png differ diff --git a/packages/website/public/chunky-reader-logo.png b/packages/website/public/images/chunky-reader-logo.png similarity index 100% rename from packages/website/public/chunky-reader-logo.png rename to packages/website/public/images/chunky-reader-logo.png diff --git a/packages/website/public/kybook-logo.png b/packages/website/public/images/kybook-logo.png similarity index 100% rename from packages/website/public/kybook-logo.png rename to packages/website/public/images/kybook-logo.png diff --git a/packages/website/public/moon-reader-logo.png b/packages/website/public/images/moon-reader-logo.png similarity index 100% rename from packages/website/public/moon-reader-logo.png rename to packages/website/public/images/moon-reader-logo.png diff --git a/packages/website/public/images/panels/iphone-12--black.png b/packages/website/public/images/panels/iphone-12--black.png new file mode 100644 index 000000000..f93ad3f1c Binary files /dev/null and b/packages/website/public/images/panels/iphone-12--black.png differ diff --git a/packages/website/public/images/panels/iphone-12--black@2x.png b/packages/website/public/images/panels/iphone-12--black@2x.png new file mode 100644 index 000000000..6deb0d3ab Binary files /dev/null and b/packages/website/public/images/panels/iphone-12--black@2x.png differ diff --git a/packages/website/public/images/panels/panels-logo--black-text.jpg b/packages/website/public/images/panels/panels-logo--black-text.jpg new file mode 100644 index 000000000..5177e242d Binary files /dev/null and b/packages/website/public/images/panels/panels-logo--black-text.jpg differ diff --git a/packages/website/public/images/panels/panels-logo--black.png b/packages/website/public/images/panels/panels-logo--black.png new file mode 100644 index 000000000..fba62d52d Binary files /dev/null and b/packages/website/public/images/panels/panels-logo--black.png differ diff --git a/packages/website/public/images/panels/panels-logo--white-text.jpeg b/packages/website/public/images/panels/panels-logo--white-text.jpeg new file mode 100644 index 000000000..4ebdb173f Binary files /dev/null and b/packages/website/public/images/panels/panels-logo--white-text.jpeg differ diff --git a/packages/website/public/images/panels/panels-logo--white.png b/packages/website/public/images/panels/panels-logo--white.png new file mode 100644 index 000000000..6d8e94b07 Binary files /dev/null and b/packages/website/public/images/panels/panels-logo--white.png differ diff --git a/packages/website/public/images/pixel-5--TODO.png b/packages/website/public/images/pixel-5--TODO.png new file mode 100644 index 000000000..80659bdf3 Binary files /dev/null and b/packages/website/public/images/pixel-5--TODO.png differ diff --git a/packages/website/public/panels-logo.png b/packages/website/public/panels-logo.png deleted file mode 100644 index 157d75117..000000000 Binary files a/packages/website/public/panels-logo.png and /dev/null differ diff --git a/packages/website/src/components/Features.tsx b/packages/website/src/components/Features.tsx deleted file mode 100644 index 989421c9a..000000000 --- a/packages/website/src/components/Features.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; - -function ReaderCompatibility() { - return ( -
-

- Compatible with your favorite readers -

-

- You aren't stuck with the built-in reader! With Stump you can easily read all your digial - media from your preferred reader - so long as they support the OPDS specifications, it's - compatible! -

-
- ); -} - -function FormatSupport() { - return ( -
-
-

Emphasis on format support

-

- While not every digital media format is compatible with the OPDS specifications, Stump's - built-in readers support a wide range of formats. -

-
- -
-
something
-
-
- ); -} - -export default function Features() { - return ( -
- - -
- ); -} diff --git a/packages/website/src/components/Footer.tsx b/packages/website/src/components/Footer.tsx index 6c10e8039..a7ab3ce84 100644 --- a/packages/website/src/components/Footer.tsx +++ b/packages/website/src/components/Footer.tsx @@ -1,6 +1,7 @@ import clsx from 'clsx'; import React from 'react'; import { Twitter, Discord, Github } from '@icons-pack/react-simple-icons'; +import { Link } from 'react-router-dom'; const navigation = { about: [ @@ -33,6 +34,7 @@ const navigation = { name: 'Twitter', href: '#', icon: Twitter, + disabled: true, }, { name: 'GitHub', @@ -61,8 +63,8 @@ const LinkSection = ({ title, links }: LinkSectionProps) => { diff --git a/packages/website/src/components/Hero.tsx b/packages/website/src/components/Hero.tsx index d10a5e94d..2a885dbc3 100644 --- a/packages/website/src/components/Hero.tsx +++ b/packages/website/src/components/Hero.tsx @@ -38,7 +38,7 @@ export default function Hero() { className="flex" initial={{ opacity: 0, scale: 0.5 }} animate={{ opacity: 1, scale: 1 }} - transition={{ duration: 0.5, delay: 1.15 }} + transition={{ duration: 0.5, delay: 1.25 }} >