diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 04e6f2614..b303e108c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -6,65 +6,36 @@ To ensure nobody's time and effort is wasted, please be sure to follow the guide 1. Check to see if an issue already exists relevant to your feature/topic 2. Create an issue (if an issue does not already exist) and express interest in working it (see the [issues](#issues) section below) -3. Create a new feature branch **off of the `develop` branch** - _not_ `main`. All PRs should be made against `develop`. +3. Create a new feature branch **off of the `experimental` or `develop` branches** - _not_ `main`. All PRs should be made against either `experimental` or `develop`. 4. Add appropiate documentation, tests, etc, if necessary. 5. Ensure you have your code formatters properly configured (both Prettier and Rustfmt). 6. Once you've completed your changes, create your PR! -7. Follow the PR naming format to help ensure the changelog generator properly picks up your additions: +7. Follow the PR naming format outlined at [gitmoji.dev](https://gitmoji.dev/specification), used for more uniform generation of release notes - > :information_source: Honestly, don't stress about this part right now. I don't even have a changelog generator!! This kind of structure will only matter once releases are more regular and providing changelogs are more important. For now, just make sure your PR name and body is descriptive and concise :heart: - - ``` - : - ``` - - Where `type` is one of the following: - - - `feat`: A new feature - - `fix`: A bug fix - - `docs`: Documentation only changes - - `refactor`: A code change that neither fixes a bug nor adds a feature - - `perf`: A code change that improves performance - - `test`: Adding missing tests or correcting existing tests - - `ci`: Changes to our CI configuration files and scripts - - `chore`: Other changes that don't modify `src` or `test` files, such as updating `package.json` or `README.md` - - `revert`: Reverts a previous commit - - `WIP`: Work in progress - - The `description` should contain a _succinct_ description of the change: - - - use the imperative, present tense: "change" not "changed" nor "changes" - - don't capitalize the first letter - - no dot (.) at the end - - Examples: - - ``` - feat: add support for Reading Lists - fix: remove broken link - docs: update CONTRIBUTING.md to include PR naming format - ``` + > :information_source: Don't stress too much about this part. Just make sure your PR name and body is descriptive and concise, :heart: 8. Stick around and make sure your PR passes all checks and gets merged! ## Issues -I use GitHub issues to track bugs, feature requests, and other tasks. No rigid structure is enforced, but please try and follow these guidelines: +I use GitHub issues to track bugs, feature requests, and other tasks. No rigid structure is enforced, but please try to fill out the templates fully as best you can. Generally, it is useful to include the following information: + +- Docker tag (or commit hash displayed in settings) +- Log output (server logs, browser console, etc) +- Access method (browser on host machine, mobile on network, etc) +- Network logs (network tab) and details (reverse proxy, VPN, etc) + +If you're not sure if an issue is relevant or appropriate, e.g. if you have more of a question to ask, feel free to pop in the [Discord](https://discord.gg/63Ybb7J3as) and ask! -- Please try and be as descriptive as possible when opening an issue. -- There are a few templates available to help guide you, but if you're not sure which one to use just use the "Blank Issue" template. - - If you're opening an issue to request a feature, please try and explain why you think it would be a good addition to the project. If applicable, include example use cases. - - If you're opening an issue to report a bug, please try and include a minimal reproduction of the bug (video, code, logs, etc). - - If you're not sure if an issue is relevant or appropriate, e.g. if you have more of a question to ask, feel free to pop in the [Discord](https://discord.gg/63Ybb7J3as) and ask! -- **Please don't ghost an issue you've been assigned** - if you're no longer interested in working on it, that is totally okay! Just leave a comment on the issue so that I know you're no longer interested and I can reassign it to someone else. I will never be offended if you no longer want to work on an issue - I'm just trying to make sure that nobody's time and effort is wasted. +## Pull Requests -## A note on merging +> :information_source: There are two development branches: `experimental` and `develop`. These correspond to the `experimental` and `nightly` tags on Docker Hub, respectively. In general, `experimental` is for large or breaking changes, while `develop` is for smaller, more incremental changes. PRs will be merged once the following criteria are met: - All CI checks pass - At least one _maintainer_ has reviewed your PR -All PRs to `develop` will be squashed. All PRs to `main` will be merge commits. This is to ensure that the commit history is clean and easy to follow, and to ensure that the changelog generator works properly. +All PRs to `experimental` will be squashed. All PRs to `develop` from `experimental` and to `main` will be merge commits. This is to ensure that the commit history is clean and easy to follow, and to ensure that the changelog generator works properly. Thanks for considering contributing to Stump! :heart: diff --git a/README.md b/README.md index 182e2b9d7..c528a8ad4 100644 --- a/README.md +++ b/README.md @@ -129,14 +129,14 @@ And that's it! #### Where to start? -If you aren't sure where to start, I recommend taking a look at [open issues](https://github.com/stumpapp/stump/issues). You can also check out the [milestones](https://github.com/stumpapp/stump/milestones) page for a more curated list of issues that need to be addressed. +If you aren't sure where to start, I recommend taking a look at [open issues](https://github.com/stumpapp/stump/issues). You can also check out the [current project board](https://github.com/orgs/stumpapp/projects/4) to see what's actively being worked on or planned. In general, the following areas are good places to start: - Translation, so Stump is accessible to as many people as possible - - [Crowdin](https://crowdin.com/project/stump) is being used for translations + - [Crowdin](https://crowdin.com/project/stump) is used for translations - Writing comprehensive tests -- Designing UI elements/sections or improving the existing UI/UX +- Designing and/improving UI/UX - Docker build optimizations, caching, etc - CI pipelines, automated releases and release notes, etc - And lots more! @@ -155,7 +155,7 @@ Stump has a monorepo structure managed by [yarn workspaces](https://yarnpkg.com/ Stand-alone applications that can be run independently, at `/apps` in the root of the project: - `desktop`: A React + Tauri desktop application -- `mobile`: A React Native application ([#125](https://github.com/stumpapp/stump/issues/125)) +- `expo`: A React Native application ([#125](https://github.com/stumpapp/stump/issues/125)) - `server`: An [Axum](https://github.com/tokio-rs/axum) HTTP server - `web`: A React application, the primary UI for both the built-in web app the server serves and the desktop app diff --git a/apps/server/src/http_server.rs b/apps/server/src/http_server.rs index 4e1289685..2220495a1 100644 --- a/apps/server/src/http_server.rs +++ b/apps/server/src/http_server.rs @@ -64,7 +64,7 @@ pub async fn run_http_server(config: StumpConfig) -> ServerResult<()> { let addr = SocketAddr::from(([0, 0, 0, 0], config.port)); tracing::info!("⚡️ Stump HTTP server starting on http://{}", addr); - // TODO: might need to refactor to use https://docs.rs/async-shutdown/latest/async_shutdown/ + // TODO: Refactor to use https://docs.rs/async-shutdown/latest/async_shutdown/ let cleanup = || async move { println!("Initializing graceful shutdown..."); diff --git a/apps/server/src/routers/api/v1/user.rs b/apps/server/src/routers/api/v1/user.rs index 0d413a02d..173a17d05 100644 --- a/apps/server/src/routers/api/v1/user.rs +++ b/apps/server/src/routers/api/v1/user.rs @@ -637,12 +637,14 @@ async fn get_navigation_arrangement( let user_preferences = db .user_preferences() - .find_unique(user_preferences::user_id::equals(user.id.clone())) + .find_first(vec![user_preferences::user::is(vec![user::id::equals( + user.id.clone(), + )])]) .exec() .await? .ok_or(APIError::NotFound(format!( - "User preferences with id {} not found", - user.id + "User preferences for {} not found", + user.username )))?; let user_preferences = UserPreferences::from(user_preferences); @@ -672,12 +674,16 @@ async fn update_navigation_arrangement( let user_preferences = db .user_preferences() - .find_unique(user_preferences::user_id::equals(user.id.clone())) + // TODO: Really old accounts potentially have users with preferences missing a `user_id` + // assignment. This should be more properly fixed in the future, e.g. by a migration. + .find_first(vec![user_preferences::user::is(vec![user::id::equals( + user.id.clone(), + )])]) .exec() .await? .ok_or(APIError::NotFound(format!( - "User preferences with id {} not found", - user.id + "User preferences for {} not found", + user.username )))?; let user_preferences = UserPreferences::from(user_preferences); diff --git a/core/src/config/stump_config.rs b/core/src/config/stump_config.rs index 128e9e26b..26f084319 100644 --- a/core/src/config/stump_config.rs +++ b/core/src/config/stump_config.rs @@ -1,8 +1,14 @@ +//! Contains the [StumpConfig] struct and related functions for loading and saving configuration +//! values for a Stump application. +//! +//! Note: [StumpConfig] is constructed _before_ tracing is initializing. This is because the +//! configuration is used to determine the log file path and verbosity level. This means that any +//! logging that occurs during the construction of the [StumpConfig] should be done using the +//! standard `println!` or `eprintln!` macros. use std::{env, path::PathBuf}; use itertools::Itertools; use serde::{Deserialize, Serialize}; -use tracing::debug; use crate::error::{CoreError, CoreResult}; @@ -154,7 +160,7 @@ impl StumpConfig { let toml_content_str = std::fs::read_to_string(stump_toml)?; let toml_configs = toml::from_str::(&toml_content_str) .map_err(|e| { - tracing::error!(error = ?e, "Failed to parse Stump.toml"); + eprintln!("Failed to parse Stump.toml: {}", e); CoreError::InitializationError(e.to_string()) })?; @@ -171,13 +177,13 @@ impl StumpConfig { if profile == "release" || profile == "debug" { env_configs.profile = Some(profile); } else { - debug!("Invalid PROFILE value: {}", profile); + eprintln!("Invalid PROFILE value: {}", profile); } } if let Ok(port) = env::var(PORT_KEY) { let port_u16 = port.parse::().map_err(|e| { - tracing::error!(error = ?e, port, "Failed to parse provided STUMP_PORT"); + eprintln!("Failed to parse provided STUMP_PORT: {}", e); CoreError::InitializationError(e.to_string()) })?; env_configs.port = Some(port_u16); @@ -185,11 +191,7 @@ impl StumpConfig { if let Ok(verbosity) = env::var(VERBOSITY_KEY) { let verbosity_u64 = verbosity.parse::().map_err(|e| { - tracing::error!( - error = ?e, - verbosity, - "Failed to parse provided STUMP_VERBOSITY" - ); + eprintln!("Failed to parse provided STUMP_VERBOSITY: {}", e); CoreError::InitializationError(e.to_string()) })?; env_configs.verbosity = Some(verbosity_u64); @@ -197,11 +199,7 @@ impl StumpConfig { if let Ok(pretty_logs) = env::var(PRETTY_LOGS_KEY) { let pretty_logs_bool = pretty_logs.parse::().map_err(|e| { - tracing::error!( - error = ?e, - pretty_logs, - "Failed to parse provided STUMP_PRETTY_LOGS" - ); + eprintln!("Failed to parse provided STUMP_PRETTY_LOGS: {}", e); CoreError::InitializationError(e.to_string()) })?; self.pretty_logs = pretty_logs_bool; @@ -249,16 +247,16 @@ impl StumpConfig { if let Ok(session_ttl) = env::var(SESSION_TTL_KEY) { match session_ttl.parse() { Ok(val) => env_configs.session_ttl = Some(val), - Err(e) => tracing::error!(?e, "Failed to parse provided SESSION_TTL"), + Err(e) => eprintln!("Failed to parse provided SESSION_TTL: {}", e), } } if let Ok(session_expiry_interval) = env::var(SESSION_EXPIRY_INTERVAL_KEY) { match session_expiry_interval.parse() { Ok(val) => env_configs.expired_session_cleanup_interval = Some(val), - Err(e) => tracing::error!( - ?e, - "Failed to parse provided SESSION_EXPIRY_CLEANUP_INTERVAL" + Err(e) => eprintln!( + "Failed to parse provided SESSION_EXPIRY_CLEANUP_INTERVAL: {}", + e ), } } @@ -266,9 +264,7 @@ impl StumpConfig { if let Ok(scanner_chunk_size) = env::var(SCANNER_CHUNK_SIZE_KEY) { match scanner_chunk_size.parse() { Ok(val) => self.scanner_chunk_size = val, - Err(e) => { - tracing::error!(?e, "Failed to parse provided SCANNER_CHUNK_SIZE") - }, + Err(e) => eprintln!("Failed to parse provided SCANNER_CHUNK_SIZE: {}", e), } } @@ -327,7 +323,7 @@ impl StumpConfig { std::fs::write( stump_toml.as_path(), toml::to_string(&self).map_err(|e| { - tracing::error!(error = ?e, "Failed to serialize StumpConfig to toml"); + eprintln!("Failed to serialize StumpConfig to toml: {}", e); CoreError::InitializationError(e.to_string()) })?, )?; @@ -441,7 +437,7 @@ impl PartialStumpConfig { if profile == "release" || profile == "debug" { config.profile = profile; } else { - debug!("Invalid PROFILE value: {}", profile); + eprintln!("Invalid PROFILE value: {}", profile); } } @@ -672,6 +668,42 @@ mod tests { .expect("Failed to delete temporary directory"); } + #[test] + fn test_simulate_first_boot() { + env::set_var(PORT_KEY, "1337"); + env::set_var(VERBOSITY_KEY, "2"); + env::set_var(DISABLE_SWAGGER_KEY, "true"); + env::set_var(HASH_COST_KEY, "1"); + + let tempdir = tempfile::tempdir().expect("Failed to create temporary directory"); + // Now we can create a StumpConfig rooted at the temporary directory + let config_dir = tempdir.path().to_string_lossy().to_string(); + let generated = StumpConfig::new(config_dir.clone()) + .with_config_file() + .expect("Failed to generate StumpConfig from Stump.toml") + .with_environment() + .expect("Failed to generate StumpConfig from environment"); + + let expected = StumpConfig { + profile: "debug".to_string(), + port: 1337, + verbosity: 2, + pretty_logs: true, + db_path: None, + client_dir: "./dist".to_string(), + config_dir, + allowed_origins: vec![], + pdfium_path: None, + disable_swagger: true, + password_hash_cost: 1, + session_ttl: DEFAULT_SESSION_TTL, + expired_session_cleanup_interval: DEFAULT_SESSION_EXPIRY_CLEANUP_INTERVAL, + scanner_chunk_size: DEFAULT_SCANNER_CHUNK_SIZE, + }; + + assert_eq!(generated, expected); + } + fn get_mock_config_file() -> String { let mock_config_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("integration-tests/data/mock-stump.toml"); diff --git a/core/src/db/entity/user/permissions.rs b/core/src/db/entity/user/permissions.rs index 86fa06615..a2d06a529 100644 --- a/core/src/db/entity/user/permissions.rs +++ b/core/src/db/entity/user/permissions.rs @@ -19,6 +19,7 @@ impl From for AgeRestriction { } } +// TODO: consider separating some of the `manage` permissions into more granular permissions // TODO: consider adding self:update permission, useful for child accounts /// Permissions that can be granted to a user. Some permissions are implied by others, /// and will be automatically granted if the "parent" permission is granted. @@ -58,7 +59,6 @@ pub enum UserPermission { /// Grant access to delete the library (manage library) #[serde(rename = "library:delete")] DeleteLibrary, - // TODO: ReadUsers, CreateUsers, ManageUsers /// Grant access to read users. /// /// Note that this is explicitly for querying users via user-specific endpoints. @@ -143,6 +143,7 @@ impl ToString for UserPermission { } } +// TODO: refactor to remove panic :grimace: impl From<&str> for UserPermission { fn from(s: &str) -> UserPermission { match s { diff --git a/docs/pages/guides/access-control/age-restrictions.mdx b/docs/pages/guides/access-control/age-restrictions.mdx index 7578f263b..696fd457b 100644 --- a/docs/pages/guides/access-control/age-restrictions.mdx +++ b/docs/pages/guides/access-control/age-restrictions.mdx @@ -3,9 +3,8 @@ import { Callout } from 'nextra-theme-docs' # Age Restrictions - This functionality is experimental. Please ensure you properly test any configured age - restrictions to ensure they work as expected with your library **before** you give the restricted - user access to their account. + Please ensure you properly test any configured age restrictions to ensure they work as expected + with your library **before** you give the restricted user access to their account. Age restrictions are set on a per-user basis, and are used to determine whether or not a user can access a book. For more information on users and user management, see the dedicated [users](/guides/access-control/users) page. @@ -40,6 +39,13 @@ The age restriction is located directly within the metadata for a book itself, o This means that **without metadata, Stump cannot determine whether or not a book is age-allowed**. There are fallback options described below. +#### EPUB + +Stump will attempt to parse one of the following from an EPUB file's metadata: + +- `typicalAgeRange`: Generally in the format of `[number]-[number]` +- `contentRating`: Subject to the publisher, but generally similar to that you'd find on a movie or TV show + ### How does Stump determine whether or not a book is age-allowed? If a book or a book's series has an age restriction set, Stump will use that age restriction to determine whether or not a user can access it. The comparison done internally is `less than or equal to X number`, meaning that if a user has an age restriction set to `13` and a book is rated to `17`, the user will not be able to access the book. If a user has an age restriction set to `17` and a book is rated to `13`, the user will be able to access the book. In other words, **the age restriction set on the user must be greater than or equal to the age restriction number set on the book or series in order to have access**. @@ -53,7 +59,8 @@ Stump doesn't currently support editing metadata directly, but it is planned for ### Other considerations -- Stump doesn't currently support dynamic thumbnails for libraries containing age-restricted books or series. This means that if it happens to be the case that the first book in the first series of a library is age-restricted, the thumbnail for the library **will still be displayed** so long as a user has access to the library. This is planned to be fixed in the future by one of two ways: - 1. Settings will be made available to override the thumbnail for a library or series - 2. Server owners will be able to associate libraries and series with tags and then set restrictions on a user that would prevent access to specific tags. See the [Tag-based restrictions](#tag-based-restrictions) section for more information. - 3. Server owners can exclude users from seeing certain libraries entirely +#### Library thumbnails + +If you generated a library thumbnail for a library which coincidentally contains an age-restricted book that is ordered first, the thumbnail will still be displayed so long as a user has access to the library. + +You can get around this by setting a the thumbnail to source from a different book, or uploading a custom thumbnail. diff --git a/docs/pages/guides/basics/scanner.mdx b/docs/pages/guides/basics/scanner.mdx index 889174328..01f35c01c 100644 --- a/docs/pages/guides/basics/scanner.mdx +++ b/docs/pages/guides/basics/scanner.mdx @@ -92,17 +92,10 @@ For convenience, there are a few preset options you may select from the dropdown > In the future, this section of the UI will change to include scheduling options for more than just scans. However, for now, it is only for scans. -## Ignore Files +## Ignoring files -Stump has minimal support for a custom `.stumpignore` file, which allows you to ignore certain files and directories from being scanned. This is useful for files which are organized with your media, but you don't want to be included in the library. - -Some examples you can achieve with this: - -```bash -# Ignore all files in the "extras" directory -extras/ -# Ignore all files in the "extras" directory, except for "extras/include-me.cbz" -!extras/include-me.cbz -``` - -Please note, that in the above example, if you exclude an entire directory and explicitly include a file in that directory, a series will still potentially be created for that directory depending on which [library pattern](/guides/libraries#library-patterns) you have configured. If it does get created, `include-me.cbz` will be the only file in the series. + + Stump has removed support for `.stumpignore` files in favor of a more robust configuration system. + The corresponding GitHub issue for tracking is + [#284](https://github.com/stumpapp/stump/issues/284) + diff --git a/docs/pages/guides/mobile/app.mdx b/docs/pages/guides/mobile/app.mdx index 3a5ff4ed6..4f1be9f8c 100644 --- a/docs/pages/guides/mobile/app.mdx +++ b/docs/pages/guides/mobile/app.mdx @@ -5,7 +5,7 @@ import { Callout } from 'nextra-theme-docs' A mobile app is currently in the **very early** stages of development, thanks primarily to the efforts of two contributors! If you are a mobile developer and would like to help out, please feel free to reach out. -If you're interested in updates, you can track the broad progress on the [mobile-app feature branch](https://github.com/stumpapp/stump/tree/mobile-app) on GitHub. As development progresses a bit more to an MVP, I'll be sure to add a dedicated project for better tracking +If you're interested in updates, you can track the broad progress via the [are we mobile yet](https://github.com/orgs/stumpapp/projects/8) project board. diff --git a/docs/pages/installation/docker.mdx b/docs/pages/installation/docker.mdx index 2f3996916..5cca8f8c3 100644 --- a/docs/pages/installation/docker.mdx +++ b/docs/pages/installation/docker.mdx @@ -26,6 +26,13 @@ echo -e "PUID=$(id -u)\nPGID=$(id -g)" + + This tutorial uses the newer `docker compose` CLI. If you find this command does not exist for + you, you might be on V1, which uses `docker-compose`. Please review [Docker's + documentation](https://docs.docker.com/compose/install/) for more information and/or + platform-specific installation. + + ### Create a `docker-compose.yml` file @@ -36,12 +43,12 @@ Below is an example of a Docker Compose file you can use to bootstrap your Stump version: '3.3' services: stump: - image: aaronleopold/stump:nightly + image: aaronleopold/stump:latest container_name: stump # Replace my paths (prior to the colons) with your own volumes: - - /Users/aaronleopold/.stump:/config - - /Users/aaronleopold/Documents/Stump:/data + - /home/aaronleopold/.stump:/config + - /media/books:/data ports: - 10801:10801 environment: @@ -70,13 +77,6 @@ docker compose up -d - - This tutorial uses the newer `docker compose` CLI. If you find this command does not exist for - you, you might be on V1, which uses `docker-compose`. Please review [Docker's - documentation](https://docs.docker.com/compose/install/) for more information and/or - platform-specific installation. - - @@ -92,17 +92,17 @@ docker create \ -e "PUID=1000" \ -e "PGID=1000" \ -p 10801:10801 \ - --volume "/Users/aaronleopold/.stump:/config" \ - --volume "/Users/aaronleopold/Documents/Stump:/data" \ + --volume "/home/aaronleopold/.stump:/config" \ + --volume "/media/books:/data" \ --restart unless-stopped \ - aaronleopold/stump:nightly + aaronleopold/stump:latest ``` If you prefer bind mounts, you can swap out the two `--volume` lines with: ```bash ---mount type=volume,source=/Users/aaronleopold/.stump,target=/config \ ---mount type=volume,source=/Users/aaronleopold/Documents/Stump,target=/data \ +--mount type=volume,source=/home/aaronleopold/.stump,target=/config \ +--mount type=volume,source=/media/books,target=/data \ ``` ### Start the container @@ -116,7 +116,7 @@ docker start stump When a new image is available, you can update your container using these commands: ```bash -docker pull aaronleopold/stump:nightly +docker pull aaronleopold/stump:latest docker restart stump ``` diff --git a/docs/pages/installation/executable.mdx b/docs/pages/installation/executable.mdx index dc9b5a5c5..63a10d536 100644 --- a/docs/pages/installation/executable.mdx +++ b/docs/pages/installation/executable.mdx @@ -5,12 +5,11 @@ import { Tabs } from 'nextra/components' # Executable - Any present tense information in this section is not accurate. Once a release is made, the - following information will apply shortly thereafter. + Native executables are not yet available. The corresponding GitHub issue for tracking is + [#310](https://github.com/stumpapp/stump/issues/310). For now, you can install Stump using + [Docker](/installation/docker) or directly from [source](/installation/source). -Pre-built binaries will be generally available on [GitHub](https://github.com/stumpapp/stump/releases) after `0.1.0` is released. Until then, only Docker images are available. - ## Platform-specific instructions Select your platform to view the installation instructions diff --git a/packages/browser/src/scenes/library/management/LibraryExclusions.tsx b/packages/browser/src/scenes/library/management/LibraryExclusions.tsx index 156a6e09c..28174f427 100644 --- a/packages/browser/src/scenes/library/management/LibraryExclusions.tsx +++ b/packages/browser/src/scenes/library/management/LibraryExclusions.tsx @@ -69,6 +69,7 @@ export default function LibraryExclusions() { return null } + // TODO: disabled state if no options return (
diff --git a/packages/browser/src/scenes/library/management/LibrarySettingsScene.tsx b/packages/browser/src/scenes/library/management/LibrarySettingsScene.tsx index 9fca8d50f..6ee7049c6 100644 --- a/packages/browser/src/scenes/library/management/LibrarySettingsScene.tsx +++ b/packages/browser/src/scenes/library/management/LibrarySettingsScene.tsx @@ -8,6 +8,8 @@ import { CreateOrUpdateLibraryForm } from './form' import LibraryExclusions from './LibraryExclusions' import QuickActions from './QuickActions' +// TODO: redesign this page, it is ugly. + export default function LibrarySettingsScene() { const { library } = useLibraryContext() const { libraries } = useLibraries()