ci: doc format check (#2325)

* ci: doc format check

* chore: linting
This commit is contained in:
Jason Rasmussen 2023-04-24 13:49:20 -04:00 committed by GitHub
parent d34585e4b0
commit 4cdc59e51c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 683 additions and 676 deletions

View file

@ -21,6 +21,28 @@ jobs:
- name: Run Immich Server E2E Test
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test
doc-tests:
name: Run documentation checks
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./docs
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Run npm install
run: npm ci
- name: Run formatter
run: npm run format
if: ${{ !cancelled() }}
- name: Run tsc
run: npm run check
if: ${{ !cancelled() }}
server-unit-tests:
name: Run server unit test suites and checks
runs-on: ubuntu-latest

2
docs/.prettierignore Normal file
View file

@ -0,0 +1,2 @@
build/
.docusaurus/

6
docs/.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120,
"semi": true
}

View file

@ -33,9 +33,8 @@ The motion part will now be uploaded and can be played on the mobile app and the
src="https://media.giphy.com/media/fTrGceZd7t1ewi8ESc/giphy.gif"
width="100%"
style={{
borderRadius: "10px",
boxShadow:
"rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px",
borderRadius: '10px',
boxShadow: 'rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px',
}}
title="LivePhoto playback on the web"
/>
@ -73,9 +72,8 @@ The web will have the option to sign in with OAuth.
width="50%"
title="Web Sign in with OAuth"
style={{
borderRadius: "10px",
boxShadow:
"rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px",
borderRadius: '10px',
boxShadow: 'rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px',
}}
/>
@ -86,9 +84,8 @@ sign-in button.
src="https://media.giphy.com/media/3iy3SaNkVYtlkEiw06/giphy.gif"
title="Mobile sign in with OAuth"
style={{
borderRadius: "10px",
boxShadow:
"rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px",
borderRadius: '10px',
boxShadow: 'rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px',
}}
/>
@ -98,9 +95,8 @@ sign-in button.
src="https://media.giphy.com/media/LStqgGESXW8XnuCv5y/giphy.gif"
width="300"
style={{
borderRadius: "10px",
boxShadow:
"rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px",
borderRadius: '10px',
boxShadow: 'rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px',
}}
title="Support the project"
/>

View file

@ -24,9 +24,9 @@ The initial approach of Immich is to become a backup tool, primarily for mobile
When a photo is initially uploaded Immich uses the create date of the file to determine where it belongs in the timeline. After that, background jobs will run that extract [exif metadata](https://en.wikipedia.org/wiki/Exif), including the CreateDate, to provide a more accurate date for the photo. If that is not available it will fallback to the modified date. If you want to ensure your photo has the right date, check the exif metadata before uploading.
If the timezone is incorrect in an uploaded photo, check the ``DateTimeOriginal`` exif field of the uploaded file. Immich uses the very competent library [exiftool-vendored.js](https://github.com/photostructure/exiftool-vendored.js#dates) to handle timezone parsing, but in some cases (like photos taken with DSLR cameras) it has to fallback on the local timezone. If you are using docker, this fallback will be UTC. (Note that even the photo backup app that can't be named [has the same bug!](https://photo.stackexchange.com/a/126978)) In Immich, it is possible to change this assumed fallback timezone system-wide by setting the timezone in the microservices docker container. You might need to run the "Extract Metadata" job after to effect the change.
If the timezone is incorrect in an uploaded photo, check the `DateTimeOriginal` exif field of the uploaded file. Immich uses the very competent library [exiftool-vendored.js](https://github.com/photostructure/exiftool-vendored.js#dates) to handle timezone parsing, but in some cases (like photos taken with DSLR cameras) it has to fallback on the local timezone. If you are using docker, this fallback will be UTC. (Note that even the photo backup app that can't be named [has the same bug!](https://photo.stackexchange.com/a/126978)) In Immich, it is possible to change this assumed fallback timezone system-wide by setting the timezone in the microservices docker container. You might need to run the "Extract Metadata" job after to effect the change.
As an example, the following modification of ```docker-compose.yml``` will set the timezone of the microservices container to be ``Europe/Stockholm``
As an example, the following modification of `docker-compose.yml` will set the timezone of the microservices container to be `Europe/Stockholm`
```
environment:
@ -34,9 +34,11 @@ As an example, the following modification of ```docker-compose.yml``` will set t
```
### Why are only photos and not videos being uploaded to Immich?
This often happens when using a reverse proxy or cloudflare tunnel in front of Immich. Make sure to set your reverse proxy to allow large POST requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Cloudflare tunnels are limited to 100 mb file sizes.
### Why is Immich slow on low-memory systems like the Raspberry Pi?
Immich uses optional machine-learning features to enhance search results. This feature, however, can be too heavy to run on a Raspberry Pi. To disable machine learning, comment out the `immich-machine-learning` section of your docker-compose.yml and set `IMMICH_MACHINE_LEARNING_URL=false` in your .env file.
### What happens to existing files after I choose a new [Storage Template](/docs/administration/storage-template.mdx)?
@ -44,6 +46,7 @@ Immich uses optional machine-learning features to enhance search results. This f
Template changes will only apply to new assets. To retroactively apply the template to previously uploaded assets, run the Storage Migration Job, available on the [Jobs](/docs/administration/jobs.md) page.
### In the uploads folder, why are photos stored in the wrong date?
This is fixed by running the storage migration job.
### Why is object detection not very good?

View file

@ -1,5 +1,4 @@
{
"label": "Administration",
"position": 4
}
}

View file

@ -36,7 +36,6 @@ Immich is a full-stack [TypeScript](https://www.typescriptlang.org/) application
- [Redis](https://redis.io/) for job queuing.
- [Typesense](https://typesense.org/) for search.
### Web Server
- [NGINX](https://www.nginx.com/) for internal communication between containers and load balancing when scaling.

View file

@ -14,17 +14,19 @@ Background backup is available thanks to the contribution effort of [@zoodyy](ht
If background backup is enabled. The app will periodically check if there are any new photos or videos in the selected album(s) to be uploaded to the cloud. If there are, it will upload them to the cloud in the background.
:::info Note
#### General
- The app must be in the background for the backup worker to start running.
- If you reopen the app and the first page you see is the backup page, the counts will not reflect the background uploaded result. You have to navigate out of the page and come back to see the updated counts.
#### Android
- It is a well-known problem that some Android models are very strict with battery optimization settings, which can cause a problem with the background worker. Please visit [Don't kill my app](https://dontkillmyapp.com/) for a guide on disabling this setting on your phone.
#### iOS
- You must enable **Background App Refresh** for the app to work in the background. You can enable it in the Settings app under General > Background App Refresh.
<div style={{textAlign: 'center'}}>

View file

@ -49,7 +49,6 @@ The API key can be obtained in the user setting panel on the web interface.
![Obtain Api Key](./img/obtain-api-key.png)
### Run via Docker
You can run the CLI inside of a docker container to avoid needing to install anything.
@ -74,6 +73,7 @@ immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
:::tip Internal networking
If you are running the CLI container on the same machine as your Immich server, you may not be able to reach the external address. In that case, try the following steps:
1. Find the internal Docker network used by Immich via `docker network ls`.
2. Adapt the above command to pass the `--network <immich_network>` argument to `docker run`, substituting `<immich_network>` with the result from step 1.
3. Use `--server http://immich-server:3001/` for the upload command instead of the external address.
@ -81,6 +81,7 @@ If you are running the CLI container on the same machine as your Immich server,
```bash title="Upload to internal address"
docker run --network immich_default -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --key HFEJ38DNSDUEG --server http://immich-server:3001/
```
:::
### Run from source

View file

@ -13,37 +13,36 @@ Install Immich using Portainer's Stack feature.
5. Replace `.env` with `stack.env` for all containers that need to use environment variables in the web editor.
<img
src={require('./img/dot-env.png').default}
width="50%"
style={{border: '1px solid #ddd'}}
alt="Dot Env Example"
src={require('./img/dot-env.png').default}
width="50%"
style={{border: '1px solid #ddd'}}
alt="Dot Env Example"
/>
8. Click on "**Advanced Mode**" in the **Environment Variables** section.
<img
src={require('./img/env-1.png').default}
width="50%"
style={{border: '1px solid #ddd'}}
alt="Dot Env Example"
src={require('./img/env-1.png').default}
width="50%"
style={{border: '1px solid #ddd'}}
alt="Dot Env Example"
/>
9. Copy the content of the `example.env` file from the [GitHub repository](https://github.com/immich-app/immich/releases/latest/download/example.env) and paste into the editor.
10. Switch back to "**Simple Mode**".
<img
src={require('./img/env-2.png').default}
width="50%"
style={{border: '1px solid #ddd'}}
alt="Dot Env Example"
src={require('./img/env-2.png').default}
width="50%"
style={{border: '1px solid #ddd'}}
alt="Dot Env Example"
/>
* Populate custom database information if necessary.
* Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets.
- Populate custom database information if necessary.
- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets.
11. Click on "**Deploy the stack**".
:::tip
For more information on how to use the application, please refer to the [Post Installation](/docs/install/post-install.mdx) guide.
:::

View file

@ -2,8 +2,8 @@
sidebar_position: 10
---
# Requirements
Hardware and software requirements for Immich
## Software

View file

@ -5,6 +5,7 @@ sidebar_position: 60
# Unraid
Immich can easily be installed and updated on Unraid via:
1. [Docker Compose Manager](https://forums.unraid.net/topic/114415-plugin-docker-compose-manager/) plugin from the Unraid Community Apps
2. Community made template on the Unraid Community Apps

View file

@ -4,7 +4,7 @@ sidebar_position: 1
# Introduction
<img src={require('./img/feature-panel.png').default} alt='Immich' />
<img src={require('./img/feature-panel.png').default} alt="Immich" />
## Welcome!

View file

@ -18,7 +18,6 @@ If you feel like this is the right cause and the app is something you see yourse
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
## Contributing
There are lots of non-monetary ways to contribute to Immich as well.

View file

@ -7,8 +7,7 @@ const darkCodeTheme = require('prism-react-renderer/themes/dracula');
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'Immich',
tagline:
'High performance self-hosted photo and video backup solution directly from your mobile phone',
tagline: 'High performance self-hosted photo and video backup solution directly from your mobile phone',
url: 'https://documentation.immich.app',
baseUrl: '/',
onBrokenLinks: 'throw',

22
docs/package-lock.json generated
View file

@ -25,6 +25,7 @@
"devDependencies": {
"@docusaurus/module-type-aliases": "2.1.0",
"@tsconfig/docusaurus": "^1.0.5",
"prettier": "^2.8.8",
"typescript": "^4.7.4"
},
"engines": {
@ -10538,6 +10539,21 @@
"node": ">=4"
}
},
"node_modules/prettier": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true,
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-error": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
@ -22086,6 +22102,12 @@
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
"integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA=="
},
"prettier": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true
},
"pretty-error": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",

View file

@ -4,6 +4,8 @@
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"format": "prettier --check .",
"format:fix": "prettier --write .",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
@ -12,7 +14,7 @@
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc"
"check": "tsc"
},
"dependencies": {
"@docusaurus/core": "2.1.0",
@ -32,6 +34,7 @@
"devDependencies": {
"@docusaurus/module-type-aliases": "2.1.0",
"@tsconfig/docusaurus": "^1.0.5",
"prettier": "^2.8.8",
"typescript": "^4.7.4"
},
"browserslist": {

View file

@ -14,7 +14,7 @@
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
// By default, Docusaurus generates a sidebar from the docs folder structure
tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }],
// But you can create a sidebar manually
/*

View file

@ -14,8 +14,8 @@ const FeatureList: FeatureItem[] = [
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
description: (
<>
Docusaurus was designed from the ground up to be easily installed and
used to get your website up and running quickly.
Docusaurus was designed from the ground up to be easily installed and used to get your website up and running
quickly.
</>
),
},
@ -24,8 +24,8 @@ const FeatureList: FeatureItem[] = [
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
description: (
<>
Docusaurus lets you focus on your docs, and we&apos;ll do the chores. Go
ahead and move your docs into the <code>docs</code> directory.
Docusaurus lets you focus on your docs, and we&apos;ll do the chores. Go ahead and move your docs into the{' '}
<code>docs</code> directory.
</>
),
},
@ -34,14 +34,14 @@ const FeatureList: FeatureItem[] = [
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
description: (
<>
Extend or customize your website layout by reusing React. Docusaurus can
be extended while reusing the same header and footer.
Extend or customize your website layout by reusing React. Docusaurus can be extended while reusing the same
header and footer.
</>
),
},
];
function Feature({title, Svg, description}: FeatureItem) {
function Feature({ title, Svg, description }: FeatureItem) {
return (
<div className={clsx('col col--4')}>
<div className="text--center">

View file

@ -7,12 +7,12 @@
@tailwind components;
@tailwind utilities;
@import url("https://fonts.googleapis.com/css2?family=Overpass:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Snowburst+One&display=swap");
@import url('https://fonts.googleapis.com/css2?family=Overpass:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Snowburst+One&display=swap');
html,
button {
font-family: "Overpass", sans-serif;
font-family: 'Overpass', sans-serif;
}
/* You can override the default Infima variables here. */
@ -29,7 +29,7 @@ button {
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme="dark"] {
[data-theme='dark'] {
--ifm-color-primary: #adcbfa;
--ifm-color-primary-dark: #85b2f8;
--ifm-color-primary-darker: #71a5f6;

View file

@ -1,6 +1,6 @@
import React from "react";
import Link from "@docusaurus/Link";
import Layout from "@theme/Layout";
import React from 'react';
import Link from '@docusaurus/Link';
import Layout from '@theme/Layout';
function HomepageHeader() {
return (

View file

@ -1,9 +1,9 @@
import Hogan from "hogan.js";
import LunrSearchAdapter from "./lunar-search";
import autocomplete from "autocomplete.js";
import templates from "./templates";
import utils from "./utils";
import $ from "autocomplete.js/zepto";
import Hogan from 'hogan.js';
import LunrSearchAdapter from './lunar-search';
import autocomplete from 'autocomplete.js';
import templates from './templates';
import utils from './utils';
import $ from 'autocomplete.js/zepto';
class DocSearch {
constructor({
@ -16,35 +16,27 @@ class DocSearch {
autocompleteOptions = {
debug: false,
hint: false,
autoselect: true
autoselect: true,
},
transformData = false,
queryHook = false,
handleSelected = false,
enhancedSearchInput = false,
layout = "collumns"
layout = 'collumns',
}) {
this.input = DocSearch.getInputFromSelector(inputSelector);
this.queryDataCallback = queryDataCallback || null;
const autocompleteOptionsDebug =
autocompleteOptions && autocompleteOptions.debug
? autocompleteOptions.debug
: false;
autocompleteOptions && autocompleteOptions.debug ? autocompleteOptions.debug : false;
// eslint-disable-next-line no-param-reassign
autocompleteOptions.debug = debug || autocompleteOptionsDebug;
this.autocompleteOptions = autocompleteOptions;
this.autocompleteOptions.cssClasses =
this.autocompleteOptions.cssClasses || {};
this.autocompleteOptions.cssClasses.prefix =
this.autocompleteOptions.cssClasses.prefix || "ds";
const inputAriaLabel =
this.input &&
typeof this.input.attr === "function" &&
this.input.attr("aria-label");
this.autocompleteOptions.ariaLabel =
this.autocompleteOptions.ariaLabel || inputAriaLabel || "search input";
this.autocompleteOptions.cssClasses = this.autocompleteOptions.cssClasses || {};
this.autocompleteOptions.cssClasses.prefix = this.autocompleteOptions.cssClasses.prefix || 'ds';
const inputAriaLabel = this.input && typeof this.input.attr === 'function' && this.input.attr('aria-label');
this.autocompleteOptions.ariaLabel = this.autocompleteOptions.ariaLabel || inputAriaLabel || 'search input';
this.isSimpleLayout = layout === "simple";
this.isSimpleLayout = layout === 'simple';
this.client = new LunrSearchAdapter(searchDocs, searchIndex, baseUrl);
@ -57,9 +49,9 @@ class DocSearch {
templates: {
suggestion: DocSearch.getSuggestionTemplate(this.isSimpleLayout),
footer: templates.footer,
empty: DocSearch.getEmptyTemplate()
}
}
empty: DocSearch.getEmptyTemplate(),
},
},
]);
const customHandleSelected = handleSelected;
@ -67,20 +59,14 @@ class DocSearch {
// We prevent default link clicking if a custom handleSelected is defined
if (customHandleSelected) {
$(".algolia-autocomplete").on("click", ".ds-suggestions a", event => {
$('.algolia-autocomplete').on('click', '.ds-suggestions a', (event) => {
event.preventDefault();
});
}
this.autocomplete.on(
"autocomplete:selected",
this.handleSelected.bind(null, this.autocomplete.autocomplete)
);
this.autocomplete.on('autocomplete:selected', this.handleSelected.bind(null, this.autocomplete.autocomplete));
this.autocomplete.on(
"autocomplete:shown",
this.handleShown.bind(null, this.input)
);
this.autocomplete.on('autocomplete:shown', this.handleShown.bind(null, this.input));
if (enhancedSearchInput) {
DocSearch.bindSearchBoxEvent();
@ -89,27 +75,24 @@ class DocSearch {
static injectSearchBox(input) {
input.before(templates.searchBox);
const newInput = input
.prev()
.prev()
.find("input");
const newInput = input.prev().prev().find('input');
input.remove();
return newInput;
}
static bindSearchBoxEvent() {
$('.searchbox [type="reset"]').on("click", function () {
$("input#docsearch").focus();
$(this).addClass("hide");
autocomplete.autocomplete.setVal("");
$('.searchbox [type="reset"]').on('click', function () {
$('input#docsearch').focus();
$(this).addClass('hide');
autocomplete.autocomplete.setVal('');
});
$("input#docsearch").on("keyup", () => {
const searchbox = document.querySelector("input#docsearch");
$('input#docsearch').on('keyup', () => {
const searchbox = document.querySelector('input#docsearch');
const reset = document.querySelector('.searchbox [type="reset"]');
reset.className = "searchbox__reset";
reset.className = 'searchbox__reset';
if (searchbox.value.length === 0) {
reset.className += " hide";
reset.className += ' hide';
}
});
}
@ -122,7 +105,7 @@ class DocSearch {
* @returns {void}
*/
static getInputFromSelector(selector) {
const input = $(selector).filter("input");
const input = $(selector).filter('input');
return input.length ? $(input[0]) : null;
}
@ -141,11 +124,8 @@ class DocSearch {
// eslint-disable-next-line no-param-reassign
query = queryHook(query) || query;
}
this.client.search(query).then(hits => {
if (
this.queryDataCallback &&
typeof this.queryDataCallback == "function"
) {
this.client.search(query).then((hits) => {
if (this.queryDataCallback && typeof this.queryDataCallback == 'function') {
this.queryDataCallback(hits);
}
if (transformData) {
@ -160,56 +140,42 @@ class DocSearch {
// a Hogan template
static formatHits(receivedHits) {
const clonedHits = utils.deepClone(receivedHits);
const hits = clonedHits.map(hit => {
const hits = clonedHits.map((hit) => {
if (hit._highlightResult) {
// eslint-disable-next-line no-param-reassign
hit._highlightResult = utils.mergeKeyWithParent(
hit._highlightResult,
"hierarchy"
);
hit._highlightResult = utils.mergeKeyWithParent(hit._highlightResult, 'hierarchy');
}
return utils.mergeKeyWithParent(hit, "hierarchy");
return utils.mergeKeyWithParent(hit, 'hierarchy');
});
// Group hits by category / subcategory
let groupedHits = utils.groupBy(hits, "lvl0");
let groupedHits = utils.groupBy(hits, 'lvl0');
$.each(groupedHits, (level, collection) => {
const groupedHitsByLvl1 = utils.groupBy(collection, "lvl1");
const flattenedHits = utils.flattenAndFlagFirst(
groupedHitsByLvl1,
"isSubCategoryHeader"
);
const groupedHitsByLvl1 = utils.groupBy(collection, 'lvl1');
const flattenedHits = utils.flattenAndFlagFirst(groupedHitsByLvl1, 'isSubCategoryHeader');
groupedHits[level] = flattenedHits;
});
groupedHits = utils.flattenAndFlagFirst(groupedHits, "isCategoryHeader");
groupedHits = utils.flattenAndFlagFirst(groupedHits, 'isCategoryHeader');
// Translate hits into smaller objects to be send to the template
return groupedHits.map(hit => {
return groupedHits.map((hit) => {
const url = DocSearch.formatURL(hit);
const category = utils.getHighlightedValue(hit, "lvl0");
const subcategory = utils.getHighlightedValue(hit, "lvl1") || category;
const category = utils.getHighlightedValue(hit, 'lvl0');
const subcategory = utils.getHighlightedValue(hit, 'lvl1') || category;
const displayTitle = utils
.compact([
utils.getHighlightedValue(hit, "lvl2") || subcategory,
utils.getHighlightedValue(hit, "lvl3"),
utils.getHighlightedValue(hit, "lvl4"),
utils.getHighlightedValue(hit, "lvl5"),
utils.getHighlightedValue(hit, "lvl6")
utils.getHighlightedValue(hit, 'lvl2') || subcategory,
utils.getHighlightedValue(hit, 'lvl3'),
utils.getHighlightedValue(hit, 'lvl4'),
utils.getHighlightedValue(hit, 'lvl5'),
utils.getHighlightedValue(hit, 'lvl6'),
])
.join(
'<span class="aa-suggestion-title-separator" aria-hidden="true"> </span>'
);
const text = utils.getSnippetedValue(hit, "content");
const isTextOrSubcategoryNonEmpty =
(subcategory && subcategory !== "") ||
(displayTitle && displayTitle !== "");
const isLvl1EmptyOrDuplicate =
!subcategory || subcategory === "" || subcategory === category;
const isLvl2 =
displayTitle && displayTitle !== "" && displayTitle !== subcategory;
const isLvl1 =
!isLvl2 &&
(subcategory && subcategory !== "" && subcategory !== category);
.join('<span class="aa-suggestion-title-separator" aria-hidden="true"> </span>');
const text = utils.getSnippetedValue(hit, 'content');
const isTextOrSubcategoryNonEmpty = (subcategory && subcategory !== '') || (displayTitle && displayTitle !== '');
const isLvl1EmptyOrDuplicate = !subcategory || subcategory === '' || subcategory === category;
const isLvl2 = displayTitle && displayTitle !== '' && displayTitle !== subcategory;
const isLvl1 = !isLvl2 && subcategory && subcategory !== '' && subcategory !== category;
const isLvl0 = !isLvl1 && !isLvl2;
return {
@ -224,7 +190,7 @@ class DocSearch {
subcategory,
title: displayTitle,
text,
url
url,
};
});
}
@ -232,38 +198,36 @@ class DocSearch {
static formatURL(hit) {
const { url, anchor } = hit;
if (url) {
const containsAnchor = url.indexOf("#") !== -1;
const containsAnchor = url.indexOf('#') !== -1;
if (containsAnchor) return url;
else if (anchor) return `${hit.url}#${hit.anchor}`;
return url;
} else if (anchor) return `#${hit.anchor}`;
/* eslint-disable */
console.warn("no anchor nor url for : ", JSON.stringify(hit));
console.warn('no anchor nor url for : ', JSON.stringify(hit));
/* eslint-enable */
return null;
}
static getEmptyTemplate() {
return args => Hogan.compile(templates.empty).render(args);
return (args) => Hogan.compile(templates.empty).render(args);
}
static getSuggestionTemplate(isSimpleLayout) {
const stringTemplate = isSimpleLayout
? templates.suggestionSimple
: templates.suggestion;
const stringTemplate = isSimpleLayout ? templates.suggestionSimple : templates.suggestion;
const template = Hogan.compile(stringTemplate);
return suggestion => template.render(suggestion);
return (suggestion) => template.render(suggestion);
}
handleSelected(input, event, suggestion, datasetNumber, context = {}) {
// Do nothing if click on the suggestion, as it's already a <a href>, the
// browser will take care of it. This allow Ctrl-Clicking on results and not
// having the main window being redirected as well
if (context.selectionMethod === "click") {
if (context.selectionMethod === 'click') {
return;
}
input.setVal("");
input.setVal('');
window.location.assign(suggestion.url);
}
@ -275,15 +239,10 @@ class DocSearch {
middleOfWindow = 900;
}
const alignClass =
middleOfInput - middleOfWindow >= 0
? "algolia-autocomplete-right"
: "algolia-autocomplete-left";
const alignClass = middleOfInput - middleOfWindow >= 0 ? 'algolia-autocomplete-right' : 'algolia-autocomplete-left';
const otherAlignClass =
middleOfInput - middleOfWindow < 0
? "algolia-autocomplete-right"
: "algolia-autocomplete-left";
const autocompleteWrapper = $(".algolia-autocomplete");
middleOfInput - middleOfWindow < 0 ? 'algolia-autocomplete-right' : 'algolia-autocomplete-left';
const autocompleteWrapper = $('.algolia-autocomplete');
if (!autocompleteWrapper.hasClass(alignClass)) {
autocompleteWrapper.addClass(alignClass);
}

View file

@ -11,8 +11,7 @@
color: #3a33d1;
}
/* Highligted search terms in the main category headers */
.algolia-docsearch-suggestion--category-header
.algolia-docsearch-suggestion--highlight {
.algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--highlight {
background-color: #4d47d5;
}
/* Currently selected suggestion */
@ -343,9 +342,7 @@
background: inherit;
}
.algolia-autocomplete
.algolia-docsearch-suggestion--text
.algolia-docsearch-suggestion--highlight {
.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight {
padding: 0 0 1px;
background: inherit;
box-shadow: inset 0 -2px 0 0 rgba(69, 142, 225, 0.8);
@ -419,14 +416,11 @@
.algolia-autocomplete
.algolia-docsearch-suggestion.algolia-docsearch-suggestion__main
.algolia-docsearch-suggestion--category-header,
.algolia-autocomplete
.algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary {
.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary {
display: block;
}
.algolia-autocomplete
.algolia-docsearch-suggestion--subcategory-column
.algolia-docsearch-suggestion--highlight {
.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight {
background-color: inherit;
color: inherit;
}
@ -459,9 +453,7 @@
margin-top: -8px;
}
.algolia-autocomplete
.algolia-docsearch-suggestion--no-results
.algolia-docsearch-suggestion--text {
.algolia-autocomplete .algolia-docsearch-suggestion--no-results .algolia-docsearch-suggestion--text {
color: #ffffff;
margin-top: 4px;
}
@ -477,14 +469,10 @@
color: #222222;
background-color: #ebebeb;
border-radius: 3px;
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
.algolia-autocomplete
.algolia-docsearch-suggestion
code
.algolia-docsearch-suggestion--highlight {
.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight {
background: none;
}

View file

@ -1,10 +1,10 @@
import React, { useRef, useCallback, useState } from "react";
import classnames from "classnames";
import { useHistory } from "@docusaurus/router";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import React, { useRef, useCallback, useState } from 'react';
import classnames from 'classnames';
import { useHistory } from '@docusaurus/router';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import { usePluginData } from '@docusaurus/useGlobalData';
import useIsBrowser from "@docusaurus/useIsBrowser";
const Search = props => {
import useIsBrowser from '@docusaurus/useIsBrowser';
const Search = (props) => {
const initialized = useRef(false);
const searchBarRef = useRef(null);
const [indexReady, setIndexReady] = useState(false);
@ -17,61 +17,58 @@ const Search = props => {
searchDocs,
searchIndex,
baseUrl,
inputSelector: "#search_input_react",
inputSelector: '#search_input_react',
// Override algolia's default selection event, allowing us to do client-side
// navigation and avoiding a full page refresh.
handleSelected: (_input, _event, suggestion) => {
const url = suggestion.url || "/";
const url = suggestion.url || '/';
// Use an anchor tag to parse the absolute url into a relative url
// Alternatively, we can use new URL(suggestion.url) but its not supported in IE
const a = document.createElement("a");
const a = document.createElement('a');
a.href = url;
// Algolia use closest parent element id #__docusaurus when a h1 page title does not have an id
// So, we can safely remove it. See https://github.com/facebook/docusaurus/issues/1828 for more details.
history.push(url);
}
},
});
};
const pluginData = usePluginData('docusaurus-lunr-search');
const getSearchDoc = () =>
process.env.NODE_ENV === "production"
process.env.NODE_ENV === 'production'
? fetch(`${baseUrl}${pluginData.fileNames.searchDoc}`).then((content) => content.json())
: Promise.resolve([]);
const getLunrIndex = () =>
process.env.NODE_ENV === "production"
process.env.NODE_ENV === 'production'
? fetch(`${baseUrl}${pluginData.fileNames.lunrIndex}`).then((content) => content.json())
: Promise.resolve([]);
const loadAlgolia = () => {
if (!initialized.current) {
Promise.all([
getSearchDoc(),
getLunrIndex(),
import("./DocSearch"),
import("./algolia.css")
]).then(([searchDocs, searchIndex, { default: DocSearch }]) => {
Promise.all([getSearchDoc(), getLunrIndex(), import('./DocSearch'), import('./algolia.css')]).then(
([searchDocs, searchIndex, { default: DocSearch }]) => {
if (searchDocs.length === 0) {
return;
}
initAlgolia(searchDocs, searchIndex, DocSearch);
setIndexReady(true);
});
},
);
initialized.current = true;
}
};
const toggleSearchIconClick = useCallback(
e => {
(e) => {
if (!searchBarRef.current.contains(e.target)) {
searchBarRef.current.focus();
}
props.handleSearchBarToggle && props.handleSearchBarToggle(!props.isSearchBarExpanded);
},
[props.isSearchBarExpanded]
[props.isSearchBarExpanded],
);
if (isBrowser) {
@ -83,8 +80,8 @@ const Search = props => {
<span
aria-label="expand searchbar"
role="button"
className={classnames("search-icon", {
"search-icon-hidden": props.isSearchBarExpanded
className={classnames('search-icon', {
'search-icon-hidden': props.isSearchBarExpanded,
})}
onClick={toggleSearchIconClick}
onKeyDown={toggleSearchIconClick}
@ -96,9 +93,9 @@ const Search = props => {
placeholder={indexReady ? 'Search' : 'Loading...'}
aria-label="Search"
className={classnames(
"navbar__search-input",
{ "search-bar-expanded": props.isSearchBarExpanded },
{ "search-bar": !props.isSearchBarExpanded }
'navbar__search-input',
{ 'search-bar-expanded': props.isSearchBarExpanded },
{ 'search-bar': !props.isSearchBarExpanded },
)}
onClick={loadAlgolia}
onMouseOver={loadAlgolia}

View file

@ -1,4 +1,4 @@
import lunr from "@generated/lunr.client";
import lunr from '@generated/lunr.client';
lunr.tokenizer.separator = /[\s\-/]+/;
class LunrSearchAdapter {
@ -12,10 +12,10 @@ class LunrSearchAdapter {
return this.lunrIndex.query(function (query) {
const tokens = lunr.tokenizer(input);
query.term(tokens, {
boost: 10
boost: 10,
});
query.term(tokens, {
wildcard: lunr.Query.wildcard.TRAILING
wildcard: lunr.Query.wildcard.TRAILING,
});
});
}
@ -24,15 +24,17 @@ class LunrSearchAdapter {
return {
hierarchy: {
lvl0: doc.pageTitle || doc.title,
lvl1: doc.type === 0 ? null : doc.title
lvl1: doc.type === 0 ? null : doc.title,
},
url: doc.url,
_snippetResult: formattedContent ? {
_snippetResult: formattedContent
? {
content: {
value: formattedContent,
matchLevel: "full"
matchLevel: 'full',
},
}
} : null,
: null,
_highlightResult: {
hierarchy: {
lvl0: {
@ -42,24 +44,37 @@ class LunrSearchAdapter {
doc.type === 0
? null
: {
value: formattedTitle || doc.title
}
}
}
value: formattedTitle || doc.title,
},
},
},
};
}
getTitleHit(doc, position, length) {
const start = position[0];
const end = position[0] + length;
let formattedTitle = doc.title.substring(0, start) + '<span class="algolia-docsearch-suggestion--highlight">' + doc.title.substring(start, end) + '</span>' + doc.title.substring(end, doc.title.length);
return this.getHit(doc, formattedTitle)
let formattedTitle =
doc.title.substring(0, start) +
'<span class="algolia-docsearch-suggestion--highlight">' +
doc.title.substring(start, end) +
'</span>' +
doc.title.substring(end, doc.title.length);
return this.getHit(doc, formattedTitle);
}
getKeywordHit(doc, position, length) {
const start = position[0];
const end = position[0] + length;
let formattedTitle = doc.title + '<br /><i>Keywords: ' + doc.keywords.substring(0, start) + '<span class="algolia-docsearch-suggestion--highlight">' + doc.keywords.substring(start, end) + '</span>' + doc.keywords.substring(end, doc.keywords.length) + '</i>'
return this.getHit(doc, formattedTitle)
let formattedTitle =
doc.title +
'<br /><i>Keywords: ' +
doc.keywords.substring(0, start) +
'<span class="algolia-docsearch-suggestion--highlight">' +
doc.keywords.substring(start, end) +
'</span>' +
doc.keywords.substring(end, doc.keywords.length) +
'</i>';
return this.getHit(doc, formattedTitle);
}
getContentHit(doc, position) {
@ -72,7 +87,7 @@ class LunrSearchAdapter {
for (let k = 0; k < 3; k++) {
const nextSpace = doc.content.lastIndexOf(' ', previewStart - 2);
const nextDot = doc.content.lastIndexOf('.', previewStart - 2);
if ((nextDot > 0) && (nextDot > nextSpace)) {
if (nextDot > 0 && nextDot > nextSpace) {
previewStart = nextDot + 1;
ellipsesBefore = false;
break;
@ -87,7 +102,7 @@ class LunrSearchAdapter {
for (let k = 0; k < 10; k++) {
const nextSpace = doc.content.indexOf(' ', previewEnd + 1);
const nextDot = doc.content.indexOf('.', previewEnd + 1);
if ((nextDot > 0) && (nextDot < nextSpace)) {
if (nextDot > 0 && nextDot < nextSpace) {
previewEnd = nextDot;
ellipsesAfter = false;
break;
@ -109,30 +124,29 @@ class LunrSearchAdapter {
preview += ' ...';
}
return this.getHit(doc, null, preview);
}
search(input) {
return new Promise((resolve, rej) => {
const results = this.getLunrResult(input);
const hits = [];
results.length > 5 && (results.length = 5);
this.titleHitsRes = []
this.contentHitsRes = []
results.forEach(result => {
this.titleHitsRes = [];
this.contentHitsRes = [];
results.forEach((result) => {
const doc = this.searchDocs[result.ref];
const { metadata } = result.matchData;
for (let i in metadata) {
if (metadata[i].title) {
if (!this.titleHitsRes.includes(result.ref)) {
const position = metadata[i].title.position[0]
const position = metadata[i].title.position[0];
hits.push(this.getTitleHit(doc, position, input.length));
this.titleHitsRes.push(result.ref);
}
} else if (metadata[i].content) {
const position = metadata[i].content.position[0]
hits.push(this.getContentHit(doc, position))
const position = metadata[i].content.position[0];
hits.push(this.getContentHit(doc, position));
} else if (metadata[i].keywords) {
const position = metadata[i].keywords.position[0]
const position = metadata[i].keywords.position[0];
hits.push(this.getKeywordHit(doc, position, input.length));
this.titleHitsRes.push(result.ref);
}

View file

@ -1,4 +1,4 @@
import $ from "autocomplete.js/zepto";
import $ from 'autocomplete.js/zepto';
const utils = {
/*
@ -97,7 +97,7 @@ const utils = {
* @return {array}
*/
values(object) {
return Object.keys(object).map(key => object[key]);
return Object.keys(object).map((key) => object[key]);
},
/*
* Flattens an array
@ -110,12 +110,12 @@ const utils = {
*/
flatten(array) {
const results = [];
array.forEach(value => {
array.forEach((value) => {
if (!Array.isArray(value)) {
results.push(value);
return;
}
value.forEach(subvalue => {
value.forEach((subvalue) => {
results.push(subvalue);
});
});
@ -151,12 +151,12 @@ const utils = {
* @return {array}
*/
flattenAndFlagFirst(object, flag) {
const values = this.values(object).map(collection =>
const values = this.values(object).map((collection) =>
collection.map((item, index) => {
// eslint-disable-next-line no-param-reassign
item[flag] = index === 0;
return item;
})
}),
);
return this.flatten(values);
},
@ -171,7 +171,7 @@ const utils = {
*/
compact(array) {
const results = [];
array.forEach(value => {
array.forEach((value) => {
if (!value) {
return;
}
@ -239,11 +239,7 @@ const utils = {
* @return {string}
**/
getSnippetedValue(object, property) {
if (
!object._snippetResult ||
!object._snippetResult[property] ||
!object._snippetResult[property].value
) {
if (!object._snippetResult || !object._snippetResult[property] || !object._snippetResult[property].value) {
return object[property];
}
let snippet = object._snippetResult[property].value;

View file

@ -4,25 +4,25 @@ module.exports = {
corePlugins: {
preflight: false, // disable Tailwind's reset
},
content: ["./src/**/*.{js,jsx,ts,tsx}", "../docs/**/*.mdx"], // my markdown stuff is in ../docs, not /src
darkMode: ["class", '[data-theme="dark"]'], // hooks into docusaurus' dark mode settigns
content: ['./src/**/*.{js,jsx,ts,tsx}', '../docs/**/*.mdx'], // my markdown stuff is in ../docs, not /src
darkMode: ['class', '[data-theme="dark"]'], // hooks into docusaurus' dark mode settigns
theme: {
extend: {
colors: {
// Light Theme
"immich-primary": "#4250af",
"immich-bg": "white",
"immich-fg": "black",
"immich-gray": "#F6F6F4",
'immich-primary': '#4250af',
'immich-bg': 'white',
'immich-fg': 'black',
'immich-gray': '#F6F6F4',
// Dark Theme
"immich-dark-primary": "#adcbfa",
"immich-dark-bg": "black",
"immich-dark-fg": "#e5e7eb",
"immich-dark-gray": "#212121",
'immich-dark-primary': '#adcbfa',
'immich-dark-bg': 'black',
'immich-dark-fg': '#e5e7eb',
'immich-dark-gray': '#212121',
},
fontFamily: {
"immich-title": ["Snowburst One", "cursive"],
'immich-title': ['Snowburst One', 'cursive'],
},
},
},