diff --git a/documentation/.gitignore b/documentation/.gitignore
index e9d4b99708..0ef98f5f98 100644
--- a/documentation/.gitignore
+++ b/documentation/.gitignore
@@ -12,6 +12,9 @@ docs/advanced-options/starsky
docs/advanced-options/starskydesktop
docs/advanced-options/starsky-tools
+# show desktop folder
+!docs/getting-started/desktop
+
# the output of the build step
build
diff --git a/documentation/docs/assets/getting-started-configuration-desktop-open.jpg b/documentation/docs/assets/getting-started-configuration-desktop-open.jpg
new file mode 100644
index 0000000000..b3508f6baf
Binary files /dev/null and b/documentation/docs/assets/getting-started-configuration-desktop-open.jpg differ
diff --git a/documentation/docs/developer-guide/api/readme.md b/documentation/docs/developer-guide/api/readme.md
index 8192afde44..7825f2d3f9 100644
--- a/documentation/docs/developer-guide/api/readme.md
+++ b/documentation/docs/developer-guide/api/readme.md
@@ -30,6 +30,9 @@ This document is auto generated
| __/api/delete__ | DELETE| Remove files from the disk, but the file must contain the !delete! (TrashKeyw...|
| _Parameters: f (subPaths, separated by dot comma), collections (true is to update files with the same name before _ |
| _ the extenstion) _ |
+| __/api/desktop-editor/open__ | GET | Open a file in the default editor or a specific editor on the desktop |
+| _Parameters: f (single or multiple subPaths), collections (to combine files with the same name before the extension) _ |
+| __/api/desktop-editor/amount-confirmation__ | GET | Check the amount of files to open before |
| __/api/disk/mkdir__ | POST | Make a directory (-p) |
| __/api/disk/rename__ | POST | Rename file/folder and update it in the database |
| _Parameters: f (from subPath), to (to subPath), collections (is collections bool), currentStatus (default is to not _ |
@@ -103,7 +106,6 @@ This document is auto generated
| _ json (text as output), extraLarge (give preference to extraLarge over large image) _ |
| __/api/thumbnail/zoom/\{f\}@\{z\}__ | GET | Get zoomed in image by fileHash.At the moment this is the source image |
| __/api/thumbnail-generation__ | POST | Create thumbnails for a folder in the background |
-| __/api/trash/detect-to-use-system-trash__ | GET | Is the system trash supported |
-| __/api/trash/move-to-trash__ | POST | (beta) Move a file to the trash |
+| __/api/trash/move-to-trash__ | POST | Move a file to the trash |
| __/api/upload__ | POST | Upload to specific folder (does not check if already has been imported)Use th...|
| __/api/upload-sidecar__ | POST | Upload sidecar file to specific folder (does not check if already has been im...|
diff --git a/documentation/docs/features/bulk-editing.md b/documentation/docs/features/bulk-editing.md
index 29680038db..f7f53bebf8 100644
--- a/documentation/docs/features/bulk-editing.md
+++ b/documentation/docs/features/bulk-editing.md
@@ -1,26 +1,36 @@
# Bulk editing
-Bulk editing is a feature that allows you to edit multiple files at once.
+Bulk editing is a feature that allows you to edit multiple files at once.
Is it possible to edit the metadata for multiple images at once.
You can use it in search and archive. In detail view you can edit the metadata for a single image.
## 1. Select the images
+
Via the search or archive you can select multiple images.
Press select and Labels and select the images you want to update.
## Update
+
When you selected an image you can update the metadata.
You can update the following metadata:
+
- Tags
- Info
- Title
- ColorClass (the color label of the image)
via the API you could also update other metadata like:
+
- Location
- Software
- etc.
# Replace the metadata in the fields: tags, info or title
+
You can search and replace the metadata in the fields: tags, info or title.
-Is easy to undo typos or update the metadata.
\ No newline at end of file
+Is easy to undo typos or update the metadata.
+
+# Open files
+
+When using the application as desktop mode you can batch open files with your favorite editor.
+See [Desktop Open](../getting-started/configuration/desktop-open.md) for more information.
\ No newline at end of file
diff --git a/documentation/docs/features/stacks.md b/documentation/docs/features/stacks.md
index 5e7581ea3a..6fff90b613 100644
--- a/documentation/docs/features/stacks.md
+++ b/documentation/docs/features/stacks.md
@@ -2,13 +2,19 @@
Stacks are enabled by default. Stacks are a way to group photos together.
-For example, you might have a photo that is associated with both a JPEG and an RAW file.
-In this case, the Stack organizes these two files together so that they are easier to find, manage and use later on.
+For example, you might have a photo that is associated with both a JPEG and an RAW file.
+In this case, the Stack organizes these two files together so that they are easier to find, manage
+and use later on.
You may come across pictures that are associated with more than one media file.
## For what reasons can files be stacked?
-- Files sharing exactly the same file and folder name will always be stacked together.
- - for example `/2018/IMG_1234.jpg` and `/2018/IMG_1234.avi`
+- Files sharing exactly the same file and folder name will always be stacked together.
+ - for example `/2018/IMG_1234.jpg` and `/2018/IMG_1234.avi`
+# Open files
+
+When using the application as desktop mode you can batch open files with your favorite editor. It
+will also respect the stack settings.
+See [Desktop Open](../getting-started/configuration/desktop-open.md) for more information.
diff --git a/documentation/docs/getting-started/configuration/_category_.json b/documentation/docs/getting-started/configuration/_category_.json
new file mode 100644
index 0000000000..f92373a37a
--- /dev/null
+++ b/documentation/docs/getting-started/configuration/_category_.json
@@ -0,0 +1,8 @@
+{
+ "label": "Configuration",
+ "position": 5,
+ "link": {
+ "type": "generated-index",
+ "description": "See configuration options for the application."
+ }
+}
diff --git a/documentation/docs/getting-started/config-options.md b/documentation/docs/getting-started/configuration/config-options.md
similarity index 65%
rename from documentation/docs/getting-started/config-options.md
rename to documentation/docs/getting-started/configuration/config-options.md
index 99348eea60..6f272e627a 100644
--- a/documentation/docs/getting-started/config-options.md
+++ b/documentation/docs/getting-started/configuration/config-options.md
@@ -1,11 +1,16 @@
-# Config Options
+---
+sidebar_position: 1
+---
- Changing values in `docker-compose.yml` or in Advanced Settings always **requires a restart** to take effect. Open a terminal, run `docker compose stop` and then
- `docker compose up -d` to restart all services.
+# Overview Config Options
+Changing values in `docker-compose.yml` or in Advanced Settings always **requires a restart** to
+take effect. Open a terminal, run `docker compose stop` and then
+`docker compose up -d` to restart all services.
## Web Application
-There are a few options that can be changed in the web application.
+
+There are a few options that can be changed in the web application.
These options are available though `appsettings.json` and environment variables.
@@ -16,13 +21,14 @@ Environment variables are always preferred over `appsettings.json` values.
and should prefix with `app__` and replace `:` with `__` and `.` with `_`.
So `app__databaseType` is the same as `"app":{"databaseType":"mysql"}` in `appsettings.json`.
-- [See advanced configuration options for the web application](../advanced-options/starsky/starsky/readme.md#recommend-settings)
+- [See advanced configuration options for the web application](../../advanced-options/starsky/starsky/readme.md#recommend-settings)
# Command line options
There are separate command line applications that target the specific needs.
-See the [Advanced options](../advanced-options/starsky/readme.md) for more information.
+See the [Advanced options](../../advanced-options/starsky/readme.md) for more information.
Add the command line argument `--help` option to see all available options.
-The options are configured in `appsettings.json` and environment variables and command line arguments.
+The options are configured in `appsettings.json` and environment variables and command line
+arguments.
diff --git a/documentation/docs/getting-started/configuration/desktop-open.md b/documentation/docs/getting-started/configuration/desktop-open.md
new file mode 100644
index 0000000000..5b271b3814
--- /dev/null
+++ b/documentation/docs/getting-started/configuration/desktop-open.md
@@ -0,0 +1,79 @@
+---
+sidebar_position: 5
+---
+
+# Configure Open With
+
+There are two options to use the desktop application
+
+- Remote (So the back-end is not running on your local machine)
+- Local / As Desktop
+
+If you don't know you are using the local one, you can check the settings in the desktop
+application.
+
+When using remote, the application will open the image in the default application.
+Then there are no settings to configure overwrites.
+
+Left: The settings are not available in a remote environment
+Right: To check if using remote or local (using the desktop application)
+
+![Desktop Open](../../assets/getting-started-configuration-desktop-open.jpg)
+
+## Local / As Desktop
+
+The following settings are for Local / As Desktop.
+The UI writes the settings to the `appsettings.patch.json` file.
+
+You can use the `appsettings.patch.json` file to configure the desktop application
+or use the Settings in the web application.
+
+### DefaultDesktopEditor
+
+This setting is done by ImageFormat,
+an imageFormat is defined by the first bytes of the file.
+
+In the UI there is an option to set the default application for a few photo formats.
+
+> Note: If you enter an invalid ApplicationPath location: The application will open the file with
+> the system default application
+
+```json
+{
+ "DefaultDesktopEditor": [
+ {
+ "ApplicationPath": "/Applications/Adobe Photoshop 2020/Adobe Photoshop 2020.app",
+ "ImageFormats": ["jpg", "bmp", "png", "gif", "tiff"]
+ }
+ ]
+}
+```
+
+### Collections / Stacks
+
+When opening an image from a collection/stack, the desktop application will one of both files
+A collection is for example two files in one folder: `2021-01-01-IMG_1234.jpg`
+and `2021-01-01-IMG_1234.dng`
+The default display is to show the jpeg first.
+
+> Note: The default setting is to open the jpeg file first
+
+### Raw First
+
+So the raw file will be open if available
+
+```json
+{
+ "DesktopCollectionsOpen": "2"
+}
+```
+
+### Jpeg first
+
+If the raw file is available, the jpeg file will be open first
+
+```json
+{
+ "DesktopCollectionsOpen": "1"
+}
+```
\ No newline at end of file
diff --git a/documentation/docs/getting-started/docker/docker-compose.md b/documentation/docs/getting-started/docker/docker-compose.md
index 9e1feab812..d9a2723937 100644
--- a/documentation/docs/getting-started/docker/docker-compose.md
+++ b/documentation/docs/getting-started/docker/docker-compose.md
@@ -1,48 +1,66 @@
# Setup Using Docker Compose
-With [Docker Compose](https://docs.docker.com/compose/), you [use a YAML file](../../developer-guide/technologies/yaml.md)
+With [Docker Compose](https://docs.docker.com/compose/),
+you [use a YAML file](../../developer-guide/technologies/yaml.md)
to configure all application services so you can easily start them with a single command.
-Before you proceed, make sure you have [Docker](https://store.docker.com/search?type=edition&offering=community)
+Before you proceed, make sure you
+have [Docker](https://store.docker.com/search?type=edition&offering=community)
installed on your system. It is available for Mac, Linux, and Windows.
## You also could use the application without Docker or Docker compose
-Docker is one way of using the application, it also possible to run it without Docker or Docker Compose.
+
+Docker is one way of using the application, it also possible to run it without Docker or Docker
+Compose.
## Step 1 Configure
-Download our [docker-compose.yml](https://raw.githubusercontent.com/qdraw/starsky/master/starsky/docker/compose/generic/docker-compose.yml) example
+Download
+our [docker-compose.yml](https://raw.githubusercontent.com/qdraw/starsky/master/starsky/docker/compose/generic/docker-compose.yml)
+example
(right click and *Save Link As...* or use `wget`) to a folder of your choice,
-and change the [configuration](../config-options.md) as needed:
+and change the [configuration](../configuration/config-options.md) as needed:
```bash
wget https://raw.githubusercontent.com/qdraw/starsky/master/starsky/docker/compose/generic/docker-compose.yml
```
Commands on Linux may have to be prefixed with `sudo` when not running as root.
-Note that this will point the home directory shortcut `~` to `/root` in the `volumes:`
-section of your `docker-compose.yml`. Kernel security modules such as AppArmor and SELinux
+Note that this will point the home directory shortcut `~` to `/root` in the `volumes:`
+section of your `docker-compose.yml`. Kernel security modules such as AppArmor and SELinux
have been [reported to cause issues](../troubleshooting/docker.md#kernel-security).
-Ensure that your server has [at least 4 GB of swap](../troubleshooting/docker.md#adding-swap) configured so that
+Ensure that your server has [at least 4 GB of swap](../troubleshooting/docker.md#adding-swap)
+configured so that
indexing doesn't cause restarts when there are memory usage spikes.
-
#### Database ####
-Our example includes a pre-configured [MariaDB](https://mariadb.com/) database server. If you remove it
+Our example includes a pre-configured [MariaDB](https://mariadb.com/) database server. If you remove
+it
and provide no other database server credentials, SQLite database files will be created in the
-*storage* folder. Local [SSD storage is best](../troubleshooting/performance.md#storage) for databases of any kind.
+*storage* folder. Local [SSD storage is best](../troubleshooting/performance.md#storage) for
+databases of any kind.
-Never [store database files](../troubleshooting/mariadb.md#corrupted-files) on an unreliable device such as a USB flash drive, SD card, or shared network folder. These may also have [unexpected file size limitations](https://thegeekpage.com/fix-the-file-size-exceeds-the-limit-allowed-and-cannot-be-saved/), which is especially problematic for databases that do not split data into smaller files.
+Never [store database files](../troubleshooting/mariadb.md#corrupted-files) on an unreliable device
+such as a USB flash drive, SD card, or shared network folder. These may also
+have [unexpected file size limitations](https://thegeekpage.com/fix-the-file-size-exceeds-the-limit-allowed-and-cannot-be-saved/),
+which is especially problematic for databases that do not split data into smaller files.
> **TL;DR**
-It is not possible to change the password via `MARIADB_PASSWORD` after the database has been started
-for the first time. Choosing a secure password is not essential if you don't [expose the database to other apps and hosts](../troubleshooting/mariadb.md#cannot-connect).
-To enable [automatic schema updates](../troubleshooting/mariadb.md#auto-upgrade) after upgrading to a new major version, set `MARIADB_AUTO_UPGRADE` to a non-empty value in your `docker-compose.yml`.
+> It is not possible to change the password via `MARIADB_PASSWORD` after the database has been
+> started
+> for the first time. Choosing a secure password is not essential if you
+> don't [expose the database to other apps and hosts](../troubleshooting/mariadb.md#cannot-connect).
+> To enable [automatic schema updates](../troubleshooting/mariadb.md#auto-upgrade) after upgrading
+> to
+> a new major version, set `MARIADB_AUTO_UPGRADE` to a non-empty value in your `docker-compose.yml`.
#### Volumes ####
-Since the app is running inside a container, you have to explicitly [mount the host folders](https://docs.docker.com/compose/compose-file/compose-file-v3/#volumes) you want to use.
-The app won't be able to see folders that have not been mounted. That's an important security feature.
+Since the app is running inside a container, you have to
+explicitly [mount the host folders](https://docs.docker.com/compose/compose-file/compose-file-v3/#volumes)
+you want to use.
+The app won't be able to see folders that have not been mounted. That's an important security
+feature.
##### /app/storageFolder #####
@@ -57,7 +75,8 @@ volumes:
- "~/Pictures:/app/storageFolder"
```
-You may [mount any folder accessible from the host](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-3)
+You
+may [mount any folder accessible from the host](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-3)
instead, including network drives. Additional directories can
be mounted as subfolders of `/app/storageFolder`:
@@ -76,32 +95,43 @@ volumes:
```
> **TL;DR**
-If *read-only mode* is enabled, all features that require write permission to the *originals/storageFolder* folder
-are disabled, uploading and deleting files. Set `app__ReadOnlyFolders__0` to `"/"`
-in `docker-compose.yml` for this.
-> You can [mount a folder with the `:ro` flag](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-3)
+> If *read-only mode* is enabled, all features that require write permission to the
+*originals/storageFolder* folder
+> are disabled, uploading and deleting files. Set `app__ReadOnlyFolders__0` to `"/"`
+> in `docker-compose.yml` for this.
+> You
+>
+can [mount a folder with the `:ro` flag](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-3)
> to make Docker block write operations as well.
##### /app/thumbnailTempFolder #####
Thumbnails files are created in the *thumbnailTempFolder* folder:
-- a *storage* folder mount must always be configured in your `docker-compose.yml` file so that you do not lose these files after a restart or upgrade
-- never configure the *thumbnailTempFolder* folder to be inside the *thumbnailTempFolder* folder unless the name starts with a `.` to indicate that it is hidden
-- we recommend placing the *thumbnailTempFolder* folder on a [local SSD drive](../troubleshooting/performance.md#storage) for best performance
-- mounting [symbolic links](https://en.wikipedia.org/wiki/Symbolic_link) or using them inside the *thumbnailTempFolder* folder is currently not supported
+- a *storage* folder mount must always be configured in your `docker-compose.yml` file so that you
+ do not lose these files after a restart or upgrade
+- never configure the *thumbnailTempFolder* folder to be inside the *thumbnailTempFolder* folder
+ unless the name starts with a `.` to indicate that it is hidden
+- we recommend placing the *thumbnailTempFolder* folder on
+ a [local SSD drive](../troubleshooting/performance.md#storage) for best performance
+- mounting [symbolic links](https://en.wikipedia.org/wiki/Symbolic_link) or using them inside the
+ *thumbnailTempFolder* folder is currently not supported
> **TL;DR**
-Should you later want to move your instance to another host, the easiest and most time-saving way is to copy the entire *storage* folder along with your originals and database.
+> Should you later want to move your instance to another host, the easiest and most time-saving way
+> is
+> to copy the entire *storage* folder along with your originals and database.
##### import #####
-At the moment we don't have a import folder for docker, but you can use the CLI to import files or use web upload
+At the moment we don't have a import folder for docker, but you can use the CLI to import files or
+use web upload
Import in a structured way that avoids duplicates:
- imported files receive a canonical filename and will be organized by year and month
-- never configure the *import* folder to be inside the *originals* folder, as this will cause a loop by importing already indexed files
+- never configure the *import* folder to be inside the *originals* folder, as this will cause a loop
+ by importing already indexed files
### Step 2: Start the server ###
@@ -112,20 +142,25 @@ Run this command to start the application and database services in the backgroun
docker compose up -d
```
-*Note that our guides now use the new `docker compose` command by default. If your server does not yet support it, you can still use `docker-compose`.*
+*Note that our guides now use the new `docker compose` command by default. If your server does not
+yet support it, you can still use `docker-compose`.*
Now open the Web UI by navigating to http://localhost:6470/. You should see a registration screen.
You may change it on the [account settings page](../../features/accountmanagement.md).
-Enabling [public mode](../config-options.md) will disable authentication.
+Enabling [public mode](../configuration/config-options.md) will disable authentication.
> **Info**
- It can be helpful to [keep Docker running in the foreground while debugging](../troubleshooting/docker.md#viewing-logs) so that log messages are displayed directly. To do this, omit the `-d` parameter when restarting.
- Should the server already be running, or you see no errors, you may have started it
- on a different host and/or port. There could also be an [issue with your browser,
- [ad blocker, or firewall settings](../troubleshooting/index.md).
+> It can be helpful
+>
+to [keep Docker running in the foreground while debugging](../troubleshooting/docker.md#viewing-logs)
+> so that log messages are displayed directly. To do this, omit the `-d` parameter when restarting.
+> Should the server already be running, or you see no errors, you may have started it
+> on a different host and/or port. There could also be an [issue with your browser,
+[ad blocker, or firewall settings](../troubleshooting/index.md).
-The server port and other [config options](../config-options.md) can be changed in `docker-compose.yml` at any time.
+The server port and other [config options](../configuration/config-options.md) can be changed
+in `docker-compose.yml` at any time.
Remember to restart the services for changes to take effect:
```bash
@@ -135,12 +170,16 @@ docker compose up -d
### Step 3: Index Your Library ###
-Our [First Steps 👣](../first-steps.md) tutorial guides you through the user interface and settings to ensure your library is indexed according to your individual preferences.
+Our [First Steps 👣](../first-steps.md) tutorial guides you through the user interface and settings
+to ensure your library is indexed according to your individual preferences.
> **Note**
- Ensure [there is enough disk space available](../troubleshooting/docker.md#disk-space) for creating thumbnails and [verify filesystem permissions](../troubleshooting/docker.md)
- before starting to index: Files in the *originals* folder must be readable, while the *storage* folder
- including all subdirectories must be readable and writeable.
+> Ensure [there is enough disk space available](../troubleshooting/docker.md#disk-space) for
+> creating
+> thumbnails and [verify filesystem permissions](../troubleshooting/docker.md)
+> before starting to index: Files in the *originals* folder must be readable, while the *storage*
+> folder
+> including all subdirectories must be readable and writeable.
Open the Web UI, go to *More* and click *Manual Sync* to start indexing your pictures.
@@ -148,13 +187,20 @@ Easy, isn't it?
### Troubleshooting ###
-If your server runs out of memory, the index is frequently locked, or other system resources are running low:
+If your server runs out of memory, the index is frequently locked, or other system resources are
+running low:
-- [ ] Try [reducing the number of workers](../config-options.md#index-workers) by setting `app__maxDegreesOfParallelism` to a reasonably small value in `docker-compose.yml`, depending on the CPU performance and number of cores
-- [ ] Make sure [your server has at least 4 GB of swap space](../troubleshooting/docker.md#adding-swap) so that indexing doesn't cause restarts when memory usage spikes; RAW image conversion and video transcoding are especially demanding
+- [ ] Try [reducing the number of workers](../configuration/config-options.md#index-workers) by
+ setting `app__maxDegreesOfParallelism` to a reasonably small value in `docker-compose.yml`,
+ depending on the CPU performance and number of cores
+- [ ] Make
+ sure [your server has at least 4 GB of swap space](../troubleshooting/docker.md#adding-swap) so
+ that indexing doesn't cause restarts when memory usage spikes; RAW image conversion and video
+ transcoding are especially demanding
- [ ] If you are using SQLite, switch to MariaDB, which is better optimized for high concurrency
-Other issues? Our [troubleshooting checklists](../troubleshooting/index.md) help you quickly diagnose and solve them.
+Other issues? Our [troubleshooting checklists](../troubleshooting/index.md) help you quickly
+diagnose and solve them.
diff --git a/documentation/docs/getting-started/setup.md b/documentation/docs/getting-started/setup.md
index 3027ffd1e6..6c9cb4841f 100644
--- a/documentation/docs/getting-started/setup.md
+++ b/documentation/docs/getting-started/setup.md
@@ -1,67 +1,105 @@
---
-sidebar_position: 4
+sidebar_position: 5
---
# Setup server app
-Starsky can be installed on all operating systems supporting Docker, as well as FreeBSD, Raspberry Pi, and many NAS devices.
+Starsky can be installed on all operating systems supporting Docker, as well as FreeBSD, Raspberry
+Pi, and many NAS devices.
There are multiple ways of installing Starsky:
1. **As background service (systemd or pm2 service)**
Run it as system service. All dependencies are included in the application
- There are multiple options to run it as a service, see [systemd](linux-systemd.md), [macOS launchctl](macos-launchctl.md), [windows service](windows-as-server/windows-service.md) or [pm2](pm2.md) for more information
+ There are multiple options to run it as a service,
+ see [systemd](linux-systemd.md), [macOS launchctl](macos-launchctl.md), [windows service](windows-as-server/windows-service.md)
+ or [pm2](pm2.md) for more information
2. **Docker**
- When using Docker we recommend running Starsky with Docker Compose when hosting it on a private server. It is available for Mac, Linux, and Windows. [Read more about docker configuration here](docker/docker-compose.md)
+ When using Docker we recommend running Starsky with Docker Compose when hosting it on a private
+ server. It is available for Mac, Linux, and
+ Windows. [Read more about docker configuration here](docker/docker-compose.md)
3. **In IIS** (Windows Pro and Server Only)
- When running the Pro and Server version of windows the IIS webserver can be used [Read more about IIS configuration here](windows-as-server/iis.md)
+ When running the Pro and Server version of windows the IIS webserver can be
+ used [Read more about IIS configuration here](windows-as-server/iis.md)
-Once the initial setup is complete, our [First Steps 👣 ](first-steps) tutorial guides you through the user interface and settings to ensure your library is indexed according to your individual preferences.
+Once the initial setup is complete, our [First Steps 👣 ](first-steps) tutorial guides you through
+the user interface and settings to ensure your library is indexed according to your individual
+preferences.
-> > Our stable version and development preview have been built into a single multi-arch Docker image for 64-bit AMD, Intel, and ARM processors. That means, Raspberry Pi 3 / 4, Apple Silicon, and other ARM64-based devices can pull from the same repository, enjoy the exact same functionality, and can follow the regular installation instructions after going through a short list of requirements. See FAQs for instructions and notes on alternative installation methods.
+> > Our stable version and development preview have been built into a single multi-arch Docker image
+> > for 64-bit AMD, Intel, and ARM processors. That means, Raspberry Pi 3 / 4, Apple Silicon, and other
+> > ARM64-based devices can pull from the same repository, enjoy the exact same functionality, and can
+> > follow the regular installation instructions after going through a short list of requirements. See
+> > FAQs for instructions and notes on alternative installation methods.
## Roadmap
-Our vision is to provide the most user- and privacy-friendly solution to keep your pictures organized and accessible. The roadmap shows what tasks are in progress, what needs testing, and which features are going to be implemented next.
+Our vision is to provide the most user- and privacy-friendly solution to keep your pictures
+organized and accessible. The roadmap shows what tasks are in progress, what needs testing, and
+which features are going to be implemented next.
-We have a low bug policy and do our best to help users when they need support or have other questions. This comes at a price, as we can't give exact deadlines for new features.
+We have a low bug policy and do our best to help users when they need support or have other
+questions. This comes at a price, as we can't give exact deadlines for new features.
-Having said that, funding really has the highest impact. [So users can do their part and become a sponsor to get their favorite features as soon as possible.](https://www.paypal.me/qdrawmedia)
+Having said that, funding really has the highest
+impact. [So users can do their part and become a sponsor to get their favorite features as soon as possible.](https://www.paypal.me/qdrawmedia)
## System Requirements
-You should host Starsky on a server with at least 2 cores, 3 GB of physical memory, 1 and a 64-bit operating system. Beyond these minimum requirements, the amount of RAM should match the number of CPU cores. Indexing large photo and video collections also benefits greatly from local SSD storage, especially for the database and cache files.
+You should host Starsky on a server with at least 2 cores, 3 GB of physical memory, 1 and a 64-bit
+operating system. Beyond these minimum requirements, the amount of RAM should match the number of
+CPU cores. Indexing large photo and video collections also benefits greatly from local SSD storage,
+especially for the database and cache files.
-If your server has less than 4 GB of swap space or a manual memory/swap limit is set, this can cause unexpected restarts, for example, when the indexer temporarily needs more memory to process large files. High-resolution panoramic images may require additional swap space and/or physical memory above the recommended minimum.
+If your server has less than 4 GB of swap space or a manual memory/swap limit is set, this can cause
+unexpected restarts, for example, when the indexer temporarily needs more memory to process large
+files. High-resolution panoramic images may require additional swap space and/or physical memory
+above the recommended minimum.
-> We take no responsibility for instability or performance problems if your device does not meet the requirements.
+> We take no responsibility for instability or performance problems if your device does not meet the
+> requirements.
### Databases
-Starsky is compatible with SQLite 3 and MariaDB 10.5.12+.2 Note that SQLite is generally not a good choice for users who require scalability and high performance, and that support for MySQL 8 has been discontinued due to low demand and missing features.
+Starsky is compatible with SQLite 3 and MariaDB 10.5.12+.2 Note that SQLite is generally not a good
+choice for users who require scalability and high performance, and that support for MySQL 8 has been
+discontinued due to low demand and missing features.
### Browsers
-Built as a Progressive Web App (PWA), the web interface works with most modern browsers, and runs best on Chrome, Chromium, Safari, Firefox, and Edge. You can conveniently install it on the home screen of all major operating systems and mobile devices. Internet Explorer is not supported.
+Built as a Progressive Web App (PWA), the web interface works with most modern browsers, and runs
+best on Chrome, Chromium, Safari, Firefox, and Edge. You can conveniently install it on the home
+screen of all major operating systems and mobile devices. Internet Explorer is not supported.
### Video playback
-Not all video and audio formats can be played with every browser. For example, AAC - the default audio codec for MPEG-4 AVC / H.264 - is supported natively in Chrome, Safari, and Edge, while it is only optionally supported by the OS in Firefox and Opera.
+Not all video and audio formats can be played with every browser. For example, AAC - the default
+audio codec for MPEG-4 AVC / H.264 - is supported natively in Chrome, Safari, and Edge, while it is
+only optionally supported by the OS in Firefox and Opera.
### HTTPS
-If you install Starsky on a public server outside your home network, always run it behind a secure HTTPS reverse proxy such as Traefik or Caddy. Your files and passwords will otherwise be transmitted in clear text and can be intercepted by anyone, including your provider, hackers, and governments.
+If you install Starsky on a public server outside your home network, always run it behind a secure
+HTTPS reverse proxy such as Traefik or Caddy. Your files and passwords will otherwise be transmitted
+in clear text and can be intercepted by anyone, including your provider, hackers, and governments.
## Getting Support
-If you need help installing our software at home, you post your question in GitHub Discussions. Common problems can be quickly diagnosed and solved using our Troubleshooting Checklists.
+If you need help installing our software at home, you post your question in GitHub Discussions.
+Common problems can be quickly diagnosed and solved using our Troubleshooting Checklists.
### Sponsor us
-We'll do our best to answer all your questions. In return, we ask you can sponsor us. Think of "free software" as in "free speech," not as in "free beer". Thank you!
+We'll do our best to answer all your questions. In return, we ask you can sponsor us. Think of "free
+software" as in "free speech," not as in "free beer". Thank you!
-In exchange for their continued support, sponsors are also welcome to request direct technical support via email. Please bear with us if we are unable to get back to you immediately due to the high volume of emails and contact requests we receive.
+In exchange for their continued support, sponsors are also welcome to request direct technical
+support via email. Please bear with us if we are unable to get back to you immediately due to the
+high volume of emails and contact requests we receive.
-> > We kindly ask you not to report bugs via GitHub Issues unless you are certain to have found a fully reproducible and previously unreported issue that must be fixed directly in the app. Contact us or a community member if you need help, it could be a local configuration problem, or a misunderstanding in how the software works.
+> > We kindly ask you not to report bugs via GitHub Issues unless you are certain to have found a
+> > fully reproducible and previously unreported issue that must be fixed directly in the app. Contact
+> > us or a community member if you need help, it could be a local configuration problem, or a
+> > misunderstanding in how the software works.
diff --git a/documentation/docs/getting-started/troubleshooting/index.md b/documentation/docs/getting-started/troubleshooting/index.md
index b06593eda3..d55be58a08 100644
--- a/documentation/docs/getting-started/troubleshooting/index.md
+++ b/documentation/docs/getting-started/troubleshooting/index.md
@@ -1,10 +1,12 @@
# Troubleshooting Checklists
-> You are welcome to ask for help in our [discussions](https://github.com/qdraw/starsky/discussions) page
+> You are welcome to ask for help in our [discussions](https://github.com/qdraw/starsky/discussions)
+> page
### Connection Fails ###
-If [your browser](browsers.md) cannot connect to the Web UI even after waiting a few minutes, run this command to display
+If [your browser](browsers.md) cannot connect to the Web UI even after waiting a few minutes, run
+this command to display
the last 100 log messages (omit `--tail=100` to see all):
```bash
@@ -13,22 +15,39 @@ docker compose logs --tail=100
Before reporting a bug, whould you please the following things:
-- [ ] Check the logs for messages like *disk full*, *disk quota exceeded*, *no space left on device*, *read-only file system*, *error creating path*, *wrong permissions*, *no route to host*, *connection failed*, and *killed*:
- - [ ] If a service has been "killed" or otherwise automatically terminated, this points to a [memory problem](docker.md#adding-swap) (add swap and/or memory; remove or increase usage limits)
- - [ ] In case the logs show "disk full", "quota exceeded", or "no space left" errors, either [the disk containing the *storage* folder is full](docker.md#disk-space) (add storage) or a disk usage limit is configured (remove or increase it)
- - [ ] Errors such as "read-only file system", "error creating path", or "wrong permissions" indicate a [filesystem permission problem](docker.md)
- - [ ] It may help to [add the `:z` mount flag to volumes](https://docs.docker.com/storage/bind-mounts/#configure-the-selinux-label) when using SELinux (RedHat/Fedora)
- - [ ] Log messages that contain "no route to host" indicate a [problem with the database](mariadb.md) or Docker network configuration (follow our [examples](../docker/docker-compose.md))
-- [ ] Make sure you are using the correct protocol (default is `http`), port (default is `4823`), and host (default is `localhost`):
- - [ ] Check if the server port you try to use [has been exposed](https://docs.docker.com/compose/compose-file/compose-file-v3/#ports) and [no firewall is blocking it](https://support.microsoft.com/en-us/windows/turn-microsoft-defender-firewall-on-or-off-ec0844f7-aebd-0583-67fe-601ecf5d774f)
+- [ ] Check the logs for messages like *disk full*, *disk quota exceeded*, *no space left on
+ device*, *read-only file system*, *error creating path*, *wrong permissions*, *no route to host*,
+ *connection failed*, and *killed*:
+ - [ ] If a service has been "killed" or otherwise automatically terminated, this points to
+ a [memory problem](docker.md#adding-swap) (add swap and/or memory; remove or increase usage
+ limits)
+ - [ ] In case the logs show "disk full", "quota exceeded", or "no space left" errors,
+ either [the disk containing the *storage* folder is full](docker.md#disk-space) (add storage)
+ or a disk usage limit is configured (remove or increase it)
+ - [ ] Errors such as "read-only file system", "error creating path", or "wrong permissions"
+ indicate a [filesystem permission problem](docker.md)
+ - [ ] It may help
+ to [add the `:z` mount flag to volumes](https://docs.docker.com/storage/bind-mounts/#configure-the-selinux-label)
+ when using SELinux (RedHat/Fedora)
+ - [ ] Log messages that contain "no route to host" indicate
+ a [problem with the database](mariadb.md) or Docker network configuration (follow
+ our [examples](../docker/docker-compose.md))
+- [ ] Make sure you are using the correct protocol (default is `http`), port (default is `4823`),
+ and host (default is `localhost`):
+ - [ ] Check if the server port you try to
+ use [has been exposed](https://docs.docker.com/compose/compose-file/compose-file-v3/#ports)
+ and [no firewall is blocking it](https://support.microsoft.com/en-us/windows/turn-microsoft-defender-firewall-on-or-off-ec0844f7-aebd-0583-67fe-601ecf5d774f)
- [ ] Only use `localhost` or `127.0.0.1` if the server is running on the same computer (host)
- [ ] Avoid using IP addresses other than `127.0.0.1` directly, as they can change
- - [ ] We recommend [configuring a local hostname](../../assets/getting-started-index-pihole-local-dns.png) to access other hosts on your network
+ - [ ] We
+ recommend [configuring a local hostname](../../assets/getting-started-index-pihole-local-dns.png)
+ to access other hosts on your network
- [ ] Note that HTTP security headers will prevent the app from loading in a frame (override them)
- [ ] Verify your computer meets the [system requirements](../readme.mdx#system-requirements)
- [ ] Go through the [checklist for fatal server errors](#fatal-server-errors)
-Should MariaDB get stuck in a restart loop and Starsky can't connect to it, this indicates a [memory](docker.md#adding-swap),
+Should MariaDB get stuck in a restart loop and Starsky can't connect to it, this indicates
+a [memory](docker.md#adding-swap),
[filesystem](docker.md), or other [permission issue](docker.md#kernel-security):
```
@@ -38,7 +57,8 @@ starsky: dial tcp 172.18.0.2:3306: connect: no route to host
mariadb: mysqld: Shutdown complete
```
-To enable [debug mode](../config-options.md), set `app__verbose` to `true` in the `environment:` section
+To enable [debug mode](../configuration/config-options.md), set `app__verbose` to `true` in
+the `environment:` section
of the `starsky` service (or use the `-v` flag when running the `starsky` command directly):
```yaml
@@ -48,8 +68,10 @@ services:
app__verbose: "true"
```
-Then restart all services for the changes to take effect. It can be helpful to keep Docker running in the foreground
-while debugging so that log messages are displayed directly. To do this, omit the `-d` parameter when restarting:
+Then restart all services for the changes to take effect. It can be helpful to keep Docker running
+in the foreground
+while debugging so that log messages are displayed directly. To do this, omit the `-d` parameter
+when restarting:
```bash
docker compose stop
@@ -57,12 +79,16 @@ docker compose up
```
!!! note ""
- If you see no errors or no logs at all, you may have started the server on a different host
- and/or port. There could also be an [issue with your browser](browsers.md), browser plugins, firewall settings,
- or other tools you may have installed.
+If you see no errors or no logs at all, you may have started the server on a different host
+and/or port. There could also be an [issue with your browser](browsers.md), browser plugins,
+firewall settings,
+or other tools you may have installed.
!!! tldr ""
- The default [Docker Compose](https://docs.docker.com/compose/) config filename is `docker-compose.yml`. For simplicity, it doesn't need to be specified when running the `docker-compose` command in the same directory. Config files for other apps or instances should be placed in separate folders.
+The default [Docker Compose](https://docs.docker.com/compose/) config filename
+is `docker-compose.yml`. For simplicity, it doesn't need to be specified when running
+the `docker-compose` command in the same directory. Config files for other apps or instances should
+be placed in separate folders.
### Docker Doesn't Work ###
@@ -87,44 +113,72 @@ docker compose up
Fatal errors are often caused by one of the following conditions:
- [ ] Your (virtual) server [disk is full](docker.md#disk-space) (add storage)
-- [ ] You have accidentally [mounted the wrong folders](../docker/docker-compose.md#volumes) (update config and restart)
-- [ ] There is disk space left, but a usage or the [inode limit](https://serverfault.com/questions/104986/what-is-the-maximum-number-of-files-a-file-system-can-contain) has been reached (change it)
-- [ ] You are using a [filesystem or network drive with a file size limit](https://thegeekpage.com/fix-the-file-size-exceeds-the-limit-allowed-and-cannot-be-saved/) (change settings or storage)
-- [ ] The *storage* folder [is not writable or mounted read-only](docker.md) (change [permissions](docker.md))
-- [ ] [Symbolic links](https://en.wikipedia.org/wiki/Symbolic_link) were mounted or used within a *storage* folder (replace with actual paths)
+- [ ] You have accidentally [mounted the wrong folders](../docker/docker-compose.md#volumes) (update
+ config and restart)
+- [ ] There is disk space left, but a usage or
+ the [inode limit](https://serverfault.com/questions/104986/what-is-the-maximum-number-of-files-a-file-system-can-contain)
+ has been reached (change it)
+- [ ] You are using
+ a [filesystem or network drive with a file size limit](https://thegeekpage.com/fix-the-file-size-exceeds-the-limit-allowed-and-cannot-be-saved/) (
+ change settings or storage)
+- [ ] The *storage* folder [is not writable or mounted read-only](docker.md) (
+ change [permissions](docker.md))
+- [ ] [Symbolic links](https://en.wikipedia.org/wiki/Symbolic_link) were mounted or used within a
+ *storage* folder (replace with actual paths)
- [ ] The [server is low on memory](../readme.mdx#system-requirements) (add memory)
- [ ] You didn't [configure at least 4 GB of swap space](docker.md#adding-swap) (add swap)
-- [ ] High-resolution panoramic images require [additional memory](performance.md#memory) above the recommended minimum (add more swap or memory)
+- [ ] High-resolution panoramic images require [additional memory](performance.md#memory) above the
+ recommended minimum (add more swap or memory)
- [ ] The server CPU is overheating (improve cooling)
- [ ] The server has an outdated operating system that is not fully compatible (update)
- [ ] The server hardware is defective and causes random panics (test on another server)
-- [ ] The [database server](mariadb.md) is not running, [incompatible](../readme.mdx#databases), or misconfigured (start, upgrade, or [fix it](mariadb.md))
-- [ ] You've [upgraded the MariaDB server](mariadb.md#version-upgrade) without running `mariadb-upgrade`
-- [ ] Files are [stored on an unreliable device such as a USB flash drive or a shared network folder](mariadb.md#corrupted-files)
+- [ ] The [database server](mariadb.md) is not running, [incompatible](../readme.mdx#databases), or
+ misconfigured (start, upgrade, or [fix it](mariadb.md))
+- [ ] You've [upgraded the MariaDB server](mariadb.md#version-upgrade) without
+ running `mariadb-upgrade`
+- [ ] Files
+ are [stored on an unreliable device such as a USB flash drive or a shared network folder](mariadb.md#corrupted-files)
- [ ] There are network problems caused by a bad configuration, firewall, or unstable connection
-- [ ] [Kernel security modules](docker.md#kernel-security) such as [AppArmor](https://wiki.ubuntu.com/AppArmor) and [SELinux](https://en.wikipedia.org/wiki/Security-Enhanced_Linux) are blocking permissions
-- [ ] Your Raspberry Pi has not been configured according to our [recommendations](../raspberry-pi.md#system-requirements)
-
-We recommend checking your [Docker Logs](docker.md#viewing-logs) for messages like *disk full*, *disk quota exceeded*,
-*no space left on device*, *read-only file system*, *error creating path*, *wrong permissions*, *no route to host*, *connection failed*, and *killed*:
-
-- [ ] If a service has been "killed" or otherwise automatically terminated, this points to a [memory problem](docker.md#adding-swap) (add swap and/or memory; remove or increase usage limits)
-- [ ] In case the logs show "disk full", "quota exceeded", or "no space left" errors, either [the disk containing the *storage* folder is full](docker.md#disk-space) (add storage) or a disk usage limit is configured (remove or increase it)
-- [ ] Errors such as "read-only file system", "error creating path", or "wrong permissions" indicate a [filesystem permission problem](docker.md)
-- [ ] Log messages that contain "no route to host" indicate a [problem with the database](mariadb.md) or network configuration (follow our [examples](../docker/docker-compose.md))
-
-*Start a full rescan if necessary, for example, if it looks like [thumbnails](index.md#broken-thumbnails) or [pictures are missing](index.md#missing-pictures).*
+- [ ] [Kernel security modules](docker.md#kernel-security) such
+ as [AppArmor](https://wiki.ubuntu.com/AppArmor)
+ and [SELinux](https://en.wikipedia.org/wiki/Security-Enhanced_Linux) are blocking permissions
+- [ ] Your Raspberry Pi has not been configured according to
+ our [recommendations](../raspberry-pi.md#system-requirements)
+
+We recommend checking your [Docker Logs](docker.md#viewing-logs) for messages like *disk full*,
+*disk quota exceeded*,
+*no space left on device*, *read-only file system*, *error creating path*, *wrong permissions*, *no
+route to host*, *connection failed*, and *killed*:
+
+- [ ] If a service has been "killed" or otherwise automatically terminated, this points to
+ a [memory problem](docker.md#adding-swap) (add swap and/or memory; remove or increase usage
+ limits)
+- [ ] In case the logs show "disk full", "quota exceeded", or "no space left" errors,
+ either [the disk containing the *storage* folder is full](docker.md#disk-space) (add storage) or a
+ disk usage limit is configured (remove or increase it)
+- [ ] Errors such as "read-only file system", "error creating path", or "wrong permissions" indicate
+ a [filesystem permission problem](docker.md)
+- [ ] Log messages that contain "no route to host" indicate
+ a [problem with the database](mariadb.md) or network configuration (follow
+ our [examples](../docker/docker-compose.md))
+
+*Start a full rescan if necessary, for example, if it looks
+like [thumbnails](index.md#broken-thumbnails) or [pictures are missing](index.md#missing-pictures).*
### App Not Loading ###
-If the app doesn't load in your browser when you navigate to the server URL, you can [check the browser console](browsers.md#getting-error-details)
-for helpful errors and warnings. Sometimes you just need to wait a moment, for example, if you are using a slow wireless
+If the app doesn't load in your browser when you navigate to the server URL, you
+can [check the browser console](browsers.md#getting-error-details)
+for helpful errors and warnings. Sometimes you just need to wait a moment, for example, if you are
+using a slow wireless
connection or the server was started only a few seconds ago. In case this does not help:
- [ ] You are using an [incompatible browser](browsers.md) (try another browser)
- [ ] JavaScript is disabled in your browser settings, so you only see the splash screen (enable it)
- [ ] JavaScript was disabled by a browser plugin (disable it or add an exception)
-- [ ] Your browser cannot communicate properly with the server, e.g. because a [Reverse Proxy](../proxies/nginx.md), VPN, or CDN is configured incorrectly (check its configuration and try without)
+- [ ] Your browser cannot communicate properly with the server, e.g. because
+ a [Reverse Proxy](../proxies/nginx.md), VPN, or CDN is configured incorrectly (check its
+ configuration and try without)
- [ ] HTTP security headers prevent the app from loading in a frame (override them)
- [ ] An ad blocker or other plugins block requests (disable them or add an exception)
- [ ] There is a problem with your network connection (test if other sites work)
@@ -132,11 +186,13 @@ connection or the server was started only a few seconds ago. In case this does n
### Missing Pictures ###
-If you have indexed your library and some images or videos are missing, first [check *Library > Errors* for errors and warnings](logs.md).
+If you have indexed your library and some images or videos are missing, first check the logs
In case the application logs don't contain anything helpful:
-- [ ] The files exceed the [size limit in megabyte or the resolution limit in megapixels](../config-options.md#storage)
-- [ ] The files have [bad filesystem permissions or the wrong owner](docker.md), so they cannot be opened
+- [ ] The files exceed
+ the [size limit in megabyte or the resolution limit in megapixels](../configuration/config-options.md#storage)
+- [ ] The files have [bad filesystem permissions or the wrong owner](docker.md), so they cannot be
+ opened
- [ ] The file type is generally unsupported
- [ ] The file type is generally supported, but a specific feature or codec is missing
- [ ] The indexer has skipped the files because they are exact duplicates
@@ -145,87 +201,130 @@ In case the application logs don't contain anything helpful:
- [ ] The file is broken, e.g. because of *short Huffman data* (try to fix it)
- [ ] [Your (virtual) server disk is full](docker.md#disk-space) (add storage)
- [ ] [The *storage* folder is not writable](docker.md) (change [permissions](docker.md))
- - [ ] A disk usage or the [inode limit](https://serverfault.com/questions/104986/what-is-the-maximum-number-of-files-a-file-system-can-contain) has been reached (remove or increase it)
+ - [ ] A disk usage or
+ the [inode limit](https://serverfault.com/questions/104986/what-is-the-maximum-number-of-files-a-file-system-can-contain)
+ has been reached (remove or increase it)
- [ ] Multiple files were [stacked](../../features/stacks.md) based on their metadata or file names
- [ ] You try to index a shared drive on a remote server, but the server is offline
-- [ ] The indexer has crashed because you didn't [configure at least 4 GB of swap](docker.md#adding-swap)
+- [ ] The indexer has crashed because you
+ didn't [configure at least 4 GB of swap](docker.md#adding-swap)
- [ ] Somebody has deleted files without telling you
- [ ] You are connected to the wrong server, VPN, CDN, or a DNS record has not been updated yet
-*Depending on the cause of the problem, you may need to perform a full rescan once the issue is resolved.*
+*Depending on the cause of the problem, you may need to perform a full rescan once the issue is
+resolved.*
#### Zip Archives ####
-When you try to download multiple pictures and find that some are missing from the resulting zip archive, or you get the error message "No files available for download," your index may be incomplete or out of date (for example, after updating Starsky). A complete rescan of your library may solve the problem.
+When you try to download multiple pictures and find that some are missing from the resulting zip
+archive, or you get the error message "No files available for download," your index may be
+incomplete or out of date (for example, after updating Starsky). A complete rescan of your library
+may solve the problem.
### Wrong Search Results ###
If search results are incorrect, for example, in the wrong order or not filtered properly:
- [ ] Indexing is still in progress and has not been completed yet
-- [ ] You need to [re-index your pictures](mariadb.md#complete-rescan), for example after updating Starsky
-- [ ] Previously [failed migrations must be re-run](mariadb.md#incompatible-schema) to update the index schema
+- [ ] You need to [re-index your pictures](mariadb.md#complete-rescan), for example after updating
+ Starsky
+- [ ] Previously [failed migrations must be re-run](mariadb.md#incompatible-schema) to update the
+ index schema
- [ ] The database server is [incompatible or needs to be updated](../readme.mdx#databases)
-*It may be a bug if you cannot find any other reasons, such as a local configuration problem or a misunderstanding in how the software works. Please note that reports must be reproducible in order for us to provide a solution.*
+*It may be a bug if you cannot find any other reasons, such as a local configuration problem or a
+misunderstanding in how the software works. Please note that reports must be reproducible in order
+for us to provide a solution.*
### Broken Thumbnails ###
-If some pictures have broken or missing thumbnails, first [check *Library > Errors* for errors and warnings](logs.md).
+If some pictures have broken or missing thumbnails, first [check the logs](logs.md).
In case the application logs don't contain anything helpful:
- [ ] The issue can be resolved by reloading the page or clearing the browser cache
- [ ] You browse non-JPEG files in *Library > Originals* which have an icon but no preview
- [ ] [Your (virtual) server disk is full](docker.md#disk-space) (add storage)
-- [ ] A disk usage or the [inode limit](https://serverfault.com/questions/104986/what-is-the-maximum-number-of-files-a-file-system-can-contain) has been reached (remove or increase it)
-- [ ] The *storage* folder [is not writable or mounted read-only](docker.md) (change [permissions](docker.md))
+- [ ] A disk usage or
+ the [inode limit](https://serverfault.com/questions/104986/what-is-the-maximum-number-of-files-a-file-system-can-contain)
+ has been reached (remove or increase it)
+- [ ] The *storage* folder [is not writable or mounted read-only](docker.md) (
+ change [permissions](docker.md))
- [ ] Files were deleted manually, for example to free up disk space
- [ ] Files can't be opened, e.g. because the file system permissions have been changed
- [ ] Files are stored on an unreliable device such as a USB flash drive or a shared network folder
-- [ ] Some thumbnails could not be created because you didn't [configure at least 4 GB of swap](docker.md#adding-swap)
-- [ ] Your browser cannot communicate properly with the server, e.g. because a [Reverse Proxy](../proxies/nginx.md), VPN, or CDN is configured incorrectly (check its configuration and try without)
+- [ ] Some thumbnails could not be created because you
+ didn't [configure at least 4 GB of swap](docker.md#adding-swap)
+- [ ] Your browser cannot communicate properly with the server, e.g. because
+ a [Reverse Proxy](../proxies/nginx.md), VPN, or CDN is configured incorrectly (check its
+ configuration and try without)
- [ ] Your proxy, router, or firewall has a request rate limit, so some requests fail
- [ ] There are other network problems caused by a firewall, router, or unstable connection
- [ ] An ad blocker or other plugins block requests (disable them or add an exception)
- [ ] You are connected to the wrong server, VPN, CDN, or a DNS record has not been updated yet
-We also recommend checking your [Docker Logs](docker.md#viewing-logs) for messages like *disk full*, *disk quota exceeded*,
-*no space left on device*, *read-only file system*, *error creating path*, *wrong permissions*, and *killed*:
+We also recommend checking your [Docker Logs](docker.md#viewing-logs) for messages like *disk full*,
+*disk quota exceeded*,
+*no space left on device*, *read-only file system*, *error creating path*, *wrong permissions*, and
+*killed*:
- [ ] If a service has been "killed" or otherwise automatically terminated, this points to a
-memory problem
-- [ ] In case the logs show "disk full", "quota exceeded", or "no space left" errors, either [the disk containing the *storage* folder is full](docker.md#disk-space) (add storage) or a disk usage limit is configured (remove or increase it)
-- [ ] Errors such as "read-only file system", "error creating path", or "wrong permissions" indicate a [filesystem permission problem](docker.md)
+ memory problem
+- [ ] In case the logs show "disk full", "quota exceeded", or "no space left" errors,
+ either [the disk containing the *storage* folder is full](docker.md#disk-space) (add storage) or a
+ disk usage limit is configured (remove or increase it)
+- [ ] Errors such as "read-only file system", "error creating path", or "wrong permissions" indicate
+ a [filesystem permission problem](docker.md)
-*Depending on the cause of the problem, you may need to perform a full rescan once the issue is resolved.*
+*Depending on the cause of the problem, you may need to perform a full rescan once the issue is
+resolved.*
### Videos Don't Play ###
If videos do not play and/or you only see a white/black area when you open a video:
-- [ ] You are using an [incompatible browser](browsers.md), e.g. without AVC support (try another browser)
-- [ ] AVC support or related JavaScript features have been disabled in your browser (check the settings and try another browser)
+- [ ] You are using an [incompatible browser](browsers.md), e.g. without AVC support (try another
+ browser)
+- [ ] AVC support or related JavaScript features have been disabled in your browser (check the
+ settings and try another browser)
- [ ] An ad blocker or other plugins block requests (disable them or add an exception)
- [ ] [Your (virtual) server disk is full](docker.md#disk-space) (add storage)
-- [ ] A disk usage or the [inode limit](https://serverfault.com/questions/104986/what-is-the-maximum-number-of-files-a-file-system-can-contain) has been reached (remove or increase it)
-- [ ] The *storage* folder [is not writable or mounted read-only](docker.md) (change [permissions](docker.md))
-- [ ] Files are stored on an unreliable device such as a USB flash drive or a shared network folder (check if the files are accessible)
-- [ ] Your browser cannot communicate properly with the server, e.g. because a [Reverse Proxy](../proxies/nginx.md), VPN, or CDN is configured incorrectly (check its configuration and try without)
-- [ ] There are other network problems caused by a proxy, firewall, or unstable connection (try a direct connection)
+- [ ] A disk usage or
+ the [inode limit](https://serverfault.com/questions/104986/what-is-the-maximum-number-of-files-a-file-system-can-contain)
+ has been reached (remove or increase it)
+- [ ] The *storage* folder [is not writable or mounted read-only](docker.md) (
+ change [permissions](docker.md))
+- [ ] Files are stored on an unreliable device such as a USB flash drive or a shared network
+ folder (check if the files are accessible)
+- [ ] Your browser cannot communicate properly with the server, e.g. because
+ a [Reverse Proxy](../proxies/nginx.md), VPN, or CDN is configured incorrectly (check its
+ configuration and try without)
+- [ ] There are other network problems caused by a proxy, firewall, or unstable connection (try a
+ direct connection)
- [ ] You are connected to the wrong server, VPN, CDN, or a DNS record has not been updated yet
-We recommend that you check your [Docker Logs](docker.md#viewing-logs) and [the browser console](browsers.md#getting-error-details)
-for messages related to *HTTP requests*, *permissions*, *security*, *FFmpeg*, *videos*, and *file conversion*.
+We recommend that you check your [Docker Logs](docker.md#viewing-logs)
+and [the browser console](browsers.md#getting-error-details)
+for messages related to *HTTP requests*, *permissions*, *security*, *FFmpeg*, *videos*, and *file
+conversion*.
Please note:
-1. Not all [video and audio formats](https://caniuse.com/?search=video%20format) can be [played with every browser](browsers.md). For example, [AAC](https://caniuse.com/aac "Advanced Audio Coding") - the default audio codec for [MPEG-4 AVC / H.264](https://caniuse.com/avc "Advanced Video Coding") - is supported natively in Chrome, Safari, and Edge, while it is only optionally supported by the OS in Firefox and Opera.
-2. HEVC/H.265 video files can have a `.mp4` file extension too, which is often associated with AVC only. This is because MP4 is a *container* format, meaning that the actual video content may be compressed with H.264, H.265, or something else. The file extension doesn't really tell you anything other than that it's probably a video file.
+1. Not all [video and audio formats](https://caniuse.com/?search=video%20format) can
+ be [played with every browser](browsers.md). For
+ example, [AAC](https://caniuse.com/aac "Advanced Audio Coding") - the default audio codec
+ for [MPEG-4 AVC / H.264](https://caniuse.com/avc "Advanced Video Coding") - is supported natively
+ in Chrome, Safari, and Edge, while it is only optionally supported by the OS in Firefox and
+ Opera.
+2. HEVC/H.265 video files can have a `.mp4` file extension too, which is often associated with AVC
+ only. This is because MP4 is a *container* format, meaning that the actual video content may be
+ compressed with H.264, H.265, or something else. The file extension doesn't really tell you
+ anything other than that it's probably a video file.
!!! info ""
- **We kindly ask you not to report bugs via *GitHub Issues* unless you are certain to have found a fully reproducible and previously unreported issue that must be fixed directly in the app.**
- Ask for technical support if you need help, it could be a local
- configuration problem, or a misunderstanding in how the software works.
+**We kindly ask you not to report bugs via *GitHub Issues* unless you are certain to have found a
+fully reproducible and previously unreported issue that must be fixed directly in the app.**
+Ask for technical support if you need help, it could be a local
+configuration problem, or a misunderstanding in how the software works.
## Used words
@@ -239,7 +338,7 @@ Please note:
*[RAW]: image format that contains unprocessed sensor data
*[URL]: Web Address
*[FFmpeg]: transcodes video files
-*[HEVC]: High Efficiency Video Coding / H.265
+*[HEVC]: High Efficiency Video Coding / H.265
*[SQLite]: self-contained, serverless SQL database
*[swap]: substitute for physical memory
*[host]: Computer, Cloud Server, or VM that runs Starsky
diff --git a/documentation/docs/getting-started/troubleshooting/logs.md b/documentation/docs/getting-started/troubleshooting/logs.md
index e6437a8732..06768535d7 100644
--- a/documentation/docs/getting-started/troubleshooting/logs.md
+++ b/documentation/docs/getting-started/troubleshooting/logs.md
@@ -5,34 +5,43 @@
The Electron stores it's cache in these folders:
Windows:
+
```
C:\Users\\AppData\Roaming\starsky\logs
```
Linux:
+
```
~/.config/starsky/logs
```
OS X:
+
```
~/Library/Application\ Support/starsky/logs
```
## "Browser"
-
-If you [have a frontend issue](browsers.md), it is often helpful to check the browser console for errors and warnings.
-A console is available in all modern browsers and can be activated via keyboard shortcuts or the browser menu.
-Problems with the user interface can be caused by a bug or an [incompatible browser](browsers.md#try-another-browser):
-Some [features may not be supported](https://caniuse.com/) by non-standard browsers, as well as nightly, unofficial,
+If you [have a frontend issue](browsers.md), it is often helpful to check the browser console for
+errors and warnings.
+A console is available in all modern browsers and can be activated via keyboard shortcuts or the
+browser menu.
+
+Problems with the user interface can be caused by a bug or
+an [incompatible browser](browsers.md#try-another-browser):
+Some [features may not be supported](https://caniuse.com/) by non-standard browsers, as well as
+nightly, unofficial,
or outdated versions.
-*In case you don't see any log messages, try reloading the page, as the problem may occur while the page is loading.*
+*In case you don't see any log messages, try reloading the page, as the problem may occur while the
+page is loading.*
**Chrome, Chromium, and Edge**
-- press ⌘+Option+J (Mac) or Ctrl+Shift+J (Windows, Linux, Chrome OS) to go directly to the Developer Tools
+- press ⌘+Option+J (Mac) or Ctrl+Shift+J (Windows, Linux, Chrome OS) to go directly to the Developer
+ Tools
- or, navigate to *More tools* > *Developer tools* in the browser menu and open the *Console* tab
**Firefox**
@@ -60,7 +69,8 @@ Run this command to display the last 100 log messages (omit `--tail=100` to see
docker compose logs --tail=100
```
-To enable [debug mode](../config-options.md), set `app__verbose` to `true` in the `environment:` section
+To enable [debug mode](../configuration/config-options.md), set `app__verbose` to `true` in
+the `environment:` section
of the `starsky` service (or use the `-v` flag when running the `starsky` command directly):
```yaml
@@ -70,18 +80,24 @@ services:
app__verbose: "true"
```
-Then restart all services for the changes to take effect. It can be helpful to keep Docker running in the foreground
-while debugging so that log messages are displayed directly. To do this, omit the `-d` parameter when restarting:
+Then restart all services for the changes to take effect. It can be helpful to keep Docker running
+in the foreground
+while debugging so that log messages are displayed directly. To do this, omit the `-d` parameter
+when restarting:
```bash
docker compose stop
docker compose up
```
-
-> **Note**
- If you see no errors or no logs at all, you may have started the server on a different host
- and/or port. There could also be an [issue with your browser](browsers.md), browser plugins, firewall settings,
- or other tools you may have installed.
-
-> **TL;DR**
- The default [Docker Compose](https://docs.docker.com/compose/) config filename is `docker-compose.yml`. For simplicity, it doesn't need to be specified when running the `docker-compose` command in the same directory. Config files for other apps or instances should be placed in separate folders.
+
+> **Note**
+> If you see no errors or no logs at all, you may have started the server on a different host
+> and/or port. There could also be an [issue with your browser](browsers.md), browser plugins,
+> firewall settings,
+> or other tools you may have installed.
+
+> **TL;DR**
+> The default [Docker Compose](https://docs.docker.com/compose/) config filename
+> is `docker-compose.yml`. For simplicity, it doesn't need to be specified when running
+> the `docker-compose` command in the same directory. Config files for other apps or instances should
+> be placed in separate folders.
diff --git a/documentation/docs/getting-started/troubleshooting/performance.md b/documentation/docs/getting-started/troubleshooting/performance.md
index 2ebda9a1c9..18e77c6ae7 100644
--- a/documentation/docs/getting-started/troubleshooting/performance.md
+++ b/documentation/docs/getting-started/troubleshooting/performance.md
@@ -2,11 +2,16 @@
## MariaDB ##
-The [InnoDB buffer pool](https://mariadb.com/kb/en/innodb-buffer-pool/) serves as a cache for data and indexes.
-It is a key component for optimizing MariaDB performance. Its size should be as large as possible to keep frequently
+The [InnoDB buffer pool](https://mariadb.com/kb/en/innodb-buffer-pool/) serves as a cache for data
+and indexes.
+It is a key component for optimizing MariaDB performance. Its size should be as large as possible to
+keep frequently
used data in memory and reduce disk I/O - typically the biggest bottleneck.
-By default, the buffer pool size is between 128 MB and 512 MB, depending on which configuration example you use. You can change it with the `--innodb-buffer-pool-size` command parameter in the `mariadb:` section of your `docker-compose.yml`. `M` stands for Megabyte, `G` for Gigabyte. Do not use spaces.
+By default, the buffer pool size is between 128 MB and 512 MB, depending on which configuration
+example you use. You can change it with the `--innodb-buffer-pool-size` command parameter in
+the `mariadb:` section of your `docker-compose.yml`. `M` stands for Megabyte, `G` for Gigabyte. Do
+not use spaces.
If your server has plenty of physical memory, we recommend increasing the size to 1 or 2 GB:
@@ -16,17 +21,28 @@ services:
command: mysqld --innodb-buffer-pool-size=1G ...
```
-As a rule of thumb, [`Innodb_buffer_pool_pages_free`](https://mariadb.com/kb/en/innodb-status-variables/#innodb_buffer_pool_pages_free) should never be [less than 5% of the total pages](https://vettabase.com/blog/is-innodb-buffer-pool-big-enough/).
-You can run the following SQL statement, for example using the [`mariadb` command](https://mariadb.com/kb/en/mysql-command-line-client/) in a terminal, to display the number of free pages and other InnoDB-related status information:
+As a rule of
+thumb, [`Innodb_buffer_pool_pages_free`](https://mariadb.com/kb/en/innodb-status-variables/#innodb_buffer_pool_pages_free)
+should never
+be [less than 5% of the total pages](https://vettabase.com/blog/is-innodb-buffer-pool-big-enough/).
+You can run the following SQL statement, for example using
+the [`mariadb` command](https://mariadb.com/kb/en/mysql-command-line-client/) in a terminal, to
+display the number of free pages and other InnoDB-related status information:
```SQL
SHOW GLOBAL STATUS LIKE 'Innodb_buffer%';
```
-Advanced users may adjust additional parameters to further improve performance. Tools such as the [mysqltuner.pl](https://github.com/major/MySQLTuner-perl) script can provide helpful recommendations for this.
+Advanced users may adjust additional parameters to further improve performance. Tools such as
+the [mysqltuner.pl](https://github.com/major/MySQLTuner-perl) script can provide helpful
+recommendations for this.
!!! info "Windows and macOS"
- If you are using *Docker Desktop* on Windows or macOS, remember to increase the [total memory available](../../assets/getting-started-docker-resources-advanced.jpg) for Docker services. Otherwise, they may run out of resources and cannot benefit from a larger cache size. In case Starsky and MariaDB are running in a virtual machine, its memory size should be increased as well. Restart for changes to take effect.
+If you are using *Docker Desktop* on Windows or macOS, remember to increase
+the [total memory available](../../assets/getting-started-docker-resources-advanced.jpg) for Docker
+services. Otherwise, they may run out of resources and cannot benefit from a larger cache size. In
+case Starsky and MariaDB are running in a virtual machine, its memory size should be increased as
+well. Restart for changes to take effect.
## Windows ##
@@ -34,7 +50,8 @@ Advanced users may adjust additional parameters to further improve performance.
## Storage ##
-Local Solid-State Drives (SSDs) are [best for databases](https://mariadb.com/de/resources/blog/how-to-tune-mariadb-write-performance/)
+Local Solid-State Drives (SSDs)
+are [best for databases](https://mariadb.com/de/resources/blog/how-to-tune-mariadb-write-performance/)
of any kind:
- database performance extremely benefits from high throughput which HDDs can't provide
@@ -42,49 +59,76 @@ of any kind:
- due to the HDD seek time, HDDs only support 5% of the reads per second of SSDs
- the cost savings from using slow hard disks are minimal
-Switching to SSDs makes a big difference, especially for write operations and when the read cache is not
+Switching to SSDs makes a big difference, especially for write operations and when the read cache is
+not
big enough or can't be used.
> **note**
- Never store database files on an unreliable device such as a USB flash drive, SD card, or shared network folder. These may also have [unexpected file size limitations](https://thegeekpage.com/fix-the-file-size-exceeds-the-limit-allowed-and-cannot-be-saved/), which is especially problematic for databases that do not split data into smaller files.
+> Never store database files on an unreliable device such as a USB flash drive, SD card, or shared
+> network folder. These may also
+> have [unexpected file size limitations](https://thegeekpage.com/fix-the-file-size-exceeds-the-limit-allowed-and-cannot-be-saved/),
+> which is especially problematic for databases that do not split data into smaller files.
## Memory ##
-Indexing large photo and video collections benefits from plenty of memory for [caching](#mariadb) and processing large media files.
-Ideally, the amount of RAM should match the number of physical CPU cores. If not, reduce the number of workers
+Indexing large photo and video collections benefits from plenty of memory for [caching](#mariadb)
+and processing large media files.
+Ideally, the amount of RAM should match the number of physical CPU cores. If not, reduce the number
+of workers
as [explained below](#troubleshooting).
-
## Server CPU ##
-Last but not least, performance can be limited by your server CPU. If you've tried everything else, then only moving
+Last but not least, performance can be limited by your server CPU. If you've tried everything else,
+then only moving
your instance to a more powerful device or cloud server may help.
-Be aware that most [NAS devices](https://kb.synology.com/en-us/DSM/tutorial/What_kind_of_CPU_does_my_NAS_have) are
-optimized for minimal power consumption and low production costs. Although their hardware gets faster with each generation,
-[benchmarks](https://www.google.com/search?q=cpu+benchmarks) show that even 8-year-old standard desktop CPUs like the [Intel Core i3-4130](https://www.cpubenchmark.net/compare/Intel-Pentium-J3710-vs-Intel-i3-4130/2784vs2015) are often many times faster:
+Be aware that
+most [NAS devices](https://kb.synology.com/en-us/DSM/tutorial/What_kind_of_CPU_does_my_NAS_have) are
+optimized for minimal power consumption and low production costs. Although their hardware gets
+faster with each generation,
+[benchmarks](https://www.google.com/search?q=cpu+benchmarks) show that even 8-year-old standard
+desktop CPUs like
+the [Intel Core i3-4130](https://www.cpubenchmark.net/compare/Intel-Pentium-J3710-vs-Intel-i3-4130/2784vs2015)
+are often many times faster:
![CPU Benchmark](../../assets/getting-started-troubleshooting-performance-passmark-cpu.svg)
## Legacy Hardware ##
-It is a known issue that the user interface and backend operations, especially face recognition, can be slow or even crash on older hardware due to a lack of resources. Like most applications, Starsky has certain requirements and our development process does not include testing on unsupported or unusual hardware.
+It is a known issue that the user interface and backend operations, especially face recognition, can
+be slow or even crash on older hardware due to a lack of resources. Like most applications, Starsky
+has certain requirements and our development process does not include testing on unsupported or
+unusual hardware.
-In many cases, performance can be improved through optimizations. Since these can prove to be very time-consuming and cost-intensive in practice, users and developers must decide on a case-by-case basis whether this provides sufficient benefit in relation to the costs or whether the use of more powerful hardware is faster and cheaper overall.
+In many cases, performance can be improved through optimizations. Since these can prove to be very
+time-consuming and cost-intensive in practice, users and developers must decide on a case-by-case
+basis whether this provides sufficient benefit in relation to the costs or whether the use of more
+powerful hardware is faster and cheaper overall.
-We kindly ask you not to open a problem report on GitHub Issues for poor performance on older hardware until a full cause and feasibility analysis has been performed. [GitHub Discussions](https://github.com/qdraw/starsky/discussions) or any of our other public forums and communities are great places to start a discussion.
+We kindly ask you not to open a problem report on GitHub Issues for poor performance on older
+hardware until a full cause and feasibility analysis has been
+performed. [GitHub Discussions](https://github.com/qdraw/starsky/discussions) or any of our other
+public forums and communities are great places to start a discussion.
-That being said, one of the advantages of open-source software is that users can submit [pull requests](https://github.com/qdraw/starsky) with performance and other enhancements they would like to see implemented. This will result in a much faster solution than waiting for a core team member to remotely analyze your problem and then provide a fix.
+That being said, one of the advantages of open-source software is that users can
+submit [pull requests](https://github.com/qdraw/starsky) with performance and other enhancements
+they would like to see implemented. This will result in a much faster solution than waiting for a
+core team member to remotely analyze your problem and then provide a fix.
## Troubleshooting ##
-If your server runs out of memory, the index is frequently locked, or other system resources are running low:
+If your server runs out of memory, the index is frequently locked, or other system resources are
+running low:
-- [ ] Try [reducing the number of workers](../config-options.md#index-workers) by setting `app__maxDegreesOfParallelism` to a reasonably small value in `docker-compose.yml`, depending on the CPU performance and number of cores
-- [ ] Make sure [your server has at least 4 GB of swap space](docker.md#adding-swap) so that indexing doesn't cause restarts when memory usage spikes; RAW image conversion and video transcoding are especially demanding
+- [ ] Try [reducing the number of workers](../configuration/config-options.md#index-workers) by
+ setting `app__maxDegreesOfParallelism` to a reasonably small value in `docker-compose.yml`,
+ depending on the CPU performance and number of cores
+- [ ] Make sure [your server has at least 4 GB of swap space](docker.md#adding-swap) so that
+ indexing doesn't cause restarts when memory usage spikes; RAW image conversion and video
+ transcoding are especially demanding
- [ ] If you are using SQLite, switch to MariaDB, which is better optimized for high concurrency
Other issues? Our [troubleshooting checklists](index.md) help you quickly diagnose and solve them.
-
*[SQLite]: self-contained, serverless SQL database
diff --git a/documentation/docs/getting-started/troubleshooting/sqlite.md b/documentation/docs/getting-started/troubleshooting/sqlite.md
index 61e432a691..15cef150ad 100644
--- a/documentation/docs/getting-started/troubleshooting/sqlite.md
+++ b/documentation/docs/getting-started/troubleshooting/sqlite.md
@@ -2,23 +2,40 @@
## Bad Performance
-If you have only a few images, concurrent users, and CPU cores, [SQLite](https://www.sqlite.org/) may seem faster compared to full-fledged database servers like [MariaDB](https://mariadb.com/).
+If you have only a few images, concurrent users, and CPU cores, [SQLite](https://www.sqlite.org/)
+may seem faster compared to full-fledged database servers like [MariaDB](https://mariadb.com/).
-This changes as the index grows and the number of concurrent requests increases. The way MariaDB handles multiple queries is completely different and optimized for high concurrency, while SQLite, for example, locks the index on updates so that other operations have to wait. In the worst case, this can lead to locking errors and timeouts during indexing - especially when combined with a slow disk or network storage.
+This changes as the index grows and the number of concurrent requests increases. The way MariaDB
+handles multiple queries is completely different and optimized for high concurrency, while SQLite,
+for example, locks the index on updates so that other operations have to wait. In the worst case,
+this can lead to locking errors and timeouts during indexing - especially when combined with a slow
+disk or network storage.
-The biggest advantage of SQLite is that you don't need to run a separate database server. This can be very useful for testing and works well if you only have a few thousand files to index. If you are looking for scalability and high performance, it is not a good choice.
+The biggest advantage of SQLite is that you don't need to run a separate database server. This can
+be very useful for testing and works well if you only have a few thousand files to index. If you are
+looking for scalability and high performance, it is not a good choice.
[Get MariaDB Performance Tips ›](performance.md#mariadb)
## Locking Errors
-If you use [traditional hard drives instead of SSDs](performance.md#storage), you will find that Starsky frequently runs into locking issues with SQLite because your CPU is many times faster than the mechanical heads of your disks. To some extent, this may also happen with solid-state drives, but it is much more likely with slow storage.
+If you use [traditional hard drives instead of SSDs](performance.md#storage), you will find that
+Starsky frequently runs into locking issues with SQLite because your CPU is many times faster than
+the mechanical heads of your disks. To some extent, this may also happen with solid-state drives,
+but it is much more likely with slow storage.
-You may be able to optimize the behavior and reduce locking errors with SQLite parameters that you can set with the [database config option](../config-options.md#database-connection), but ultimately you should use an SSD if you want to keep SQLite or switch to MariaDB. Please note that our team cannot provide support otherwise.
+You may be able to optimize the behavior and reduce locking errors with SQLite parameters that you
+can set with the [database config option](../configuration/config-options.md#database-connection),
+but ultimately you should use an SSD if you want to keep SQLite or switch to MariaDB. Please note
+that our team cannot provide support otherwise.
## Server Crashes
-If the server crashes unexpectedly or your database files get corrupted frequently, it is usually because they are stored on an unreliable device such as a USB flash drive, an SD card, or a shared network folder mounted via NFS or CIFS. These may also have [unexpected file size limitations](https://thegeekpage.com/fix-the-file-size-exceeds-the-limit-allowed-and-cannot-be-saved/), which is especially problematic for databases that do not split data into smaller files.
+If the server crashes unexpectedly or your database files get corrupted frequently, it is usually
+because they are stored on an unreliable device such as a USB flash drive, an SD card, or a shared
+network folder mounted via NFS or CIFS. These may also
+have [unexpected file size limitations](https://thegeekpage.com/fix-the-file-size-exceeds-the-limit-allowed-and-cannot-be-saved/),
+which is especially problematic for databases that do not split data into smaller files.
- [ ] Never use the same database files with more than one server instance
- [ ] Use SSDs instead of traditional hard drives, never use network storage
diff --git a/documentation/static/openapi/openapi.json b/documentation/static/openapi/openapi.json
index 39a4ca64d6..07e46a6cee 100644
--- a/documentation/static/openapi/openapi.json
+++ b/documentation/static/openapi/openapi.json
@@ -1967,7 +1967,7 @@
},
{
"unresolvedReference": false,
- "name": "UseLocalDesktopUi",
+ "name": "UseLocalDesktop",
"in": 0,
"required": false,
"deprecated": false,
@@ -2674,6 +2674,348 @@
"parameters": [],
"extensions": {}
},
+ "/api/desktop-editor/open": {
+ "operations": {
+ "Get": {
+ "tags": [
+ {
+ "name": "DesktopEditor",
+ "extensions": {},
+ "unresolvedReference": false
+ }
+ ],
+ "summary": "Open a file in the default editor or a specific editor on the desktop",
+ "parameters": [
+ {
+ "unresolvedReference": false,
+ "name": "f",
+ "in": 0,
+ "description": "single or multiple subPaths",
+ "required": false,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "explode": false,
+ "allowReserved": false,
+ "schema": {
+ "type": "string",
+ "default": {
+ "primitiveType": 4,
+ "anyType": 0,
+ "value": ""
+ },
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": false,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "examples": {},
+ "content": {},
+ "extensions": {}
+ },
+ {
+ "unresolvedReference": false,
+ "name": "collections",
+ "in": 0,
+ "description": "to combine files with the same name before the extension",
+ "required": false,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "explode": false,
+ "allowReserved": false,
+ "schema": {
+ "type": "boolean",
+ "default": {
+ "primitiveType": 7,
+ "anyType": 0,
+ "value": true
+ },
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": false,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "examples": {},
+ "content": {},
+ "extensions": {}
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "returns a list of items from the database",
+ "headers": {},
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "items": {
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": false,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false,
+ "reference": {
+ "type": 0,
+ "id": "PathImageFormatExistsAppPathModel",
+ "isExternal": false,
+ "isLocal": true,
+ "referenceV3": "#/components/schemas/PathImageFormatExistsAppPathModel",
+ "referenceV2": "#/definitions/PathImageFormatExistsAppPathModel"
+ }
+ },
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": false,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "examples": {},
+ "encoding": {},
+ "extensions": {}
+ }
+ },
+ "links": {},
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "204": {
+ "description": "No Content",
+ "headers": {},
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "items": {
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": false,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false,
+ "reference": {
+ "type": 0,
+ "id": "PathImageFormatExistsAppPathModel",
+ "isExternal": false,
+ "isLocal": true,
+ "referenceV3": "#/components/schemas/PathImageFormatExistsAppPathModel",
+ "referenceV2": "#/definitions/PathImageFormatExistsAppPathModel"
+ }
+ },
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": false,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "examples": {},
+ "encoding": {},
+ "extensions": {}
+ }
+ },
+ "links": {},
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {},
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "string",
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": false,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "examples": {},
+ "encoding": {},
+ "extensions": {}
+ }
+ },
+ "links": {},
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "401": {
+ "description": "User unauthorized",
+ "headers": {},
+ "content": {},
+ "links": {},
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "404": {
+ "description": "subPath not found in the database",
+ "headers": {},
+ "content": {},
+ "links": {},
+ "extensions": {},
+ "unresolvedReference": false
+ }
+ },
+ "callbacks": {},
+ "deprecated": false,
+ "security": [],
+ "servers": [],
+ "extensions": {}
+ }
+ },
+ "servers": [],
+ "parameters": [],
+ "extensions": {}
+ },
+ "/api/desktop-editor/amount-confirmation": {
+ "operations": {
+ "Get": {
+ "tags": [
+ {
+ "name": "DesktopEditor",
+ "extensions": {},
+ "unresolvedReference": false
+ }
+ ],
+ "summary": "Check the amount of files to open before",
+ "parameters": [
+ {
+ "unresolvedReference": false,
+ "name": "f",
+ "in": 0,
+ "description": "single or multiple subPaths",
+ "required": false,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "explode": false,
+ "allowReserved": false,
+ "schema": {
+ "type": "string",
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": false,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "examples": {},
+ "content": {},
+ "extensions": {}
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "bool, true is no confirmation, false is ask confirmation",
+ "headers": {},
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean",
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": false,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "examples": {},
+ "encoding": {},
+ "extensions": {}
+ }
+ },
+ "links": {},
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "401": {
+ "description": "User unauthorized",
+ "headers": {},
+ "content": {},
+ "links": {},
+ "extensions": {},
+ "unresolvedReference": false
+ }
+ },
+ "callbacks": {},
+ "deprecated": false,
+ "security": [],
+ "servers": [],
+ "extensions": {}
+ }
+ },
+ "servers": [],
+ "parameters": [],
+ "extensions": {}
+ },
"/api/disk/mkdir": {
"operations": {
"Post": {
@@ -10397,69 +10739,6 @@
"parameters": [],
"extensions": {}
},
- "/api/trash/detect-to-use-system-trash": {
- "operations": {
- "Get": {
- "tags": [
- {
- "name": "Trash",
- "extensions": {},
- "unresolvedReference": false
- }
- ],
- "summary": "Is the system trash supported",
- "parameters": [],
- "responses": {
- "200": {
- "description": "the item including the updated content",
- "headers": {},
- "content": {
- "application/json": {
- "schema": {
- "type": "boolean",
- "readOnly": false,
- "writeOnly": false,
- "allOf": [],
- "oneOf": [],
- "anyOf": [],
- "required": [],
- "properties": {},
- "additionalPropertiesAllowed": true,
- "enum": [],
- "nullable": false,
- "deprecated": false,
- "extensions": {},
- "unresolvedReference": false
- },
- "examples": {},
- "encoding": {},
- "extensions": {}
- }
- },
- "links": {},
- "extensions": {},
- "unresolvedReference": false
- },
- "401": {
- "description": "User unauthorized",
- "headers": {},
- "content": {},
- "links": {},
- "extensions": {},
- "unresolvedReference": false
- }
- },
- "callbacks": {},
- "deprecated": false,
- "security": [],
- "servers": [],
- "extensions": {}
- }
- },
- "servers": [],
- "parameters": [],
- "extensions": {}
- },
"/api/trash/move-to-trash": {
"operations": {
"Post": {
@@ -10470,7 +10749,7 @@
"unresolvedReference": false
}
],
- "summary": "(beta) Move a file to the trash",
+ "summary": "Move a file to the trash",
"parameters": [
{
"unresolvedReference": false,
@@ -11969,7 +12248,94 @@
"extensions": {},
"unresolvedReference": false
},
- "syncOnStartup": {
+ "syncOnStartup": {
+ "type": "boolean",
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": true,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "importIgnore": {
+ "type": "array",
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "items": {
+ "type": "string",
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": false,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": true,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "videoUseLocalTime": {
+ "type": "array",
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "items": {
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": false,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false,
+ "reference": {
+ "type": 0,
+ "id": "CameraMakeModel",
+ "isExternal": false,
+ "isLocal": true,
+ "referenceV3": "#/components/schemas/CameraMakeModel",
+ "referenceV2": "#/definitions/CameraMakeModel"
+ }
+ },
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": true,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "useLocalDesktop": {
"type": "boolean",
"readOnly": false,
"writeOnly": false,
@@ -11985,7 +12351,7 @@
"extensions": {},
"unresolvedReference": false
},
- "importIgnore": {
+ "defaultDesktopEditor": {
"type": "array",
"readOnly": false,
"writeOnly": false,
@@ -11994,7 +12360,6 @@
"anyOf": [],
"required": [],
"items": {
- "type": "string",
"readOnly": false,
"writeOnly": false,
"allOf": [],
@@ -12007,7 +12372,15 @@
"nullable": false,
"deprecated": false,
"extensions": {},
- "unresolvedReference": false
+ "unresolvedReference": false,
+ "reference": {
+ "type": 0,
+ "id": "AppSettingsDefaultEditorApplication",
+ "isExternal": false,
+ "isLocal": true,
+ "referenceV3": "#/components/schemas/AppSettingsDefaultEditorApplication",
+ "referenceV2": "#/definitions/AppSettingsDefaultEditorApplication"
+ }
},
"properties": {},
"additionalPropertiesAllowed": true,
@@ -12017,47 +12390,32 @@
"extensions": {},
"unresolvedReference": false
},
- "videoUseLocalTime": {
- "type": "array",
+ "desktopCollectionsOpen": {
"readOnly": false,
"writeOnly": false,
"allOf": [],
"oneOf": [],
"anyOf": [],
"required": [],
- "items": {
- "readOnly": false,
- "writeOnly": false,
- "allOf": [],
- "oneOf": [],
- "anyOf": [],
- "required": [],
- "properties": {},
- "additionalPropertiesAllowed": true,
- "enum": [],
- "nullable": false,
- "deprecated": false,
- "extensions": {},
- "unresolvedReference": false,
- "reference": {
- "type": 0,
- "id": "CameraMakeModel",
- "isExternal": false,
- "isLocal": true,
- "referenceV3": "#/components/schemas/CameraMakeModel",
- "referenceV2": "#/definitions/CameraMakeModel"
- }
- },
"properties": {},
"additionalPropertiesAllowed": true,
"enum": [],
- "nullable": true,
+ "nullable": false,
"deprecated": false,
"extensions": {},
- "unresolvedReference": false
+ "unresolvedReference": false,
+ "reference": {
+ "type": 0,
+ "id": "RawJpegMode",
+ "isExternal": false,
+ "isLocal": true,
+ "referenceV3": "#/components/schemas/RawJpegMode",
+ "referenceV2": "#/definitions/RawJpegMode"
+ }
},
- "useLocalDesktopUi": {
- "type": "boolean",
+ "desktopEditorAmountBeforeConfirmation": {
+ "type": "integer",
+ "format": "int32",
"readOnly": false,
"writeOnly": false,
"allOf": [],
@@ -12250,6 +12608,78 @@
"extensions": {},
"unresolvedReference": false
},
+ "AppSettingsDefaultEditorApplication": {
+ "type": "object",
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {
+ "imageFormats": {
+ "type": "array",
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "items": {
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": false,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false,
+ "reference": {
+ "type": 0,
+ "id": "ImageFormat",
+ "isExternal": false,
+ "isLocal": true,
+ "referenceV3": "#/components/schemas/ImageFormat",
+ "referenceV2": "#/definitions/ImageFormat"
+ }
+ },
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": true,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "applicationPath": {
+ "type": "string",
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": true,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false
+ }
+ },
+ "additionalPropertiesAllowed": false,
+ "enum": [],
+ "nullable": false,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false
+ },
"AppSettingsKeyValue": {
"type": "object",
"readOnly": false,
@@ -14484,6 +14914,150 @@
"extensions": {},
"unresolvedReference": false
},
+ "PathImageFormatExistsAppPathModel": {
+ "type": "object",
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {
+ "subPath": {
+ "type": "string",
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": true,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "fullFilePath": {
+ "type": "string",
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": true,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "imageFormat": {
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": false,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false,
+ "reference": {
+ "type": 0,
+ "id": "ImageFormat",
+ "isExternal": false,
+ "isLocal": true,
+ "referenceV3": "#/components/schemas/ImageFormat",
+ "referenceV2": "#/definitions/ImageFormat"
+ }
+ },
+ "status": {
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": false,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false,
+ "reference": {
+ "type": 0,
+ "id": "ExifStatus",
+ "isExternal": false,
+ "isLocal": true,
+ "referenceV3": "#/components/schemas/ExifStatus",
+ "referenceV2": "#/definitions/ExifStatus"
+ }
+ },
+ "appPath": {
+ "type": "string",
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [],
+ "nullable": true,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false
+ }
+ },
+ "additionalPropertiesAllowed": false,
+ "enum": [],
+ "nullable": false,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false
+ },
+ "RawJpegMode": {
+ "type": "integer",
+ "format": "int32",
+ "readOnly": false,
+ "writeOnly": false,
+ "allOf": [],
+ "oneOf": [],
+ "anyOf": [],
+ "required": [],
+ "properties": {},
+ "additionalPropertiesAllowed": true,
+ "enum": [
+ {
+ "primitiveType": 0,
+ "anyType": 0,
+ "value": 0
+ },
+ {
+ "primitiveType": 0,
+ "anyType": 0,
+ "value": 1
+ },
+ {
+ "primitiveType": 0,
+ "anyType": 0,
+ "value": 2
+ }
+ ],
+ "nullable": false,
+ "deprecated": false,
+ "extensions": {},
+ "unresolvedReference": false
+ },
"RelativeObjects": {
"type": "object",
"readOnly": false,
diff --git a/history.md b/history.md
index 31ca8cfc3a..b849c7dd75 100644
--- a/history.md
+++ b/history.md
@@ -42,7 +42,25 @@ Semantic Versioning 2.0.0 is from version 0.1.6+
## List of versions
## version 0.6.0-beta.2 - _(Unreleased)_ - 2024-02-? {#v0.6.0-beta.2}
+
- [x] (Changed) Back-end Upgrade to .NET 8 - SDK 8.0.201 (Runtime: 8.0.2) (PR #1402)
+- [x] (Added) _Back-end_ Native Open File on Windows & Mac OS (PR #1381)
+- [x] (Added) _Back-end_ Native Open File with specific editor on Windows & Mac OS (PR #1381)
+- [x] (Added) _Back-end_ AppSettings for Collections / Stacks and Open File (PR #1381)
+- [x] (Breaking Change) _Back-end_ Rename UseLocalDesktopUi to UseLocalDesktop (PR #1381)
+- [x] (Added) _Back-end_ ImageFormat = ExtensionRolesHelper.ImageFormat.directory (PR #1381)
+- [x] (Added) _Back-end_ Add role to info api (PR #1381)
+- [x] (Added) _Front-end_ Add settings for Open File (PR #1381)
+- [x] (Added) _Back-end_ rename starsky core to starsky.project.web (PR #1381)
+- [x] (Removed) _Back-end_ Remove /api/trash/detect-to-use-system-trash (PR #1381)
+- [x] (Removed) _Back-end_ Remove verbose option in UI (setting is hidden now) (PR #1381)
+- [x] (Added) _Front-end_ German translations (PR #1381)
+- [x] (Added) _Front-end_ command + shift + k go to settings now (PR #1381)
+- [x] (Removed) _App_ Removed overwrite of open app in desktop (replaced with native open file)
+ (PR #1381)
+- [x] (Added) _App_ Add 'App Settings' to the menu (PR #1381)
+- [x] (Added) _Front-end_ Add warning when opening a lot pictures at one: "Do you really want to
+ edit all of the selected photos?" (PR #1381)
## version 0.6.0-beta.1 - 2024-02-18 {#v0.6.0-beta.1}
@@ -193,7 +211,7 @@ _Known issues #1106, #1107 and #1108_
- [x] (Added) _Front-end_ Add MoreMenu remove current folder (PR #1085)
- [x] (Changed) _Front-end_ MoreMenu refactor (PR #1085)
- [x] (Changed) _Front-end_ Removal of Directories (PR #1085)
-- [x] (Changed) _Front-end_ Hide parts of menu in UseLocalDesktopUi mode (PR #1087)
+- [x] (Changed) _Front-end_ Hide parts of menu in UseLocalDesktop(Ui) mode (PR #1087)
- [x] (Fixed) _Front-end_ Fixed 300 eslint issues (PR #1087)
- [x] (Changed) _Back-end_ when deleting in systemTrash mode xmp files are now deleted (PR #1088)
- [x] (Changed) _Back-end_ test when deleting in server mode: xmp files are gone fixed (PR #1088)
diff --git a/starsky-tools/mock/api/desktop-editor/amount-confirmation.json b/starsky-tools/mock/api/desktop-editor/amount-confirmation.json
new file mode 100644
index 0000000000..f32a5804e2
--- /dev/null
+++ b/starsky-tools/mock/api/desktop-editor/amount-confirmation.json
@@ -0,0 +1 @@
+true
\ No newline at end of file
diff --git a/starsky-tools/mock/api/desktop-editor/open.json b/starsky-tools/mock/api/desktop-editor/open.json
new file mode 100644
index 0000000000..61ad49a945
--- /dev/null
+++ b/starsky-tools/mock/api/desktop-editor/open.json
@@ -0,0 +1,9 @@
+[
+ {
+ "subPath": "/20221029_101722_DSC05623.arw",
+ "fullFilePath": "/data/testcontent//20221029_101722_DSC05623.arw",
+ "imageFormat": 12,
+ "status": 8,
+ "appPath": ""
+ }
+]
diff --git a/starsky-tools/mock/api/env/features.json b/starsky-tools/mock/api/env/features.json
index 3b514380a0..ec7f18de73 100644
--- a/starsky-tools/mock/api/env/features.json
+++ b/starsky-tools/mock/api/env/features.json
@@ -1 +1 @@
-{"systemTrashEnabled":false,"useLocalDesktopUi":false}
\ No newline at end of file
+{"systemTrashEnabled":false,"useLocalDesktop":false, "openEditorEnabled": true}
\ No newline at end of file
diff --git a/starsky-tools/mock/set-router.js b/starsky-tools/mock/set-router.js
index 1f587ebe1a..3b7450c507 100644
--- a/starsky-tools/mock/set-router.js
+++ b/starsky-tools/mock/set-router.js
@@ -1,43 +1,45 @@
const express = require("express");
const path = require("path");
-var apiAccountChangeSecretIndex = require("./api/account/change-secret/index.json");
-var apiAccountPermissionsIndex = require("./api/account/permissions/index.json");
+const apiAccountChangeSecretIndex = require("./api/account/change-secret/index.json");
+const apiAccountPermissionsIndex = require("./api/account/permissions/index.json");
-var accountStatus = require("./api/account/status/index.json");
-var apiHealthDetails = require("./api/health/details/index.json");
-var apiHealthCheckForUpdates = require("./api/health/check-for-updates/index.json");
-var apiGeoReverseLookup = require("./api/geo-reverse-lookup/index.json");
+const accountStatus = require("./api/account/status/index.json");
+const apiHealthDetails = require("./api/health/details/index.json");
+const apiHealthCheckForUpdates = require("./api/health/check-for-updates/index.json");
+const apiGeoReverseLookup = require("./api/geo-reverse-lookup/index.json");
-var apiIndexIndex = require("./api/index/index.json");
-var apiIndex__Starsky = require("./api/index/__starsky.json");
-var apiIndex0001 = require("./api/index/0001.json");
-var apiIndex0001_toggleDeleted = require("./api/index/0001_toggleDeleted.json");
+const apiIndexIndex = require("./api/index/index.json");
+const apiIndex__Starsky = require("./api/index/__starsky.json");
+const apiIndex0001 = require("./api/index/0001.json");
+const apiIndex0001_toggleDeleted = require("./api/index/0001_toggleDeleted.json");
-var apiIndex__Starsky01dif = require("./api/index/__starsky_01-dif.json");
-var apiIndex__Starsky01difColorclass0 = require("./api/index/__starsky_01-dif_colorclass0.json");
+const apiIndex__Starsky01dif = require("./api/index/__starsky_01-dif.json");
+const apiIndex__Starsky01difColorclass0 = require("./api/index/__starsky_01-dif_colorclass0.json");
-var apiIndex__Starsky01dif20180101170001 = require("./api/index/__starsky_01-dif-2018.01.01.17.00.01.json");
+const apiIndex__Starsky01dif20180101170001 = require("./api/index/__starsky_01-dif-2018.01.01.17.00.01.json");
-var apiInfo__testJpg = require("./api/info/test.jpg.json");
+const apiInfo__testJpg = require("./api/info/test.jpg.json");
-var apiSearchTrash = require("./api/search/trash/index.json");
-var apiSearch = require("./api/search/index.json");
-var apiSearchTest = require("./api/search/test.json");
-var apiSearchTest1 = require("./api/search/test1.json");
-var apiUpdate__Starsky01dif20180101170001_Deleted = require("./api/update/__starsky_01-dif-2018.01.01.17.00.01_Deleted.json");
-var apiUpdate__Starsky01dif20180101170001_Ok = require("./api/update/__starsky_01-dif-2018.01.01.17.00.01_Ok.json");
+const apiSearchTrash = require("./api/search/trash/index.json");
+const apiSearch = require("./api/search/index.json");
+const apiSearchTest = require("./api/search/test.json");
+const apiSearchTest1 = require("./api/search/test1.json");
+const apiUpdate__Starsky01dif20180101170001_Deleted = require("./api/update/__starsky_01-dif-2018.01.01.17.00.01_Deleted.json");
+const apiUpdate__Starsky01dif20180101170001_Ok = require("./api/update/__starsky_01-dif-2018.01.01.17.00.01_Ok.json");
-var apiEnvIndex = require("./api/env/index.json");
-var apiEnvFeatures = require("./api/env/features.json");
+const apiEnvIndex = require("./api/env/index.json");
+const apiEnvFeatures = require("./api/env/features.json");
-var apiPublishIndex = require("./api/publish/index.json");
-var apiPublishCreateIndex = require("./api/publish/create/index.json");
+const apiPublishIndex = require("./api/publish/index.json");
+const apiPublishCreateIndex = require("./api/publish/create/index.json");
-var githubComReposQdrawStarskyReleaseIndex = require("./github.com/repos/qdraw/starsky/releases/index.json");
+const apiDeskopEditorOpen = require("./api/desktop-editor/open.json");
+
+const githubComReposQdrawStarskyReleaseIndex = require("./github.com/repos/qdraw/starsky/releases/index.json");
function setRouter(app, isStoryBook = false) {
- var prefix = "/starsky";
+ const prefix = "/starsky";
app.use(
prefix + "/api/thumbnail",
@@ -59,7 +61,7 @@ function setRouter(app, isStoryBook = false) {
res.json(accountStatus);
});
- var isChangePasswordSuccess = false;
+ let isChangePasswordSuccess = false;
app.post(prefix + "/api/account/change-secret/", (req, res) => {
console.log(req.body);
@@ -159,7 +161,7 @@ function setRouter(app, isStoryBook = false) {
return res.json("not found");
});
- var isDeleted = true;
+ let isDeleted = true;
app.post(prefix + "/api/update", (req, res) => {
if (!req.body) {
res.statusCode = 500;
@@ -248,6 +250,30 @@ function setRouter(app, isStoryBook = false) {
return res.json(apiEnvIndex);
});
+ app.post(prefix + "/api/desktop-editor/amount-confirmation", (req, res) => {
+ if (!req.body) {
+ return res.json("no body ~ the normal api does ignore it");
+ }
+ console.log(`amount-confirmation ${req.body.f}`);
+
+ return res.json(req.body.f !== "/true.jpg");
+ });
+
+ app.post(prefix + "/api/desktop-editor/open", (req, res) => {
+ if (!req.body) {
+ return res.json("no body ~ the normal api does ignore it");
+ }
+ console.log(`open ${req.body.f}`);
+
+ if (req.body.f === "/true.jpg") {
+ res.statusCode = 400;
+ res.json()
+ return
+ }
+
+ return res.json(apiDeskopEditorOpen);
+ });
+
app.get(prefix + "/api/health/application-insights", (req, res) => {
res.set("Content-Type", "application/javascript");
return res.send("");
@@ -282,7 +308,7 @@ function setRouter(app, isStoryBook = false) {
});
// Simulate waiting
- var fakeLoading = {};
+ let fakeLoading = {};
app.get(prefix + "/export/zip/:id", (req, res) => {
if (!fakeLoading[req.params.id]) {
fakeLoading[req.params.id] = 0;
diff --git a/starsky/starsky.feature.desktop/Interfaces/IOpenEditorDesktopService.cs b/starsky/starsky.feature.desktop/Interfaces/IOpenEditorDesktopService.cs
new file mode 100644
index 0000000000..ccc8312200
--- /dev/null
+++ b/starsky/starsky.feature.desktop/Interfaces/IOpenEditorDesktopService.cs
@@ -0,0 +1,29 @@
+using starsky.feature.desktop.Models;
+
+namespace starsky.feature.desktop.Interfaces;
+
+public interface IOpenEditorDesktopService
+{
+ ///
+ /// Check if the file is less then the amount of files that are allowed to open
+ /// If there are more files to open it will return false and the front-end will ask for confirmation
+ ///
+ /// dot comma list of paths
+ /// true is no confirmation and false ask are you sure
+ bool OpenAmountConfirmationChecker(string f);
+
+ ///
+ /// Is supported and enabled in the feature toggle
+ ///
+ /// Should you use it?
+ bool IsEnabled();
+
+ ///
+ /// Open a file in the default editor or specific editor which is set in the app settings
+ ///
+ /// dot comma split list with subPaths
+ /// should pick raw/jpeg file even its not specified
+ /// files done and list of results
+ Task<(bool?, string, List)> OpenAsync(string f,
+ bool collections);
+}
diff --git a/starsky/starsky.feature.desktop/Interfaces/IOpenEditorPreflight.cs b/starsky/starsky.feature.desktop/Interfaces/IOpenEditorPreflight.cs
new file mode 100644
index 0000000000..57f4fa37c4
--- /dev/null
+++ b/starsky/starsky.feature.desktop/Interfaces/IOpenEditorPreflight.cs
@@ -0,0 +1,9 @@
+using starsky.feature.desktop.Models;
+
+namespace starsky.feature.desktop.Interfaces;
+
+public interface IOpenEditorPreflight
+{
+ Task> PreflightAsync(
+ List inputFilePaths, bool collections);
+}
diff --git a/starsky/starsky.feature.desktop/Models/PathImageFormatExistsAppPathModel.cs b/starsky/starsky.feature.desktop/Models/PathImageFormatExistsAppPathModel.cs
new file mode 100644
index 0000000000..1000784e0a
--- /dev/null
+++ b/starsky/starsky.feature.desktop/Models/PathImageFormatExistsAppPathModel.cs
@@ -0,0 +1,18 @@
+using starsky.foundation.database.Models;
+using starsky.foundation.platform.Helpers;
+
+namespace starsky.feature.desktop.Models;
+
+public class PathImageFormatExistsAppPathModel
+{
+ public string SubPath { get; set; } = string.Empty;
+
+ public string FullFilePath { get; set; } = string.Empty;
+
+ public ExtensionRolesHelper.ImageFormat ImageFormat { get; set; } =
+ ExtensionRolesHelper.ImageFormat.notfound;
+
+ public FileIndexItem.ExifStatus Status { get; set; } = FileIndexItem.ExifStatus.Default;
+
+ public string AppPath { get; set; } = string.Empty;
+}
diff --git a/starsky/starsky.feature.desktop/Service/OpenEditorDesktopService.cs b/starsky/starsky.feature.desktop/Service/OpenEditorDesktopService.cs
new file mode 100644
index 0000000000..8e38a478a3
--- /dev/null
+++ b/starsky/starsky.feature.desktop/Service/OpenEditorDesktopService.cs
@@ -0,0 +1,131 @@
+using System.Runtime.CompilerServices;
+using starsky.feature.desktop.Interfaces;
+using starsky.feature.desktop.Models;
+using starsky.foundation.database.Models;
+using starsky.foundation.injection;
+using starsky.foundation.native.OpenApplicationNative.Interfaces;
+using starsky.foundation.platform.Helpers;
+using starsky.foundation.platform.Models;
+
+[assembly: InternalsVisibleTo("starskytest")]
+
+namespace starsky.feature.desktop.Service;
+
+[Service(typeof(IOpenEditorDesktopService), InjectionLifetime = InjectionLifetime.Scoped)]
+public class OpenEditorDesktopService : IOpenEditorDesktopService
+{
+ private readonly AppSettings _appSettings;
+ private readonly IOpenApplicationNativeService _openApplicationNativeService;
+ private readonly IOpenEditorPreflight _openEditorPreflight;
+
+ public OpenEditorDesktopService(AppSettings appSettings,
+ IOpenApplicationNativeService openApplicationNativeService,
+ IOpenEditorPreflight openEditorPreflight)
+ {
+ _appSettings = appSettings;
+ _openApplicationNativeService = openApplicationNativeService;
+ _openEditorPreflight = openEditorPreflight;
+ }
+
+ ///
+ /// Get value from App Settings without getting a negative value
+ ///
+ /// setting
+ private int GetDesktopEditorAmountBeforeConfirmation()
+ {
+ var desktopEditorAmountBeforeConfirmation =
+ _appSettings.DesktopEditorAmountBeforeConfirmation ??
+ DesktopEditorAmountBeforeConfirmationDefault;
+ if ( _appSettings.DesktopEditorAmountBeforeConfirmation <= 1 )
+ {
+ desktopEditorAmountBeforeConfirmation = DesktopEditorAmountBeforeConfirmationDefault;
+ }
+
+ return desktopEditorAmountBeforeConfirmation;
+ }
+
+ ///
+ /// Default Setting for Desktop Editor Amount Before Confirmation
+ ///
+ private const int DesktopEditorAmountBeforeConfirmationDefault = 5;
+
+ ///
+ /// Check for Desktop Editor Amount Before Confirmation
+ ///
+ /// dot comma seperated values
+ /// true
+ public bool OpenAmountConfirmationChecker(string f)
+ {
+ var inputFilePaths = PathHelper.SplitInputFilePaths(f);
+ return GetDesktopEditorAmountBeforeConfirmation() >= inputFilePaths.Length;
+ }
+
+ ///
+ /// Is feature toggle enabled and supported
+ ///
+ /// true is feature toggle enabled and supported
+ public bool IsEnabled()
+ {
+ return _appSettings.UseLocalDesktop == true &&
+ _openApplicationNativeService.DetectToUseOpenApplication();
+ }
+
+ public async Task<(bool?, string, List)> OpenAsync(string f,
+ bool collections)
+ {
+ var inputFilePaths = PathHelper.SplitInputFilePaths(f);
+ return await OpenAsync(inputFilePaths.ToList(), collections);
+ }
+
+ internal async Task<(bool?, string, List)> OpenAsync(
+ List subPaths, bool collections)
+ {
+ if ( _appSettings.UseLocalDesktop == false )
+ {
+ return ( null, "UseLocalDesktop feature toggle is disabled", [] );
+ }
+
+ if ( !_openApplicationNativeService.DetectToUseOpenApplication() )
+ {
+ return ( null, "OpenEditor is not supported on this configuration", [] );
+ }
+
+ var subPathAndImageFormatList = await _openEditorPreflight
+ .PreflightAsync(subPaths, collections);
+
+ if ( subPathAndImageFormatList.Count == 0 )
+ {
+ return ( false, "No files selected", [] );
+ }
+
+ var (openDefaultList, openWithEditorList) =
+ FilterListOpenDefaultEditorAndSpecificEditor(subPathAndImageFormatList);
+
+ _openApplicationNativeService.OpenDefault(openDefaultList);
+ _openApplicationNativeService.OpenApplicationAtUrl(openWithEditorList);
+
+ return ( true, "Opened", subPathAndImageFormatList );
+ }
+
+ ///
+ /// Filter the list
+ /// First is the list with the files that exists and AppPath is set
+ /// Second is the list with the files that exists but AppPath is not set
+ ///
+ ///
+ ///
+ internal static (List, List<(string FullFilePath, string AppPath)>)
+ FilterListOpenDefaultEditorAndSpecificEditor(
+ IReadOnlyCollection subPathAndImageFormatList)
+ {
+ var appPathList = subPathAndImageFormatList
+ .Where(p => p.Status == FileIndexItem.ExifStatus.Ok &&
+ string.IsNullOrEmpty(p.AppPath))
+ .Select(p => p.FullFilePath).ToList();
+ var noAppPathList = subPathAndImageFormatList
+ .Where(p => p.Status == FileIndexItem.ExifStatus.Ok &&
+ !string.IsNullOrEmpty(p.AppPath))
+ .Select(p => ( p.FullFilePath, p.AppPath )).ToList();
+ return ( appPathList, noAppPathList );
+ }
+}
diff --git a/starsky/starsky.feature.desktop/Service/OpenEditorPreflight.cs b/starsky/starsky.feature.desktop/Service/OpenEditorPreflight.cs
new file mode 100644
index 0000000000..94687db487
--- /dev/null
+++ b/starsky/starsky.feature.desktop/Service/OpenEditorPreflight.cs
@@ -0,0 +1,186 @@
+using starsky.feature.desktop.Interfaces;
+using starsky.feature.desktop.Models;
+using starsky.foundation.database.Helpers;
+using starsky.foundation.database.Interfaces;
+using starsky.foundation.database.Models;
+using starsky.foundation.injection;
+using starsky.foundation.platform.Enums;
+using starsky.foundation.platform.Helpers;
+using starsky.foundation.platform.Interfaces;
+using starsky.foundation.platform.Models;
+using starsky.foundation.storage.Interfaces;
+using starsky.foundation.storage.Models;
+using starsky.foundation.storage.Storage;
+
+namespace starsky.feature.desktop.Service;
+
+[Service(typeof(IOpenEditorPreflight), InjectionLifetime = InjectionLifetime.Scoped)]
+public class OpenEditorPreflight : IOpenEditorPreflight
+{
+ private readonly IQuery _query;
+ private readonly AppSettings _appSettings;
+ private readonly IWebLogger _logger;
+ private readonly IStorage _iStorage;
+ private readonly IStorage _hostFileSystem;
+
+ public OpenEditorPreflight(IQuery query, AppSettings appSettings,
+ ISelectorStorage selectorStorage, IWebLogger logger)
+ {
+ _query = query;
+ _appSettings = appSettings;
+ _logger = logger;
+ _iStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath);
+ _hostFileSystem = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem);
+ }
+
+ public async Task> PreflightAsync(
+ List inputFilePaths, bool collections)
+ {
+ var fileIndexItemList = await GetObjectsToOpenFromDatabase(inputFilePaths, collections);
+ fileIndexItemList = GroupByFileCollectionName(fileIndexItemList, collections);
+
+ var subPathAndImageFormatList = new List();
+
+ foreach ( var fileIndexItem in fileIndexItemList )
+ {
+ subPathAndImageFormatList.Add(new PathImageFormatExistsAppPathModel
+ {
+ AppPath = GetDesktopEditorPath(fileIndexItem.ImageFormat),
+ Status = fileIndexItem.Status,
+ ImageFormat = fileIndexItem.ImageFormat,
+ SubPath = fileIndexItem.FilePath!,
+ FullFilePath = _appSettings.DatabasePathToFilePath(fileIndexItem.FilePath!)
+ });
+ }
+
+ return subPathAndImageFormatList;
+ }
+
+ private string GetDesktopEditorPath(ExtensionRolesHelper.ImageFormat imageFormat)
+ {
+ var appSettingsDefaultEditor = _appSettings.DefaultDesktopEditor.Find(p =>
+ p.ImageFormats.Contains(imageFormat));
+
+ var appPath = appSettingsDefaultEditor?.ApplicationPath ?? string.Empty;
+
+ if ( string.IsNullOrEmpty(appPath) )
+ {
+ return string.Empty;
+ }
+
+ // Under Mac OS the ApplicationPath is a .app folder
+ // Under Windows the ApplicationPath is a .exe file
+ if ( _hostFileSystem.IsFolderOrFile(appPath) !=
+ FolderOrFileModel.FolderOrFileTypeList.Deleted )
+ {
+ return appPath;
+ }
+
+ _logger.LogError("[OpenEditorPreflight] AppPath not found: " + appPath);
+ return string.Empty;
+ }
+
+ internal async Task> GetObjectsToOpenFromDatabase(
+ List inputFilePaths, bool collections)
+ {
+ var resultFileIndexItemsList = await _query.GetObjectsByFilePathAsync(
+ inputFilePaths, collections);
+ var fileIndexList = new List();
+
+ foreach ( var fileIndexItem in resultFileIndexItemsList )
+ {
+ // Files that are not on disk
+ if ( _iStorage.IsFolderOrFile(fileIndexItem.FilePath!) ==
+ FolderOrFileModel.FolderOrFileTypeList.Deleted )
+ {
+ StatusCodesHelper.ReturnExifStatusError(fileIndexItem,
+ FileIndexItem.ExifStatus.NotFoundSourceMissing,
+ fileIndexList);
+ continue;
+ }
+
+ // Dir is readonly / don't edit
+ if ( new StatusCodesHelper(_appSettings).IsReadOnlyStatus(fileIndexItem)
+ == FileIndexItem.ExifStatus.ReadOnly )
+ {
+ StatusCodesHelper.ReturnExifStatusError(fileIndexItem,
+ FileIndexItem.ExifStatus.ReadOnly,
+ fileIndexList);
+ continue;
+ }
+
+ if ( fileIndexItem.ImageFormat is ExtensionRolesHelper.ImageFormat.xmp
+ or ExtensionRolesHelper.ImageFormat.meta_json )
+ {
+ continue;
+ }
+
+ if ( fileIndexItem.Status is FileIndexItem.ExifStatus.Default
+ or FileIndexItem.ExifStatus.OkAndSame )
+ {
+ fileIndexItem.Status = FileIndexItem.ExifStatus.Ok;
+ }
+
+ fileIndexList.Add(fileIndexItem);
+ }
+
+ return fileIndexList.DistinctBy(p => p.FilePath).ToList();
+ }
+
+ internal List GroupByFileCollectionName(
+ IEnumerable fileIndexInputList, bool collections = true)
+ {
+ // Skip if no collections, no need to filter on the right file
+ if ( !collections )
+ {
+ return fileIndexInputList.ToList();
+ }
+
+ if ( _appSettings.DesktopCollectionsOpen is CollectionsOpenType.RawJpegMode.Default )
+ {
+ _appSettings.DesktopCollectionsOpen = CollectionsOpenType.RawJpegMode.Jpeg;
+ }
+
+ var toOpenResultList = new List();
+
+ var groupedByName = fileIndexInputList.GroupBy(item => item.FileCollectionName);
+ foreach ( var group in groupedByName )
+ {
+ if ( group.Count() == 1 )
+ {
+ toOpenResultList.AddRange(group);
+ continue;
+ }
+
+ var byOrderResultList = new List();
+
+ switch ( _appSettings.DesktopCollectionsOpen )
+ {
+ case CollectionsOpenType.RawJpegMode.Jpeg:
+ byOrderResultList.AddRange(group.Where(p =>
+ p.ImageFormat is ExtensionRolesHelper.ImageFormat.jpg
+ or ExtensionRolesHelper.ImageFormat.bmp
+ or ExtensionRolesHelper.ImageFormat.png
+ or ExtensionRolesHelper.ImageFormat.gif
+ ));
+ break;
+ case CollectionsOpenType.RawJpegMode.Raw:
+ byOrderResultList.AddRange(group.Where(p =>
+ p.ImageFormat == ExtensionRolesHelper.ImageFormat.tiff));
+ break;
+ }
+
+ // When files are not found in the list, take the first one
+ if ( byOrderResultList.Count == 0 && group.FirstOrDefault() != null )
+ {
+ byOrderResultList.Add(group.First());
+ }
+
+ var fileIndexItem = byOrderResultList.OrderBy(p => p.ImageFormat).First();
+ toOpenResultList.Add(fileIndexItem);
+ }
+
+ // could be that the same file is in multiple collections
+ return toOpenResultList.DistinctBy(p => p.FilePath).ToList();
+ }
+}
diff --git a/starsky/starsky.feature.desktop/starsky.feature.desktop.csproj b/starsky/starsky.feature.desktop/starsky.feature.desktop.csproj
new file mode 100644
index 0000000000..fbabcff611
--- /dev/null
+++ b/starsky/starsky.feature.desktop/starsky.feature.desktop.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net8.0
+
+ {ca693c59-e50a-4dc6-a2ba-fe7e0caf417a}
+ 0.6.0-beta.0
+ enable
+ enable
+ starsky.feature.desktop
+
+
+
+
+
+
+
+
+
+
+
diff --git a/starsky/starsky.feature.import/Services/Import.cs b/starsky/starsky.feature.import/Services/Import.cs
index a0104c7b81..8a00bdaf2a 100644
--- a/starsky/starsky.feature.import/Services/Import.cs
+++ b/starsky/starsky.feature.import/Services/Import.cs
@@ -907,6 +907,7 @@ private async Task CreateNewDatabaseDirectory(string parentPath)
{
AddToDatabase = DateTime.UtcNow,
IsDirectory = true,
+ ImageFormat = ExtensionRolesHelper.ImageFormat.directory,
ColorClass = ColorClassParser.Color.None
};
diff --git a/starsky/starsky.feature.trash/Interfaces/IMoveToTrashService.cs b/starsky/starsky.feature.trash/Interfaces/IMoveToTrashService.cs
index 469dcaf294..d4b04a32b0 100644
--- a/starsky/starsky.feature.trash/Interfaces/IMoveToTrashService.cs
+++ b/starsky/starsky.feature.trash/Interfaces/IMoveToTrashService.cs
@@ -14,13 +14,6 @@ public interface IMoveToTrashService
Task> MoveToTrashAsync(List inputFilePaths,
bool collections);
- ///
- /// Is it supported to use the system trash
- /// But it does NOT check if the feature toggle is enabled
- ///
- /// true if supported
- bool DetectToUseSystemTrash();
-
///
/// Is supported and enabled in the feature toggle
///
diff --git a/starsky/starsky.feature.trash/Services/MoveToTrashService.cs b/starsky/starsky.feature.trash/Services/MoveToTrashService.cs
index 1f09bfc099..025ce4c603 100644
--- a/starsky/starsky.feature.trash/Services/MoveToTrashService.cs
+++ b/starsky/starsky.feature.trash/Services/MoveToTrashService.cs
@@ -10,6 +10,7 @@
using starsky.foundation.worker.Interfaces;
[assembly: InternalsVisibleTo("starskytest")]
+
namespace starsky.feature.trash.Services;
[Service(typeof(IMoveToTrashService), InjectionLifetime = InjectionLifetime.Scoped)]
@@ -46,17 +47,7 @@ ITrashConnectionService connectionService
public bool IsEnabled()
{
return _appSettings.UseSystemTrash == true &&
- _systemTrashService.DetectToUseSystemTrash();
- }
-
- ///
- /// Is it supported to use the system trash
- /// But it does NOT check if the feature toggle is enabled
- ///
- /// true if supported
- public bool DetectToUseSystemTrash()
- {
- return _systemTrashService.DetectToUseSystemTrash();
+ _systemTrashService.DetectToUseSystemTrash();
}
///
@@ -74,11 +65,13 @@ public async Task> MoveToTrashAsync(
await _metaPreflight.PreflightAsync(inputModel, inputFilePaths,
false, collections, 0);
- (fileIndexResultsList, changedFileIndexItemName) = await AppendChildItemsToTrashList(fileIndexResultsList, changedFileIndexItemName);
+ ( fileIndexResultsList, changedFileIndexItemName ) =
+ await AppendChildItemsToTrashList(fileIndexResultsList, changedFileIndexItemName);
var moveToTrashList =
fileIndexResultsList.Where(p =>
- p.Status is FileIndexItem.ExifStatus.Ok or FileIndexItem.ExifStatus.Deleted).ToList();
+ p.Status is FileIndexItem.ExifStatus.Ok or FileIndexItem.ExifStatus.Deleted)
+ .ToList();
var isSystemTrashEnabled = IsEnabled();
@@ -92,9 +85,8 @@ await _queue.QueueBackgroundWorkItemAsync(async _ =>
return;
}
- await MetaTrashInQueue(changedFileIndexItemName!,
+ await MetaTrashInQueue(changedFileIndexItemName,
fileIndexResultsList, inputModel, collections);
-
}, "trash");
return TrashConnectionService.StatusUpdate(moveToTrashList, isSystemTrashEnabled);
@@ -112,8 +104,9 @@ await _metaUpdateService.UpdateAsync(changedFileIndexItemName,
///
///
///
- internal async Task<(List, Dictionary>?)> AppendChildItemsToTrashList(List moveToTrash,
- Dictionary> changedFileIndexItemName)
+ internal async Task<(List, Dictionary>)>
+ AppendChildItemsToTrashList(List moveToTrash,
+ Dictionary> changedFileIndexItemName)
{
var parentSubPaths = moveToTrash
.Where(p => !string.IsNullOrEmpty(p.FilePath) && p.IsDirectory == true)
@@ -122,7 +115,7 @@ await _metaUpdateService.UpdateAsync(changedFileIndexItemName,
if ( parentSubPaths.Count == 0 )
{
- return (moveToTrash, changedFileIndexItemName);
+ return ( moveToTrash, changedFileIndexItemName );
}
var childItems = ( await _query.GetAllObjectsAsync(parentSubPaths) )
@@ -139,7 +132,7 @@ await _metaUpdateService.UpdateAsync(changedFileIndexItemName,
changedFileIndexItemName.TryAdd(childItem.FilePath!, new List { "tags" });
}
- return (moveToTrash, changedFileIndexItemName);
+ return ( moveToTrash, changedFileIndexItemName );
}
private async Task SystemTrashInQueue(List moveToTrash)
diff --git a/starsky/starsky.foundation.accountmanagement/Interfaces/IUserManager.cs b/starsky/starsky.foundation.accountmanagement/Interfaces/IUserManager.cs
index 05c63af249..17496fa95d 100644
--- a/starsky/starsky.foundation.accountmanagement/Interfaces/IUserManager.cs
+++ b/starsky/starsky.foundation.accountmanagement/Interfaces/IUserManager.cs
@@ -96,6 +96,8 @@ Task RemoveUser(string credentialTypeCode,
Task ExistAsync(int userTableId);
Role? GetRole(string credentialTypeCode, string identifier);
+
+ Task GetRoleAsync(int userId);
bool PreflightValidate(string userName, string password, string confirmPassword);
}
}
diff --git a/starsky/starsky.foundation.accountmanagement/Models/Account/UserStatusModel.cs b/starsky/starsky.foundation.accountmanagement/Models/Account/UserStatusModel.cs
index 73268b6b78..0e16160598 100644
--- a/starsky/starsky.foundation.accountmanagement/Models/Account/UserStatusModel.cs
+++ b/starsky/starsky.foundation.accountmanagement/Models/Account/UserStatusModel.cs
@@ -10,5 +10,6 @@ public sealed class UserIdentifierStatusModel
public DateTime Created { get; set; }
public List? CredentialsIdentifiers { get; set; } = new List();
public List? CredentialTypeIds { get; set; } = new List();
+ public string? RoleCode { get; set; }
}
}
diff --git a/starsky/starsky.foundation.accountmanagement/Services/UserManager.cs b/starsky/starsky.foundation.accountmanagement/Services/UserManager.cs
index fac0da02e7..2552255262 100644
--- a/starsky/starsky.foundation.accountmanagement/Services/UserManager.cs
+++ b/starsky/starsky.foundation.accountmanagement/Services/UserManager.cs
@@ -763,6 +763,14 @@ public int GetCurrentUserId(HttpContext httpContext)
return _dbContext.Roles.TagWith("GetRole").FirstOrDefault(p => p.Id == roleId);
}
+ public async Task GetRoleAsync(int userId)
+ {
+ var role = await _dbContext.UserRoles.FirstOrDefaultAsync(p => p.User != null && p.User.Id == userId);
+ if ( role == null ) return null;
+ var roleId = role.RoleId;
+ return _dbContext.Roles.TagWith("GetRole").FirstOrDefault(p => p.Id == roleId);
+ }
+
public Credential? GetCredentialsByUserId(int userId)
{
return _dbContext.Credentials
diff --git a/starsky/starsky.foundation.database/Helpers/Duplicate.cs b/starsky/starsky.foundation.database/Helpers/Duplicate.cs
index 4811732eda..552e2b1970 100644
--- a/starsky/starsky.foundation.database/Helpers/Duplicate.cs
+++ b/starsky/starsky.foundation.database/Helpers/Duplicate.cs
@@ -16,11 +16,12 @@ public Duplicate(IQuery query)
}
///
- /// Check and remove duplicate
+ /// Check and remove duplicate from database
///
///
///
- public async Task> RemoveDuplicateAsync(List databaseSubFolderList)
+ public async Task> RemoveDuplicateAsync(
+ List databaseSubFolderList)
{
// Get a list of duplicate items
var duplicateItemsByFilePath = databaseSubFolderList.GroupBy(item => item.FilePath)
@@ -37,6 +38,7 @@ public async Task> RemoveDuplicateAsync(List
await _query.RemoveItemAsync(duplicateItems[i]);
}
}
+
return databaseSubFolderList;
}
}
diff --git a/starsky/starsky.foundation.database/Helpers/StatusCodesHelper.cs b/starsky/starsky.foundation.database/Helpers/StatusCodesHelper.cs
index b43520434b..abc6aff159 100644
--- a/starsky/starsky.foundation.database/Helpers/StatusCodesHelper.cs
+++ b/starsky/starsky.foundation.database/Helpers/StatusCodesHelper.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using starsky.foundation.database.Models;
+using starsky.foundation.platform.Helpers;
using starsky.foundation.platform.Models;
namespace starsky.foundation.database.Helpers
@@ -16,7 +17,8 @@ public StatusCodesHelper(AppSettings appSettings)
public FileIndexItem.ExifStatus IsReadOnlyStatus(FileIndexItem fileIndexItem)
{
- if ( fileIndexItem.IsDirectory == true && _appSettings.IsReadOnly(fileIndexItem.FilePath!) )
+ if ( fileIndexItem.IsDirectory == true &&
+ _appSettings.IsReadOnly(fileIndexItem.FilePath!) )
{
return FileIndexItem.ExifStatus.DirReadOnly;
}
@@ -53,13 +55,16 @@ public FileIndexItem.ExifStatus IsReadOnlyStatus(DetailView? detailView)
public static FileIndexItem.ExifStatus IsDeletedStatus(FileIndexItem? fileIndexItem)
{
- return fileIndexItem?.Tags != null && fileIndexItem.Tags.Contains(TrashKeyword.TrashKeywordString) ?
- FileIndexItem.ExifStatus.Deleted : FileIndexItem.ExifStatus.Default;
+ return fileIndexItem?.Tags != null &&
+ fileIndexItem.Tags.Contains(TrashKeyword.TrashKeywordString)
+ ? FileIndexItem.ExifStatus.Deleted
+ : FileIndexItem.ExifStatus.Default;
}
public static FileIndexItem.ExifStatus IsDeletedStatus(DetailView? detailView)
{
- if ( !string.IsNullOrEmpty(detailView?.FileIndexItem?.Tags) && detailView.FileIndexItem.Tags.Contains(TrashKeyword.TrashKeywordString) )
+ if ( !string.IsNullOrEmpty(detailView?.FileIndexItem?.Tags) &&
+ detailView.FileIndexItem.Tags.Contains(TrashKeyword.TrashKeywordString) )
{
return FileIndexItem.ExifStatus.Deleted;
}
@@ -83,6 +88,7 @@ public static bool ReturnExifStatusError(FileIndexItem statusModel,
{
case FileIndexItem.ExifStatus.DirReadOnly:
statusModel.IsDirectory = true;
+ statusModel.ImageFormat = ExtensionRolesHelper.ImageFormat.directory;
statusModel.Status = FileIndexItem.ExifStatus.DirReadOnly;
fileIndexResultsList.Add(statusModel);
return true;
@@ -107,6 +113,7 @@ public static bool ReturnExifStatusError(FileIndexItem statusModel,
fileIndexResultsList.Add(statusModel);
return true;
}
+
return false;
}
@@ -120,6 +127,7 @@ public static bool ReadonlyDenied(FileIndexItem statusModel,
fileIndexResultsList.Add(statusModel);
return true;
}
+
return false;
}
@@ -132,7 +140,5 @@ public static void ReadonlyAllowed(FileIndexItem statusModel,
statusModel.Status = FileIndexItem.ExifStatus.ReadOnly;
fileIndexResultsList.Add(statusModel);
}
-
-
}
}
diff --git a/starsky/starsky.foundation.database/Query/QuerySingleItem.cs b/starsky/starsky.foundation.database/Query/QuerySingleItem.cs
index 964475a218..f4d83ca5d5 100644
--- a/starsky/starsky.foundation.database/Query/QuerySingleItem.cs
+++ b/starsky/starsky.foundation.database/Query/QuerySingleItem.cs
@@ -119,6 +119,7 @@ public partial class Query
if ( currentFileIndexItem.IsDirectory == true )
{
currentFileIndexItem.CollectionPaths = new List { singleItemDbPath };
+
return new DetailView
{
IsDirectory = true,
diff --git a/starsky/starsky.foundation.native/Helpers/OperatingSystemHelper.cs b/starsky/starsky.foundation.native/Helpers/OperatingSystemHelper.cs
index efccca4dfc..7bbf979784 100644
--- a/starsky/starsky.foundation.native/Helpers/OperatingSystemHelper.cs
+++ b/starsky/starsky.foundation.native/Helpers/OperatingSystemHelper.cs
@@ -11,20 +11,30 @@ public static OSPlatform GetPlatform()
internal delegate bool IsOsPlatformDelegate(OSPlatform osPlatform);
+ ///
+ /// Used to make the function testable
+ ///
+ /// Delegate to know what the OS is
+ /// Runtime OS
internal static OSPlatform GetPlatformInternal(IsOsPlatformDelegate isOsPlatformDelegate)
{
if ( isOsPlatformDelegate(OSPlatform.Windows) )
{
return OSPlatform.Windows;
}
+
if ( isOsPlatformDelegate(OSPlatform.OSX) )
{
return OSPlatform.OSX;
}
+
if ( isOsPlatformDelegate(OSPlatform.Linux) )
{
return OSPlatform.Linux;
}
- return isOsPlatformDelegate(OSPlatform.FreeBSD) ? OSPlatform.FreeBSD : OSPlatform.Create("Unknown");
+
+ return isOsPlatformDelegate(OSPlatform.FreeBSD)
+ ? OSPlatform.FreeBSD
+ : OSPlatform.Create("Unknown");
}
}
diff --git a/starsky/starsky.foundation.native/OpenApplicationNative/Helpers/MacOsOpenUrl.cs b/starsky/starsky.foundation.native/OpenApplicationNative/Helpers/MacOsOpenUrl.cs
new file mode 100644
index 0000000000..9397fc8b76
--- /dev/null
+++ b/starsky/starsky.foundation.native/OpenApplicationNative/Helpers/MacOsOpenUrl.cs
@@ -0,0 +1,150 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+using starsky.foundation.native.Trash.Helpers;
+
+namespace starsky.foundation.native.OpenApplicationNative.Helpers;
+
+[SuppressMessage("Interoperability",
+ "SYSLIB1054:Use \'LibraryImportAttribute\' instead of \'DllImportAttribute\' to " +
+ "generate P/Invoke marshalling code at compile time")]
+public static class MacOsOpenUrl
+{
+ ///
+ /// Add check if not Mac OS X
+ ///
+ ///
+ ///
+ ///
+ internal static bool? OpenDefault(
+ List fileUrls, OSPlatform platform)
+ {
+ return platform != OSPlatform.OSX ? null : OpenDefault(fileUrls);
+ }
+
+ ///
+ /// Does NOT check if file exists
+ ///
+ /// Absolute Path of file
+ ///
+ public static bool OpenDefault(
+ List fileUrls)
+ {
+ if ( fileUrls.Count == 0 )
+ {
+ return false;
+ }
+
+ var fileUrlsIntPtr = MacOsTrashBindingHelper.GetUrls(fileUrls);
+
+ var result = new List();
+ foreach ( var fileUrlIntPtr in fileUrlsIntPtr )
+ {
+ result.Add(InvokeOpenUrl(fileUrlIntPtr));
+ }
+
+ return result.TrueForAll(p => p);
+ }
+
+ internal static bool? OpenApplicationAtUrl(
+ List fileUrls,
+ string applicationUrl, OSPlatform platform)
+ {
+ return platform != OSPlatform.OSX ? null : OpenApplicationAtUrl(fileUrls, applicationUrl);
+ }
+
+ ///
+ /// Does NOT check if a file exists
+ /// No Fallback if NOT Mac OS X
+ ///
+ /// Absolute Paths
+ /// Open with .app folder
+ /// When not Mac OS
+ internal static bool? OpenApplicationAtUrl(
+ List fileUrls,
+ string applicationUrl)
+ {
+ if ( fileUrls.Count == 0 )
+ {
+ return false;
+ }
+
+ var filesUrlIntPtr = MacOsTrashBindingHelper.GetUrls(fileUrls);
+ var fileUrlIntPtrUrlArray = MacOsTrashBindingHelper.CreateCfArray(filesUrlIntPtr);
+
+ var applicationUrlIntPtr =
+ MacOsTrashBindingHelper.GetUrls([applicationUrl]).FirstOrDefault();
+
+ var nsWorkspaceOpenConfiguration = objc_getClass("NSWorkspaceOpenConfiguration");
+ var nsWorkspaceOpenConfigurationDefault = objc_msgSend_retIntPtr(
+ nsWorkspaceOpenConfiguration, MacOsTrashBindingHelper.GetSelector("configuration"));
+
+ // https://developer.apple.com/documentation/appkit/nsworkspace/3172702-openurls?language=objc
+ OpenUrLsWithApplicationAtUrl(fileUrlIntPtrUrlArray, applicationUrlIntPtr,
+ nsWorkspaceOpenConfigurationDefault);
+ return true;
+ }
+
+ ///
+ /// Open Default Url
+ ///
+ /// Pointer for urls
+ /// Is Success
+ internal static bool InvokeOpenUrl(IntPtr fileUrlIntPtr)
+ {
+ return objc_msgSend_retBool_IntPtr_IntPtr(
+ NsWorkspaceSharedWorkSpace(),
+ MacOsTrashBindingHelper.GetSelector("openURL:"),
+ fileUrlIntPtr);
+ }
+
+
+ ///
+ /// @see: https://developer.apple.com/documentation/appkit/nsworkspace/3172702-openurls?language=objc
+ ///
+ internal static void OpenUrLsWithApplicationAtUrl(nint fileUrlIntPtrUrlArray,
+ nint applicationUrlIntPtr, nint nsWorkspaceOpenConfigurationDefault)
+ {
+ objc_msgSend_retVoid_IntPtr_IntPtr_IntPtr_IntPtr(
+ NsWorkspaceSharedWorkSpace(),
+ MacOsTrashBindingHelper.GetSelector(
+ "openURLs:withApplicationAtURL:configuration:completionHandler:"),
+ fileUrlIntPtrUrlArray,
+ applicationUrlIntPtr,
+ nsWorkspaceOpenConfigurationDefault,
+ IntPtr.Zero);
+ }
+
+ private const string FoundationFramework =
+ "/System/Library/Frameworks/Foundation.framework/Foundation";
+
+ private const string AppKitFramework =
+ "/System/Library/Frameworks/AppKit.framework/AppKit";
+
+ [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")]
+ private static extern IntPtr objc_msgSend_retIntPtr(IntPtr target, IntPtr selector);
+
+ [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")]
+ private static extern IntPtr objc_msgSend_retVoid_IntPtr_IntPtr_IntPtr_IntPtr(
+ IntPtr target,
+ IntPtr selector,
+ IntPtr param1,
+ IntPtr param2,
+ IntPtr param3,
+ IntPtr param4);
+
+ [DllImport(AppKitFramework)]
+ [SuppressMessage("Globalization", "CA2101:Specify marshaling for P/Invoke string arguments")]
+ static extern IntPtr objc_getClass(string className);
+
+ [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")]
+ private static extern bool objc_msgSend_retBool_IntPtr_IntPtr(IntPtr target, IntPtr selector,
+ IntPtr param);
+
+ internal static IntPtr NsWorkspaceSharedWorkSpace()
+ {
+ // Namespace
+ var nsWorkspace = objc_getClass("NSWorkspace");
+ return objc_msgSend_retIntPtr(nsWorkspace,
+ MacOsTrashBindingHelper.GetSelector("sharedWorkspace"));
+ }
+}
diff --git a/starsky/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsOpenDesktopApp.cs b/starsky/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsOpenDesktopApp.cs
new file mode 100644
index 0000000000..eeb1a099de
--- /dev/null
+++ b/starsky/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsOpenDesktopApp.cs
@@ -0,0 +1,112 @@
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace starsky.foundation.native.OpenApplicationNative.Helpers;
+
+public static class WindowsOpenDesktopApp
+{
+ ///
+ /// Add check if is Windows
+ ///
+ /// full file paths
+ /// running platform
+ ///
+ internal static bool? OpenDefault(
+ List fileUrls, OSPlatform platform)
+ {
+ return platform != OSPlatform.Windows ? null : OpenDefault(fileUrls);
+ }
+
+ public static bool? OpenDefault(List fileUrls)
+ {
+ if ( fileUrls.Count == 0 )
+ {
+ return false;
+ }
+
+ var result = new List();
+ foreach ( var fileUrl in fileUrls )
+ {
+ result.Add(OpenDefault(fileUrl));
+ }
+
+ return result.TrueForAll(p => p == true);
+ }
+
+ ///
+ /// Does NOT check if file exists
+ ///
+ /// Absolute Path of file
+ ///
+ public static bool? OpenDefault(
+ string fileUrl)
+ {
+ try
+ {
+ var projectStartInfo = new ProcessStartInfo
+ {
+ FileName = fileUrl,
+ UseShellExecute = true,
+ WindowStyle = ProcessWindowStyle.Normal
+ };
+ var projectProcess = Process.Start(projectStartInfo);
+ return projectProcess != null;
+ }
+ catch ( Win32Exception )
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Skip if is MacOS
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal static bool? OpenApplicationAtUrl(
+ List fileUrls,
+ string applicationUrl, OSPlatform platform)
+ {
+ return platform != OSPlatform.Windows
+ ? null
+ : OpenApplicationAtUrl(fileUrls, applicationUrl);
+ }
+
+ ///
+ /// Internal
+ ///
+ ///
+ ///
+ ///
+ internal static bool OpenApplicationAtUrl(
+ List fileUrls,
+ string applicationUrl)
+ {
+ if ( fileUrls.Count == 0 )
+ {
+ return false;
+ }
+
+ var results = new List();
+ foreach ( var url in fileUrls )
+ {
+ var projectStartInfo = new ProcessStartInfo
+ {
+ FileName = applicationUrl,
+ WindowStyle = ProcessWindowStyle.Normal,
+ Arguments = url
+ };
+
+ var process = new Process { StartInfo = projectStartInfo };
+ var projectProcess = process.Start();
+ results.Add(projectProcess);
+
+ process.Dispose();
+ }
+
+ return results.TrueForAll(p => p);
+ }
+}
diff --git a/starsky/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsSetFileAssociations.cs b/starsky/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsSetFileAssociations.cs
new file mode 100644
index 0000000000..faa4292a0d
--- /dev/null
+++ b/starsky/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsSetFileAssociations.cs
@@ -0,0 +1,91 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+using Microsoft.Win32;
+using starsky.foundation.platform.Helpers;
+
+namespace starsky.foundation.native.OpenApplicationNative.Helpers;
+
+public class FileAssociation
+{
+ public string Extension { get; set; } = string.Empty;
+ public string ProgId { get; set; } = string.Empty;
+ public string FileTypeDescription { get; set; } = string.Empty;
+ public string ExecutableFilePath { get; set; } = string.Empty;
+}
+
+[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility",
+ Justification = "Check build in")]
+[SuppressMessage("ReSharper", "IdentifierTypo")]
+[SuppressMessage("ReSharper", "InconsistentNaming")]
+[SuppressMessage("Performance", "CA1806:Do not ignore method results")]
+[SuppressMessage("Interoperability", "SYSLIB1054:Use \'LibraryImportAttribute\' " +
+ "instead of \'DllImportAttribute\' to generate P/Invoke " +
+ "marshalling code at compile time")]
+public static class WindowsSetFileAssociations
+{
+ ///
+ /// needed so that Explorer windows get refreshed after the registry is updated
+ /// https://stackoverflow.com/questions/2681878/associate-file-extension-with-application
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ [DllImport("Shell32.dll")]
+ private static extern int SHChangeNotify(int eventId, int flags, IntPtr item1, IntPtr item2);
+
+ private const int SHCNE_ASSOCCHANGED = 0x8000000;
+ private const int SHCNF_FLUSH = 0x1000;
+
+ public static bool EnsureAssociationsSet(params FileAssociation[] associations)
+ {
+ var madeChanges = false;
+ foreach ( var association in associations )
+ {
+ madeChanges |= SetAssociation(
+ association.Extension,
+ association.ProgId,
+ association.FileTypeDescription,
+ association.ExecutableFilePath);
+ }
+
+ if ( madeChanges )
+ {
+ SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_FLUSH, IntPtr.Zero, IntPtr.Zero);
+ }
+
+ return madeChanges;
+ }
+
+ private static bool SetAssociation(string extension, string progId, string fileTypeDescription,
+ string applicationFilePath)
+ {
+ var madeChanges = false;
+ madeChanges |= SetKeyValue(@"Software\Classes\" + extension, progId);
+ madeChanges |= SetKeyValue(@"Software\Classes\" + progId, fileTypeDescription);
+ madeChanges |= SetKeyValue($@"Software\Classes\{progId}\shell\open\command",
+ "\"" + applicationFilePath + "\" \"%1\"");
+ return madeChanges;
+ }
+
+ internal static bool SetKeyValue(string keyPath, string value)
+ {
+ if ( !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) )
+ {
+ return false;
+ }
+
+ return RetryHelper.Do(SetValue, TimeSpan.FromSeconds(1), 2);
+
+ // Can sometimes have: System.IO.IOException: Illegal operation attempted
+ // on a registry key that has been marked for deletion
+ bool SetValue()
+ {
+ using var key = Registry.CurrentUser.CreateSubKey(keyPath);
+ if ( key.GetValue(null) as string == value ) return false;
+ key.SetValue(null, value);
+ return true;
+ }
+ }
+}
diff --git a/starsky/starsky.foundation.native/OpenApplicationNative/Interfaces/IOpenApplicationNativeService.cs b/starsky/starsky.foundation.native/OpenApplicationNative/Interfaces/IOpenApplicationNativeService.cs
new file mode 100644
index 0000000000..76c6ea6831
--- /dev/null
+++ b/starsky/starsky.foundation.native/OpenApplicationNative/Interfaces/IOpenApplicationNativeService.cs
@@ -0,0 +1,27 @@
+namespace starsky.foundation.native.OpenApplicationNative.Interfaces;
+
+public interface IOpenApplicationNativeService
+{
+ ///
+ /// Check if the system is supported to open a file
+ /// Not all configurations are supported
+ ///
+ /// true is supported and false is not supported
+ bool DetectToUseOpenApplication();
+
+ ///
+ /// Open with Default Editor
+ /// Please check DetectToUseOpenApplication() before using this method
+ ///
+ /// List first item is fullFilePath, second is ApplicationUrl
+ /// open = true, null is unsupported
+ bool? OpenApplicationAtUrl(List<(string fullFilePath, string applicationUrl)> fullPathAndApplicationUrl);
+
+ ///
+ /// Open with Default Editor
+ /// Please check DetectToUseOpenApplication() before using this method
+ ///
+ /// Paths on disk
+ /// open = true, null is unsupported
+ bool? OpenDefault(List fullPaths);
+}
diff --git a/starsky/starsky.foundation.native/OpenApplicationNative/OpenApplicationNativeService.cs b/starsky/starsky.foundation.native/OpenApplicationNative/OpenApplicationNativeService.cs
new file mode 100644
index 0000000000..0ce2cc0d12
--- /dev/null
+++ b/starsky/starsky.foundation.native/OpenApplicationNative/OpenApplicationNativeService.cs
@@ -0,0 +1,134 @@
+using System.Runtime.InteropServices;
+using starsky.foundation.injection;
+using starsky.foundation.native.Helpers;
+using starsky.foundation.native.OpenApplicationNative.Helpers;
+using starsky.foundation.native.OpenApplicationNative.Interfaces;
+
+namespace starsky.foundation.native.OpenApplicationNative;
+
+[Service(typeof(IOpenApplicationNativeService), InjectionLifetime = InjectionLifetime.Scoped)]
+public class OpenApplicationNativeService : IOpenApplicationNativeService
+{
+ ///
+ /// Is Open File supported on this configuration
+ ///
+ /// true if supported, false if not supported
+ public bool DetectToUseOpenApplication()
+ {
+ return DetectToUseOpenApplicationInternal(RuntimeInformation.IsOSPlatform,
+ Environment.UserInteractive);
+ }
+
+ ///
+ /// Use to overwrite the RuntimeInformation.IsOSPlatform
+ ///
+ internal delegate bool IsOsPlatformDelegate(OSPlatform osPlatform);
+
+ ///
+ /// Is Open File supported on this configuration
+ ///
+ /// RuntimeInformation.IsOSPlatform
+ /// Environment.UserInteractive
+ /// true if supported, false if not supported
+ internal static bool DetectToUseOpenApplicationInternal(
+ IsOsPlatformDelegate runtimeInformationIsOsPlatform,
+ bool environmentUserInteractive)
+ {
+ // Linux is not supported yet
+ if ( runtimeInformationIsOsPlatform(OSPlatform.Linux) ||
+ runtimeInformationIsOsPlatform(OSPlatform.FreeBSD) )
+ {
+ return false;
+ }
+
+ // When running in Windows as Service it does not open the application
+ // On Mac OS it does open the application
+ if ( !environmentUserInteractive && runtimeInformationIsOsPlatform(OSPlatform.Windows) )
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+
+ ///
+ /// Open file with specified application
+ ///
+ /// List first item is fullFilePath, second is ApplicationUrl
+ /// true is operation succeed, false failed | null is platform unsupported
+ public bool? OpenApplicationAtUrl(
+ List<(string fullFilePath, string applicationUrl)> fullPathAndApplicationUrl)
+ {
+ if ( fullPathAndApplicationUrl.Count == 0 )
+ {
+ return false;
+ }
+
+ var filesByApplicationPath = SortToOpenFilesByApplicationPath(fullPathAndApplicationUrl);
+
+ var results = new List();
+ foreach ( var (fullFilePaths, applicationPath) in filesByApplicationPath )
+ {
+ results.Add(OpenApplicationAtUrl(fullFilePaths, applicationPath));
+ }
+
+ if ( results.Contains(null) )
+ {
+ return null;
+ }
+
+ return results.TrueForAll(p => p == true);
+ }
+
+ ///
+ /// Open file with specified application
+ ///
+ /// full path style
+ /// applicationUrl
+ /// true is operation succeed, false failed | null is platform unsupported
+ internal static bool? OpenApplicationAtUrl(List fullPaths, string applicationUrl)
+ {
+ var currentPlatform = OperatingSystemHelper.GetPlatform();
+ var macOsOpenResult = MacOsOpenUrl.OpenApplicationAtUrl(fullPaths,
+ applicationUrl, currentPlatform);
+
+ var windowsOpenResult = WindowsOpenDesktopApp.OpenApplicationAtUrl(fullPaths,
+ applicationUrl, currentPlatform);
+
+ return macOsOpenResult ?? windowsOpenResult;
+ }
+
+ internal static List<(List, string)> SortToOpenFilesByApplicationPath(
+ List<(string fullFilePath, string applicationUrl)> fullPathAndApplicationUrl)
+ {
+ // Group applications by their names
+ var groupedApplications = fullPathAndApplicationUrl.GroupBy(x => x.Item2).ToList();
+
+ // Extract full paths for each application and call the implemented function
+ var results = new List<(List, string)>();
+ foreach ( var group in groupedApplications )
+ {
+ var fullPaths = group.Select(item => item.Item1).ToList();
+ var applicationUrl = group.Key;
+ results.Add(( fullPaths, applicationUrl ));
+ }
+
+ return results;
+ }
+
+ ///
+ /// Open file with default application
+ ///
+ /// full path style
+ /// true is operation succeed, false failed | null is platform unsupported
+ public bool? OpenDefault(List fullPaths)
+ {
+ var currentPlatform = OperatingSystemHelper.GetPlatform();
+ var macOsOpenResult = MacOsOpenUrl.OpenDefault(fullPaths, currentPlatform);
+ var windowsOpenResult = WindowsOpenDesktopApp.OpenDefault(fullPaths,
+ currentPlatform);
+
+ return macOsOpenResult ?? windowsOpenResult;
+ }
+}
diff --git a/starsky/starsky.foundation.native/References/OpenDefaultApp.bak b/starsky/starsky.foundation.native/References/OpenDefaultApp.bak
new file mode 100644
index 0000000000..be0727ac0f
--- /dev/null
+++ b/starsky/starsky.foundation.native/References/OpenDefaultApp.bak
@@ -0,0 +1,40 @@
+using System.Diagnostics.CodeAnalysis;
+using starsky.foundation.native.Trash.Helpers;
+
+namespace starsky.foundation.native.OpenApplicationNative.Helpers;
+
+using System;
+using System.Runtime.InteropServices;
+
+public class MacOsOpenDefaultApp
+{
+ public static void SetDefaultApplicationAtURL(
+ string applicationURL,
+ string fileURL)
+ {
+ var nsUrl = MacOsTrashBindingHelper.GetUrls([applicationURL]).FirstOrDefault();
+ var fileUrl = MacOsTrashBindingHelper.GetUrls([fileURL]).FirstOrDefault();
+
+ objc_msgSend_retVoid_IntPtr_IntPtr_IntPtr(
+ MacOsOpenUrl.NsWorkspaceSharedWorksPace(),
+ MacOsTrashBindingHelper.GetSelector(
+ "setDefaultApplicationAtURL:toOpenFileAtURL:completionHandler:"),
+ nsUrl,
+ fileUrl,
+ IntPtr.Zero);
+ }
+
+ private const string FoundationFramework =
+ "/System/Library/Frameworks/Foundation.framework/Foundation";
+
+ [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")]
+ private static extern void objc_msgSend_retVoid_IntPtr_IntPtr_IntPtr(
+ IntPtr target,
+ IntPtr selector,
+ IntPtr param1,
+ IntPtr param2,
+ IntPtr param3);
+
+
+
+}
diff --git a/starsky/starsky.foundation.native/Trash/Helpers/MacOSTrashBindingHelper.cs b/starsky/starsky.foundation.native/Trash/Helpers/MacOSTrashBindingHelper.cs
index 93e54f568c..5a6ed0957c 100644
--- a/starsky/starsky.foundation.native/Trash/Helpers/MacOSTrashBindingHelper.cs
+++ b/starsky/starsky.foundation.native/Trash/Helpers/MacOSTrashBindingHelper.cs
@@ -79,6 +79,11 @@ internal static void TrashInternal(List filesFullPath)
// CFRelease the fileUrl, sharedWorkspace, nsWorkspace gives a crash (error 139)
}
+ ///
+ /// Get Selector in the Objective-C runtime
+ ///
+ /// Name
+ /// Object
internal static IntPtr GetSelector(string name)
{
var cfStrSelector = CreateCfString(name);
diff --git a/starsky/starsky.foundation.native/Trash/TrashService.cs b/starsky/starsky.foundation.native/Trash/TrashService.cs
index 55a3734776..cc5b79d1ff 100644
--- a/starsky/starsky.foundation.native/Trash/TrashService.cs
+++ b/starsky/starsky.foundation.native/Trash/TrashService.cs
@@ -9,7 +9,6 @@ namespace starsky.foundation.native.Trash;
[Service(typeof(ITrashService), InjectionLifetime = InjectionLifetime.Scoped)]
public class TrashService : ITrashService
{
-
///
/// Is the system trash supported
///
@@ -33,14 +32,15 @@ public bool DetectToUseSystemTrash()
/// Environment.UserInteractive
/// Environment.UserName
/// true if supported, false if not supported
- internal static bool DetectToUseSystemTrashInternal(IsOsPlatformDelegate runtimeInformationIsOsPlatform,
+ internal static bool DetectToUseSystemTrashInternal(
+ IsOsPlatformDelegate runtimeInformationIsOsPlatform,
bool environmentUserInteractive,
string environmentUserName)
{
// ReSharper disable once ConvertIfStatementToReturnStatement
if ( runtimeInformationIsOsPlatform(OSPlatform.Linux) ||
- runtimeInformationIsOsPlatform(OSPlatform.FreeBSD) ||
- environmentUserName == "root" || !environmentUserInteractive )
+ runtimeInformationIsOsPlatform(OSPlatform.FreeBSD) ||
+ environmentUserName == "root" || !environmentUserInteractive )
{
return false;
}
@@ -63,10 +63,7 @@ internal static bool DetectToUseSystemTrashInternal(IsOsPlatformDelegate runtime
/// operation succeed (NOT if file is gone)
public bool? Trash(string fullPath)
{
- var currentPlatform = OperatingSystemHelper.GetPlatform();
- var macOsTrash = MacOsTrashBindingHelper.Trash(fullPath, currentPlatform);
- var (windowsTrash, _) = WindowsShellTrashBindingHelper.Trash(fullPath, currentPlatform);
- return macOsTrash ?? windowsTrash;
+ return Trash([fullPath]);
}
public bool? Trash(List fullPaths)
diff --git a/starsky/starsky.foundation.native/starsky.foundation.native.csproj b/starsky/starsky.foundation.native/starsky.foundation.native.csproj
index 7918c8d1ee..3d1dd59be7 100644
--- a/starsky/starsky.foundation.native/starsky.foundation.native.csproj
+++ b/starsky/starsky.foundation.native/starsky.foundation.native.csproj
@@ -12,7 +12,7 @@
-
+
+
-
diff --git a/starsky/starsky.foundation.platform/Enums/CollectionsOpenType.cs b/starsky/starsky.foundation.platform/Enums/CollectionsOpenType.cs
new file mode 100644
index 0000000000..5f54929d1b
--- /dev/null
+++ b/starsky/starsky.foundation.platform/Enums/CollectionsOpenType.cs
@@ -0,0 +1,11 @@
+namespace starsky.foundation.platform.Enums;
+
+public static class CollectionsOpenType
+{
+ public enum RawJpegMode
+ {
+ Default = 0,
+ Jpeg = 1,
+ Raw = 2,
+ }
+}
diff --git a/starsky/starsky.foundation.platform/Helpers/AppSettingsCompareHelper.cs b/starsky/starsky.foundation.platform/Helpers/AppSettingsCompareHelper.cs
index dcf80bef8c..60b1395490 100644
--- a/starsky/starsky.foundation.platform/Helpers/AppSettingsCompareHelper.cs
+++ b/starsky/starsky.foundation.platform/Helpers/AppSettingsCompareHelper.cs
@@ -3,6 +3,8 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json;
+using starsky.foundation.platform.Enums;
+using starsky.foundation.platform.Helpers.Compare;
using starsky.foundation.platform.JsonConverter;
using starsky.foundation.platform.Models;
@@ -10,7 +12,6 @@ namespace starsky.foundation.platform.Helpers
{
public static class AppSettingsCompareHelper
{
-
///
/// Compare a fileIndex item and update items if there are changed in the updateObject
/// append => (propertyName == "Tags" add it with comma space or with single space)
@@ -21,8 +22,10 @@ public static class AppSettingsCompareHelper
public static List Compare(AppSettings sourceIndexItem, object? updateObject = null)
{
updateObject ??= new AppSettings();
- var propertiesA = sourceIndexItem.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
- var propertiesB = updateObject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
+ var propertiesA = sourceIndexItem.GetType()
+ .GetProperties(BindingFlags.Public | BindingFlags.Instance);
+ var propertiesB = updateObject.GetType()
+ .GetProperties(BindingFlags.Public | BindingFlags.Instance);
var differenceList = new List();
foreach ( var propertyB in propertiesB )
@@ -31,42 +34,91 @@ public static List Compare(AppSettings sourceIndexItem, object? updateOb
var propertyInfoFromA = Array.Find(propertiesA, p => p.Name == propertyB.Name);
if ( propertyInfoFromA == null ) continue;
- CompareMultipleSingleItems(propertyB, propertyInfoFromA, sourceIndexItem, updateObject, differenceList);
- CompareMultipleListDictionary(propertyB, propertyInfoFromA, sourceIndexItem, updateObject, differenceList);
- CompareMultipleObjects(propertyB, propertyInfoFromA, sourceIndexItem, updateObject, differenceList);
-
+ CompareMultipleSingleItems(propertyB, propertyInfoFromA, sourceIndexItem,
+ updateObject, differenceList);
+ CompareMultipleListDictionary(propertyB, propertyInfoFromA, sourceIndexItem,
+ updateObject, differenceList);
+ CompareListMultipleObjects(propertyB, propertyInfoFromA, sourceIndexItem,
+ updateObject,
+ differenceList);
}
+
return differenceList;
}
- private static void CompareMultipleObjects(PropertyInfo propertyB, PropertyInfo propertyInfoFromA, AppSettings sourceIndexItem, object updateObject, List differenceList)
+ private static void CompareListMultipleObjects(PropertyInfo propertyB,
+ PropertyInfo propertyInfoFromA, AppSettings sourceIndexItem, object updateObject,
+ List differenceList)
{
- if ( propertyInfoFromA.PropertyType == typeof(OpenTelemetrySettings) && propertyB.PropertyType == typeof(OpenTelemetrySettings) )
+ if ( propertyInfoFromA.PropertyType == typeof(OpenTelemetrySettings) &&
+ propertyB.PropertyType == typeof(OpenTelemetrySettings) )
+ {
+ var oldObjectValue =
+ ( OpenTelemetrySettings? )propertyInfoFromA.GetValue(sourceIndexItem, null);
+ var newObjectValue =
+ ( OpenTelemetrySettings? )propertyB.GetValue(updateObject, null);
+ CompareOpenTelemetrySettingsObject(propertyB.Name, sourceIndexItem, oldObjectValue,
+ newObjectValue, differenceList);
+ }
+
+ if ( propertyInfoFromA.PropertyType ==
+ typeof(List) &&
+ propertyB.PropertyType == typeof(List) )
{
- var oldObjectValue = ( OpenTelemetrySettings? )propertyInfoFromA.GetValue(sourceIndexItem, null);
- var newObjectValue = ( OpenTelemetrySettings? )propertyB.GetValue(updateObject, null);
- CompareOpenTelemetrySettingsObject(propertyB.Name, sourceIndexItem, oldObjectValue, newObjectValue, differenceList);
+ var oldObjectValue =
+ ( List? )propertyInfoFromA.GetValue(
+ sourceIndexItem, null);
+ var newObjectValue =
+ ( List? )propertyB.GetValue(updateObject,
+ null);
+ CompareAppSettingsDefaultEditorApplication(propertyB.Name, sourceIndexItem,
+ oldObjectValue,
+ newObjectValue, differenceList);
}
}
- [SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance")]
- private static void CompareOpenTelemetrySettingsObject(string propertyName, AppSettings? sourceIndexItem,
+ [SuppressMessage("Performance",
+ "CA1859:Use concrete types when possible for improved performance")]
+ private static void CompareOpenTelemetrySettingsObject(string propertyName,
+ AppSettings? sourceIndexItem,
OpenTelemetrySettings? oldKeyValuePairStringStringValue,
- OpenTelemetrySettings? newKeyValuePairStringStringValue, ICollection differenceList)
+ OpenTelemetrySettings? newKeyValuePairStringStringValue,
+ ICollection differenceList)
{
if ( oldKeyValuePairStringStringValue == null ||
- newKeyValuePairStringStringValue == null ||
- // compare lists
- JsonSerializer.Serialize(oldKeyValuePairStringStringValue) ==
- JsonSerializer.Serialize(newKeyValuePairStringStringValue) ||
- // default options
- JsonSerializer.Serialize(newKeyValuePairStringStringValue) ==
- JsonSerializer.Serialize(new OpenTelemetrySettings()) )
+ newKeyValuePairStringStringValue == null ||
+ // compare lists
+ JsonSerializer.Serialize(oldKeyValuePairStringStringValue) ==
+ JsonSerializer.Serialize(newKeyValuePairStringStringValue) ||
+ // default options
+ JsonSerializer.Serialize(newKeyValuePairStringStringValue) ==
+ JsonSerializer.Serialize(new OpenTelemetrySettings()) )
{
return;
}
- sourceIndexItem?.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newKeyValuePairStringStringValue, null);
+ sourceIndexItem?.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem,
+ newKeyValuePairStringStringValue, null);
+ differenceList.Add(propertyName.ToLowerInvariant());
+ }
+
+ private static void CompareAppSettingsDefaultEditorApplication(string propertyName,
+ AppSettings? sourceIndexItem,
+ List? oldKeyValuePairStringStringValue,
+ List? newKeyValuePairStringStringValue,
+ List differenceList)
+ {
+ if ( oldKeyValuePairStringStringValue == null ||
+ newKeyValuePairStringStringValue == null ||
+ newKeyValuePairStringStringValue.Count == 0 ||
+ AreListsEqualHelper.AreListsEqual(oldKeyValuePairStringStringValue,
+ newKeyValuePairStringStringValue) )
+ {
+ return;
+ }
+
+ sourceIndexItem?.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem,
+ newKeyValuePairStringStringValue, null);
differenceList.Add(propertyName.ToLowerInvariant());
}
@@ -75,34 +127,53 @@ private static void CompareMultipleSingleItems(PropertyInfo propertyB,
AppSettings sourceIndexItem, object updateObject,
List differenceList)
{
- if ( propertyInfoFromA.PropertyType == typeof(bool?) && propertyB.PropertyType == typeof(bool?) )
+ if ( propertyInfoFromA.PropertyType == typeof(bool?) &&
+ propertyB.PropertyType == typeof(bool?) )
{
var oldBoolValue = ( bool? )propertyInfoFromA.GetValue(sourceIndexItem, null);
var newBoolValue = ( bool? )propertyB.GetValue(updateObject, null);
- CompareBool(propertyB.Name, sourceIndexItem, oldBoolValue, newBoolValue, differenceList);
+ CompareBool(propertyB.Name, sourceIndexItem, oldBoolValue, newBoolValue,
+ differenceList);
}
if ( propertyB.PropertyType == typeof(string) )
{
var oldStringValue = ( string? )propertyInfoFromA.GetValue(sourceIndexItem, null);
var newStringValue = ( string? )propertyB.GetValue(updateObject, null);
- CompareString(propertyB.Name, sourceIndexItem, oldStringValue!, newStringValue!, differenceList);
+ CompareString(propertyB.Name, sourceIndexItem, oldStringValue!, newStringValue!,
+ differenceList);
}
if ( propertyB.PropertyType == typeof(int) )
{
var oldIntValue = ( int )propertyInfoFromA.GetValue(sourceIndexItem, null)!;
var newIntValue = ( int )propertyB.GetValue(updateObject, null)!;
- CompareInt(propertyB.Name, sourceIndexItem, oldIntValue, newIntValue, differenceList);
+ CompareInt(propertyB.Name, sourceIndexItem, oldIntValue, newIntValue,
+ differenceList);
}
if ( propertyB.PropertyType == typeof(AppSettings.DatabaseTypeList) )
{
- var oldListStringValue = ( AppSettings.DatabaseTypeList? )propertyInfoFromA.GetValue(sourceIndexItem, null);
- var newListStringValue = ( AppSettings.DatabaseTypeList? )propertyB.GetValue(updateObject, null);
+ var oldListStringValue =
+ ( AppSettings.DatabaseTypeList? )propertyInfoFromA.GetValue(sourceIndexItem,
+ null);
+ var newListStringValue =
+ ( AppSettings.DatabaseTypeList? )propertyB.GetValue(updateObject, null);
CompareDatabaseTypeList(propertyB.Name, sourceIndexItem, oldListStringValue,
newListStringValue, differenceList);
}
+
+ if ( propertyB.PropertyType == typeof(CollectionsOpenType.RawJpegMode) )
+ {
+ var oldRawJpegModeEnumItem =
+ ( CollectionsOpenType.RawJpegMode? )propertyInfoFromA.GetValue(sourceIndexItem,
+ null);
+ var newRawJpegModeEnumItem =
+ ( CollectionsOpenType.RawJpegMode? )propertyB.GetValue(updateObject, null);
+ CompareCollectionsOpenTypeRawJpegMode(propertyB.Name, sourceIndexItem,
+ oldRawJpegModeEnumItem,
+ newRawJpegModeEnumItem, differenceList);
+ }
}
private static void CompareMultipleListDictionary(PropertyInfo propertyB,
@@ -111,7 +182,8 @@ private static void CompareMultipleListDictionary(PropertyInfo propertyB,
{
if ( propertyB.PropertyType == typeof(List) )
{
- var oldListStringValue = ( List? )propertyInfoFromA.GetValue(sourceIndexItem, null);
+ var oldListStringValue =
+ ( List? )propertyInfoFromA.GetValue(sourceIndexItem, null);
var newListStringValue = ( List? )propertyB.GetValue(updateObject, null);
CompareListString(propertyB.Name, sourceIndexItem, oldListStringValue,
newListStringValue, differenceList);
@@ -119,19 +191,26 @@ private static void CompareMultipleListDictionary(PropertyInfo propertyB,
if ( propertyB.PropertyType == typeof(List) )
{
- var oldKeyValuePairStringStringValue = ( List? )propertyInfoFromA.GetValue(sourceIndexItem, null);
- var newKeyValuePairStringStringValue = ( List? )propertyB.GetValue(updateObject, null);
- CompareKeyValuePairStringString(propertyB.Name, sourceIndexItem, oldKeyValuePairStringStringValue!,
+ var oldKeyValuePairStringStringValue =
+ ( List? )propertyInfoFromA.GetValue(sourceIndexItem, null);
+ var newKeyValuePairStringStringValue =
+ ( List? )propertyB.GetValue(updateObject, null);
+ CompareKeyValuePairStringString(propertyB.Name, sourceIndexItem,
+ oldKeyValuePairStringStringValue!,
newKeyValuePairStringStringValue!, differenceList);
}
- if ( propertyB.PropertyType == typeof(Dictionary>) )
+ if ( propertyB.PropertyType ==
+ typeof(Dictionary>) )
{
- var oldListPublishProfilesValue = ( Dictionary>? )
+ var oldListPublishProfilesValue =
+ ( Dictionary>? )
propertyInfoFromA.GetValue(sourceIndexItem, null);
- var newListPublishProfilesValue = ( Dictionary>? )
+ var newListPublishProfilesValue =
+ ( Dictionary>? )
propertyB.GetValue(updateObject, null);
- CompareListPublishProfiles(propertyB.Name, sourceIndexItem, oldListPublishProfilesValue,
+ CompareListPublishProfiles(propertyB.Name, sourceIndexItem,
+ oldListPublishProfilesValue,
newListPublishProfilesValue, differenceList);
}
@@ -146,40 +225,45 @@ private static void CompareMultipleListDictionary(PropertyInfo propertyB,
}
}
- private static void CompareStringDictionary(string propertyName, AppSettings sourceIndexItem,
+ private static void CompareStringDictionary(string propertyName,
+ AppSettings sourceIndexItem,
Dictionary? oldDictionaryValue,
Dictionary? newDictionaryValue, List differenceList)
{
if ( oldDictionaryValue == null || newDictionaryValue?.Count == 0 ) return;
if ( JsonSerializer.Serialize(oldDictionaryValue,
- DefaultJsonSerializer.CamelCase) == JsonSerializer.Serialize(newDictionaryValue,
- DefaultJsonSerializer.CamelCase) )
+ DefaultJsonSerializer.CamelCase) == JsonSerializer.Serialize(newDictionaryValue,
+ DefaultJsonSerializer.CamelCase) )
{
return;
}
- sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newDictionaryValue, null);
+ sourceIndexItem.GetType().GetProperty(propertyName)
+ ?.SetValue(sourceIndexItem, newDictionaryValue, null);
differenceList.Add(propertyName.ToLowerInvariant());
}
- private static void CompareKeyValuePairStringString(string propertyName, AppSettings sourceIndexItem,
+ private static void CompareKeyValuePairStringString(string propertyName,
+ AppSettings sourceIndexItem,
List? oldKeyValuePairStringStringValue,
- List? newKeyValuePairStringStringValue, List differenceList)
+ List? newKeyValuePairStringStringValue,
+ List differenceList)
{
if ( oldKeyValuePairStringStringValue == null ||
- newKeyValuePairStringStringValue == null ||
- newKeyValuePairStringStringValue.Count == 0 )
+ newKeyValuePairStringStringValue == null ||
+ newKeyValuePairStringStringValue.Count == 0 )
{
return;
}
if ( oldKeyValuePairStringStringValue.Equals(
- newKeyValuePairStringStringValue) )
+ newKeyValuePairStringStringValue) )
{
return;
}
- sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newKeyValuePairStringStringValue, null);
+ sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem,
+ newKeyValuePairStringStringValue, null);
differenceList.Add(propertyName.ToLowerInvariant());
}
@@ -191,12 +275,35 @@ private static void CompareKeyValuePairStringString(string propertyName, AppSett
/// oldDatabaseTypeList to compare with newDatabaseTypeList
/// newDatabaseTypeList to compare with oldDatabaseTypeList
/// list of different values
- internal static void CompareDatabaseTypeList(string propertyName, AppSettings sourceIndexItem,
+ internal static void CompareDatabaseTypeList(string propertyName,
+ AppSettings sourceIndexItem,
AppSettings.DatabaseTypeList? oldDatabaseTypeList,
AppSettings.DatabaseTypeList? newDatabaseTypeList, List differenceList)
{
- if ( oldDatabaseTypeList == newDatabaseTypeList || newDatabaseTypeList == new AppSettings().DatabaseType ) return;
- sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newDatabaseTypeList, null);
+ if ( oldDatabaseTypeList == newDatabaseTypeList ||
+ newDatabaseTypeList == new AppSettings().DatabaseType ) return;
+ sourceIndexItem.GetType().GetProperty(propertyName)
+ ?.SetValue(sourceIndexItem, newDatabaseTypeList, null);
+ differenceList.Add(propertyName.ToLowerInvariant());
+ }
+
+ ///
+ /// Compare DatabaseTypeList type
+ ///
+ /// name of property e.g. DatabaseTypeList
+ /// source object
+ /// oldDatabaseTypeList to compare with newDatabaseTypeList
+ /// newDatabaseTypeList to compare with oldDatabaseTypeList
+ /// list of different values
+ private static void CompareCollectionsOpenTypeRawJpegMode(string propertyName,
+ AppSettings sourceIndexItem,
+ CollectionsOpenType.RawJpegMode? oldDatabaseTypeList,
+ CollectionsOpenType.RawJpegMode? newDatabaseTypeList, List differenceList)
+ {
+ if ( oldDatabaseTypeList == newDatabaseTypeList ||
+ newDatabaseTypeList == CollectionsOpenType.RawJpegMode.Default ) return;
+ sourceIndexItem.GetType().GetProperty(propertyName)
+ ?.SetValue(sourceIndexItem, newDatabaseTypeList, null);
differenceList.Add(propertyName.ToLowerInvariant());
}
@@ -210,17 +317,19 @@ internal static void CompareDatabaseTypeList(string propertyName, AppSettings so
/// newListStringValue to compare with oldListStringValue
/// list of different values
internal static void CompareListString(string propertyName, AppSettings sourceIndexItem,
- List? oldListStringValue, List? newListStringValue, List differenceList)
+ List? oldListStringValue, List? newListStringValue,
+ List differenceList)
{
if ( oldListStringValue == null || newListStringValue?.Count == 0 ) return;
if ( JsonSerializer.Serialize(oldListStringValue,
- DefaultJsonSerializer.CamelCase) == JsonSerializer.Serialize(newListStringValue,
- DefaultJsonSerializer.CamelCase) )
+ DefaultJsonSerializer.CamelCase) == JsonSerializer.Serialize(newListStringValue,
+ DefaultJsonSerializer.CamelCase) )
{
return;
}
- sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newListStringValue, null);
+ sourceIndexItem.GetType().GetProperty(propertyName)
+ ?.SetValue(sourceIndexItem, newListStringValue, null);
differenceList.Add(propertyName.ToLowerInvariant());
}
@@ -232,14 +341,17 @@ internal static void CompareListString(string propertyName, AppSettings sourceIn
/// oldListPublishValue to compare with newListPublishValue
/// newListPublishValue to compare with oldListPublishValue
/// list of different values
- internal static void CompareListPublishProfiles(string propertyName, AppSettings sourceIndexItem,
+ internal static void CompareListPublishProfiles(string propertyName,
+ AppSettings sourceIndexItem,
Dictionary>? oldListPublishValue,
- Dictionary>? newListPublishValue, List differenceList)
+ Dictionary>? newListPublishValue,
+ List differenceList)
{
if ( oldListPublishValue == null || newListPublishValue?.Count == 0 ) return;
if ( oldListPublishValue.Equals(newListPublishValue) ) return;
- sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newListPublishValue, null);
+ sourceIndexItem.GetType().GetProperty(propertyName)
+ ?.SetValue(sourceIndexItem, newListPublishValue, null);
differenceList.Add(propertyName.ToLowerInvariant());
}
@@ -252,18 +364,22 @@ internal static void CompareListPublishProfiles(string propertyName, AppSettings
/// oldBoolValue to compare with newBoolValue
/// oldBoolValue to compare with newBoolValue
/// list of different values
- internal static void CompareBool(string propertyName, AppSettings sourceIndexItem, bool? oldBoolValue,
+ internal static void CompareBool(string propertyName, AppSettings sourceIndexItem,
+ bool? oldBoolValue,
bool? newBoolValue, List differenceList)
{
if ( newBoolValue == null )
{
return;
}
+
if ( oldBoolValue == newBoolValue )
{
return;
}
- sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newBoolValue, null);
+
+ sourceIndexItem.GetType().GetProperty(propertyName)
+ ?.SetValue(sourceIndexItem, newBoolValue, null);
differenceList.Add(propertyName.ToLowerInvariant());
}
@@ -288,7 +404,7 @@ internal static void CompareString(string propertyName, AppSettings sourceIndexI
}
if ( oldStringValue == newStringValue ||
- ( string.IsNullOrEmpty(newStringValue) ) )
+ ( string.IsNullOrEmpty(newStringValue) ) )
{
return;
}
@@ -338,6 +454,5 @@ internal static void CompareInt(string propertyName, AppSettings sourceIndexItem
return Array.Find(car.GetType().GetProperties(), pi => pi.Name == propertyName)?
.GetValue(car, null);
}
-
}
}
diff --git a/starsky/starsky.foundation.platform/Helpers/Compare/AreListsEqual.cs b/starsky/starsky.foundation.platform/Helpers/Compare/AreListsEqual.cs
new file mode 100644
index 0000000000..601a59bb0b
--- /dev/null
+++ b/starsky/starsky.foundation.platform/Helpers/Compare/AreListsEqual.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+
+namespace starsky.foundation.platform.Helpers.Compare;
+
+public static class AreListsEqualHelper
+{
+ ///
+ /// Compare two lists
+ ///
+ /// First list
+ /// Second list
+ /// type of both lists
+ /// true if same, false if not the same
+ internal static bool AreListsEqual(List list1, List list2)
+ {
+ ArgumentNullException.ThrowIfNull(list1);
+ ArgumentNullException.ThrowIfNull(list2);
+
+ if ( list1.Count != list2.Count )
+ {
+ return false;
+ }
+
+ for ( var i = 0; i < list1.Count; i++ )
+ {
+ if ( list1[i]?.Equals(list2[i]) == false )
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/starsky/starsky.foundation.platform/Helpers/ExtensionRolesHelper.cs b/starsky/starsky.foundation.platform/Helpers/ExtensionRolesHelper.cs
index 8c23c89da1..e65a253f58 100644
--- a/starsky/starsky.foundation.platform/Helpers/ExtensionRolesHelper.cs
+++ b/starsky/starsky.foundation.platform/Helpers/ExtensionRolesHelper.cs
@@ -372,7 +372,10 @@ public enum ImageFormat
mp4 = 50,
// archives
- zip = 60
+ zip = 60,
+
+ // folder
+ directory = 1000
}
diff --git a/starsky/starsky.foundation.platform/Helpers/ReadAppSettings.cs b/starsky/starsky.foundation.platform/Helpers/ReadAppSettings.cs
index 664f357657..a386464bdc 100644
--- a/starsky/starsky.foundation.platform/Helpers/ReadAppSettings.cs
+++ b/starsky/starsky.foundation.platform/Helpers/ReadAppSettings.cs
@@ -8,7 +8,7 @@ namespace starsky.foundation.platform.Helpers;
public static class ReadAppSettings
{
- internal static async Task Read(string path)
+ public static async Task Read(string path)
{
if ( !File.Exists(path) )
{
diff --git a/starsky/starsky.foundation.platform/Helpers/SetupAppSettings.cs b/starsky/starsky.foundation.platform/Helpers/SetupAppSettings.cs
index 26db9cce03..9b709af273 100644
--- a/starsky/starsky.foundation.platform/Helpers/SetupAppSettings.cs
+++ b/starsky/starsky.foundation.platform/Helpers/SetupAppSettings.cs
@@ -11,11 +11,13 @@
using starsky.foundation.platform.Models;
[assembly: InternalsVisibleTo("starskytest")]
+
namespace starsky.foundation.platform.Helpers
{
public static class SetupAppSettings
{
- public static async Task FirstStepToAddSingleton(ServiceCollection services)
+ public static async Task FirstStepToAddSingleton(
+ ServiceCollection services)
{
services.AddSingleton(new ConfigurationBuilder().Build());
var configurationRoot = await AppSettingsToBuilder();
@@ -35,7 +37,8 @@ public static async Task AppSettingsToBuilder(string[]? args
var settings = await MergeJsonFiles(appSettings.BaseDirectoryProject);
// Make sure is wrapped in a AppContainer app
- var utf8Bytes = JsonSerializer.SerializeToUtf8Bytes(new AppContainerAppSettings { App = settings });
+ var appContainer = new AppContainerAppSettings { App = settings };
+ var utf8Bytes = JsonSerializer.SerializeToUtf8Bytes(appContainer);
builder
.AddJsonStream(new MemoryStream(utf8Bytes))
@@ -117,6 +120,5 @@ public static AppSettings ConfigurePoCoAppSettings(IServiceCollection services,
return serviceProvider.GetRequiredService();
}
-
}
}
diff --git a/starsky/starsky.foundation.platform/Helpers/StringHelper.cs b/starsky/starsky.foundation.platform/Helpers/StringHelper.cs
index eeec46af9c..dadc18abdc 100644
--- a/starsky/starsky.foundation.platform/Helpers/StringHelper.cs
+++ b/starsky/starsky.foundation.platform/Helpers/StringHelper.cs
@@ -4,10 +4,11 @@ public static class StringHelper
{
public static string AsciiNullReplacer(string newStringValue)
{
- return ( newStringValue == "\\0" || newStringValue == "\\\\0" ) ? string.Empty : newStringValue;
+ return ( newStringValue == "\\0" || newStringValue == "\\\\0" )
+ ? string.Empty
+ : newStringValue;
}
- public static readonly string AsciiNullChar = "\\\\0";
-
+ public const string AsciiNullChar = @"\\0";
}
}
diff --git a/starsky/starsky.foundation.platform/JsonConverter/EnumListConverter.cs b/starsky/starsky.foundation.platform/JsonConverter/EnumListConverter.cs
new file mode 100644
index 0000000000..57632d1a7e
--- /dev/null
+++ b/starsky/starsky.foundation.platform/JsonConverter/EnumListConverter.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace starsky.foundation.platform.JsonConverter;
+
+///
+/// Enum converter for Lists with Enum into Json
+///
+/// Enum
+public class EnumListConverter : JsonConverter> where T : struct, Enum
+{
+ public override List Read(ref Utf8JsonReader reader, Type typeToConvert,
+ JsonSerializerOptions options)
+ {
+ if ( reader.TokenType != JsonTokenType.StartArray )
+ throw new JsonException();
+
+ var result = new List();
+
+ while ( reader.Read() )
+ {
+ if ( reader.TokenType == JsonTokenType.EndArray )
+ return result;
+
+ if ( reader.TokenType != JsonTokenType.String )
+ throw new JsonException();
+
+ if ( Enum.TryParse(reader.GetString(), out var enumValue) )
+ {
+ result.Add(enumValue);
+ }
+ else
+ {
+ throw new JsonException($"Unknown enum value: {reader.GetString()}");
+ }
+ }
+
+ throw new JsonException("Unexpected end of JSON input");
+ }
+
+ public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options)
+ {
+ writer.WriteStartArray();
+
+ foreach ( var item in value )
+ {
+ writer.WriteStringValue(item.ToString());
+ }
+
+ writer.WriteEndArray();
+ }
+}
diff --git a/starsky/starsky.foundation.platform/Middleware/ContentSecurityPolicyMiddleware.cs b/starsky/starsky.foundation.platform/Middleware/ContentSecurityPolicyMiddleware.cs
index eb8ddfa2ce..552e552fc3 100644
--- a/starsky/starsky.foundation.platform/Middleware/ContentSecurityPolicyMiddleware.cs
+++ b/starsky/starsky.foundation.platform/Middleware/ContentSecurityPolicyMiddleware.cs
@@ -52,7 +52,7 @@ public async Task Invoke(HttpContext httpContext)
// Currently not supported in Firefox and Safari (Edge user agent also includes the word Chrome)
if ( httpContext.Request.Headers.UserAgent.Contains("Chrome") ||
- httpContext.Request.Headers.UserAgent.Contains("csp-evaluator") )
+ httpContext.Request.Headers.UserAgent.Contains("csp-evaluator") )
{
cspHeader += "require-trusted-types-for 'script'; ";
}
@@ -64,16 +64,16 @@ public async Task Invoke(HttpContext httpContext)
// @see: https://www.permissionspolicy.com/
if ( string.IsNullOrEmpty(
- httpContext.Response.Headers["Permissions-Policy"]) )
+ httpContext.Response.Headers["Permissions-Policy"]) )
{
httpContext.Response.Headers
.Append("Permissions-Policy", "autoplay=(self), " +
- "fullscreen=(self), " +
- "geolocation=(self), " +
- "picture-in-picture=(self), " +
- "clipboard-read=(self), " +
- "clipboard-write=(self), " +
- "window-placement=(self)");
+ "fullscreen=(self), " +
+ "geolocation=(self), " +
+ "picture-in-picture=(self), " +
+ "clipboard-read=(self), " +
+ "clipboard-write=(self), " +
+ "window-placement=(self)");
}
if ( string.IsNullOrEmpty(httpContext.Response.Headers["Referrer-Policy"]) )
diff --git a/starsky/starsky.foundation.platform/Models/AppSettings.cs b/starsky/starsky.foundation.platform/Models/AppSettings.cs
index 83c1730b31..0a246d5f8f 100644
--- a/starsky/starsky.foundation.platform/Models/AppSettings.cs
+++ b/starsky/starsky.foundation.platform/Models/AppSettings.cs
@@ -8,13 +8,14 @@
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using starsky.foundation.platform.Attributes;
+using starsky.foundation.platform.Enums;
using starsky.foundation.platform.Helpers;
using starsky.foundation.platform.JsonConverter;
using TimeZoneConverter;
namespace starsky.foundation.platform.Models
{
- [SuppressMessage("ReSharper", "CA1822")]
+ [SuppressMessage("Performance", "CA1822:Mark members as static")]
public sealed class AppSettings
{
public AppSettings()
@@ -381,7 +382,7 @@ public static void StructureCheck(string? structure)
}
throw new ArgumentException("(StructureCheck) Structure is not confirm regex - " +
- structure);
+ structure);
}
///
@@ -587,8 +588,8 @@ public string WebFtp
if ( string.IsNullOrEmpty(value) ) return;
Uri uriAddress = new Uri(value);
if ( uriAddress.UserInfo.Split(":".ToCharArray()).Length == 2
- && uriAddress.Scheme == "ftp"
- && uriAddress.LocalPath.Length >= 1 )
+ && uriAddress.Scheme == "ftp"
+ && uriAddress.LocalPath.Length >= 1 )
{
_webFtp = value;
}
@@ -650,8 +651,7 @@ public Dictionary>? PublishProfiles
/// Value for AccountRolesDefaultByEmailRegisterOverwrite
///
private Dictionary
- AccountRolesByEmailRegisterOverwritePrivate
- { get; set; } =
+ AccountRolesByEmailRegisterOverwritePrivate { get; set; } =
new Dictionary();
///
@@ -669,7 +669,7 @@ public Dictionary? AccountRolesByEmailRegisterOverwrite
{
if ( value == null ) return;
foreach ( var singleValue in value.Where(singleValue =>
- AccountRoles.GetAllRoles().Contains(singleValue.Value)) )
+ AccountRoles.GetAllRoles().Contains(singleValue.Value)) )
{
AccountRolesByEmailRegisterOverwritePrivate.TryAdd(
singleValue.Key, singleValue.Value);
@@ -731,6 +731,9 @@ public Dictionary? AccountRolesByEmailRegisterOverwrite
"/lost+found", "/.stfolder", "/.git"
};
+ ///
+ /// Auto Sync on Startup
+ ///
public bool? SyncOnStartup { get; set; } = true;
///
@@ -747,6 +750,7 @@ public Dictionary? AccountRolesByEmailRegisterOverwrite
/// But it seems a lot of cameras don't do this
/// We assume that the standard is followed, and for Camera brands that don't follow the specs use this setting.
///
+ [PackageTelemetry]
public List? VideoUseLocalTime { get; set; } = new List
{
new CameraMakeModel("Sony", "A58")
@@ -757,14 +761,31 @@ public Dictionary? AccountRolesByEmailRegisterOverwrite
///
private bool? EnablePackageTelemetryPrivate { get; set; }
-
///
/// Disable logout buttons in UI
/// And hides server specific features that are strange on a local desktop
+ /// Enable Desktop based features
+ ///
+ [PackageTelemetry]
+ public bool? UseLocalDesktop { get; set; } = false;
+
+ ///
+ /// Editor by imageFormat
///
[PackageTelemetry]
- public bool? UseLocalDesktopUi { get; set; } = false;
+ public List DefaultDesktopEditor { get; set; } = [];
+ ///
+ /// When open with desktop app, open the raw or jpeg (Default: NotSet / Jpeg)
+ ///
+ [PackageTelemetry]
+ public CollectionsOpenType.RawJpegMode DesktopCollectionsOpen { get; set; } =
+ CollectionsOpenType.RawJpegMode.Default;
+
+ ///
+ /// Number of files to open before confirmation
+ ///
+ public int? DesktopEditorAmountBeforeConfirmation { get; set; }
///
/// Helps us improve the software
@@ -884,7 +905,7 @@ public AppSettings CloneToDisplay()
}
if ( appSettings.DatabaseType == DatabaseTypeList.Sqlite &&
- !string.IsNullOrEmpty(userProfileFolder) )
+ !string.IsNullOrEmpty(userProfileFolder) )
{
appSettings.DatabaseConnection =
appSettings.DatabaseConnection.Replace(userProfileFolder, "~");
@@ -896,7 +917,7 @@ public AppSettings CloneToDisplay()
}
if ( !string.IsNullOrEmpty(appSettings.AppSettingsPath) &&
- !string.IsNullOrEmpty(userProfileFolder) )
+ !string.IsNullOrEmpty(userProfileFolder) )
{
appSettings.AppSettingsPath =
appSettings.AppSettingsPath.Replace(userProfileFolder, "~");
@@ -905,7 +926,7 @@ public AppSettings CloneToDisplay()
if ( appSettings.PublishProfiles != null )
{
foreach ( var value in appSettings.PublishProfiles.SelectMany(profile =>
- profile.Value) )
+ profile.Value) )
{
ReplaceAppSettingsPublishProfilesCloneToDisplay(value);
}
@@ -952,7 +973,7 @@ private static void ReplaceAppSettingsPublishProfilesCloneToDisplay(
AppSettingsPublishProfiles value)
{
if ( !string.IsNullOrEmpty(value.Path) &&
- value.Path != AppSettingsPublishProfiles.GetDefaultPath() )
+ value.Path != AppSettingsPublishProfiles.GetDefaultPath() )
{
value.Path = CloneToDisplaySecurityWarning;
}
@@ -1071,7 +1092,7 @@ internal static string ReplaceEnvironmentVariable(string input)
public string SqLiteFullPath(string connectionString, string baseDirectoryProject)
{
if ( DatabaseType == DatabaseTypeList.Mysql &&
- string.IsNullOrWhiteSpace(connectionString) )
+ string.IsNullOrWhiteSpace(connectionString) )
throw new ArgumentException("The 'DatabaseConnection' field is null or empty");
if ( DatabaseType != DatabaseTypeList.Sqlite )
@@ -1092,7 +1113,7 @@ public string SqLiteFullPath(string connectionString, string baseDirectoryProjec
if ( baseDirectoryProject.Contains("entityframeworkcore") ) return connectionString;
var dataSource = "Data Source=" + baseDirectoryProject +
- Path.DirectorySeparatorChar + databaseFileName;
+ Path.DirectorySeparatorChar + databaseFileName;
return dataSource;
}
@@ -1121,7 +1142,7 @@ internal static void CopyProperties(object source, object destination)
var destinationProperty = destinationType.GetProperty(sourceProperty.Name);
if ( destinationProperty == null ||
- !destinationProperty.CanWrite )
+ !destinationProperty.CanWrite )
{
continue;
}
diff --git a/starsky/starsky.foundation.platform/Models/AppSettingsDefaultEditorApplication.cs b/starsky/starsky.foundation.platform/Models/AppSettingsDefaultEditorApplication.cs
new file mode 100644
index 0000000000..6ca0df653f
--- /dev/null
+++ b/starsky/starsky.foundation.platform/Models/AppSettingsDefaultEditorApplication.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+using starsky.foundation.platform.Helpers;
+using starsky.foundation.platform.JsonConverter;
+
+namespace starsky.foundation.platform.Models;
+
+public class AppSettingsDefaultEditorApplication
+{
+ ///
+ /// For what type of files
+ ///
+ [JsonConverter(typeof(EnumListConverter))]
+ public List ImageFormats { get; set; } = [];
+
+ ///
+ /// Path to .exe on windows and .app on Mac OS
+ /// No check if exists here
+ ///
+ public string ApplicationPath { get; set; } = string.Empty;
+}
diff --git a/starsky/starsky.foundation.platform/Models/AppSettingsTransferObject.cs b/starsky/starsky.foundation.platform/Models/AppSettingsTransferObject.cs
index 483cc02ddb..cc2fcc061d 100644
--- a/starsky/starsky.foundation.platform/Models/AppSettingsTransferObject.cs
+++ b/starsky/starsky.foundation.platform/Models/AppSettingsTransferObject.cs
@@ -1,3 +1,6 @@
+using System.Collections.Generic;
+using starsky.foundation.platform.Enums;
+
namespace starsky.foundation.platform.Models
{
///
@@ -10,6 +13,12 @@ public sealed class AppSettingsTransferObject
public string? StorageFolder { get; set; }
public bool? UseSystemTrash { get; set; }
- public bool? UseLocalDesktopUi { get; set; }
+ public bool? UseLocalDesktop { get; set; }
+
+ public List DefaultDesktopEditor { get; set; } = [];
+
+ public CollectionsOpenType.RawJpegMode DesktopCollectionsOpen { get; set; } =
+ CollectionsOpenType.RawJpegMode.Default;
+
}
}
diff --git a/starsky/starsky.foundation.realtime/Middleware/DisabledWebSocketsMiddleware.cs b/starsky/starsky.foundation.realtime/Middleware/DisabledWebSocketsMiddleware.cs
index fbaf1df3bf..5ed56c6fc6 100644
--- a/starsky/starsky.foundation.realtime/Middleware/DisabledWebSocketsMiddleware.cs
+++ b/starsky/starsky.foundation.realtime/Middleware/DisabledWebSocketsMiddleware.cs
@@ -11,6 +11,8 @@ namespace starsky.foundation.realtime.Middleware
[SuppressMessage("Performance", "IDE0060:UnusedParameter.Local")]
public sealed class DisabledWebSocketsMiddleware
{
+ [SuppressMessage("ReSharper", "UnusedParameter.Local")]
+ [SuppressMessage("Usage", "IDE0060:Remove unused parameter")]
public DisabledWebSocketsMiddleware(RequestDelegate next)
{
}
@@ -25,6 +27,7 @@ await webSocket.CloseOutputAsync(WebSocketCloseStatus.MessageTooBig,
"Feature toggle disabled", CancellationToken.None);
return;
}
+
context.Response.StatusCode = StatusCodes.Status400BadRequest;
}
}
diff --git a/starsky/starsky.foundation.storage/Interfaces/ISelectorStorage.cs b/starsky/starsky.foundation.storage/Interfaces/ISelectorStorage.cs
index 1bdcad628f..1ddbeb92fd 100644
--- a/starsky/starsky.foundation.storage/Interfaces/ISelectorStorage.cs
+++ b/starsky/starsky.foundation.storage/Interfaces/ISelectorStorage.cs
@@ -2,6 +2,9 @@
namespace starsky.foundation.storage.Interfaces
{
+ ///
+ /// ISelectionStorage
+ ///
public interface ISelectorStorage
{
IStorage Get(SelectorStorage.StorageServices storageServices);
diff --git a/starsky/starsky.foundation.storage/Services/StructureService.cs b/starsky/starsky.foundation.storage/Services/StructureService.cs
index 60d71aa6cc..ee0bd5d658 100644
--- a/starsky/starsky.foundation.storage/Services/StructureService.cs
+++ b/starsky/starsky.foundation.storage/Services/StructureService.cs
@@ -36,8 +36,10 @@ public string ParseFileName(DateTime dateTime,
CheckStructureFormat();
var fileName = FilenamesHelper.GetFileName(_structure);
var fileNameStructure = PathHelper.PrefixDbSlash(fileName);
- var parsedStructuredList = ParseStructure(fileNameStructure, dateTime, fileNameBase, extensionWithoutDot);
- return PathHelper.RemovePrefixDbSlash(ApplyStructureRangeToStorage(parsedStructuredList));
+ var parsedStructuredList = ParseStructure(fileNameStructure, dateTime, fileNameBase,
+ extensionWithoutDot);
+ return PathHelper.RemovePrefixDbSlash(
+ ApplyStructureRangeToStorage(parsedStructuredList));
}
///
@@ -66,7 +68,8 @@ public string ParseSubfolders(DateTime dateTime, string fileNameBase = "",
string extensionWithoutDot = "")
{
CheckStructureFormat();
- var parsedStructuredList = ParseStructure(_structure, dateTime, fileNameBase, extensionWithoutDot);
+ var parsedStructuredList =
+ ParseStructure(_structure, dateTime, fileNameBase, extensionWithoutDot);
return ApplyStructureRangeToStorage(
parsedStructuredList.GetRange(0, parsedStructuredList.Count - 1));
@@ -83,7 +86,6 @@ private string ApplyStructureRangeToStorage(List> parsedStr
var parentFolderBuilder = new StringBuilder();
foreach ( var subStructureItem in parsedStructuredList )
{
-
var currentChildFolderBuilder = new StringBuilder();
currentChildFolderBuilder.Append('/');
@@ -92,21 +94,23 @@ private string ApplyStructureRangeToStorage(List> parsedStr
currentChildFolderBuilder.Append(structureItem.Output);
}
- var parentFolderSubPath = FilenamesHelper.GetParentPath(parentFolderBuilder.ToString());
+ var parentFolderSubPath =
+ FilenamesHelper.GetParentPath(parentFolderBuilder.ToString());
var existParentFolder = _storage.ExistFolder(parentFolderSubPath);
// default situation without asterisk or child directory is NOT found
if ( !currentChildFolderBuilder.ToString().Contains('*') || !existParentFolder )
{
- var currentChildFolderRemovedAsterisk = RemoveAsteriskFromString(currentChildFolderBuilder);
+ var currentChildFolderRemovedAsterisk =
+ RemoveAsteriskFromString(currentChildFolderBuilder);
parentFolderBuilder.Append(currentChildFolderRemovedAsterisk);
continue;
}
parentFolderBuilder =
MatchChildDirectories(parentFolderBuilder, currentChildFolderBuilder);
-
}
+
return parentFolderBuilder.ToString();
}
@@ -118,10 +122,12 @@ private string ApplyStructureRangeToStorage(List> parsedStr
/// the current folder name with asterisk
/// other child folder items (item in loop of childDirectories)
/// is match
- private static bool MatchChildFolderSearch(StringBuilder parentFolderBuilder, StringBuilder currentChildFolderBuilder, string p)
+ private static bool MatchChildFolderSearch(StringBuilder parentFolderBuilder,
+ StringBuilder currentChildFolderBuilder, string p)
{
var matchDirectFolderName = RemoveAsteriskFromString(currentChildFolderBuilder);
- if ( matchDirectFolderName != "/" && p == parentFolderBuilder + matchDirectFolderName ) return true;
+ if ( matchDirectFolderName != "/" && p == parentFolderBuilder + matchDirectFolderName )
+ return true;
var matchRegex = new Regex(
parentFolderBuilder + currentChildFolderBuilder.ToString().Replace("*", ".+"),
@@ -136,14 +142,15 @@ private static bool MatchChildFolderSearch(StringBuilder parentFolderBuilder, St
/// parent folder (subPath style)
/// child folder with asterisk
/// SubPath without asterisk
- private StringBuilder MatchChildDirectories(StringBuilder parentFolderBuilder, StringBuilder currentChildFolderBuilder)
+ private StringBuilder MatchChildDirectories(StringBuilder parentFolderBuilder,
+ StringBuilder currentChildFolderBuilder)
{
// should return a list of: 2019/10/2019_10_08>
var childDirectories = _storage.GetDirectories(parentFolderBuilder.ToString()).ToList();
var matchingFoldersPath = childDirectories.Find(p =>
MatchChildFolderSearch(parentFolderBuilder, currentChildFolderBuilder, p)
- );
+ );
// When a new folder with asterisk is created
if ( matchingFoldersPath == null )
@@ -154,6 +161,7 @@ private StringBuilder MatchChildDirectories(StringBuilder parentFolderBuilder, S
{
defaultValue = "/default";
}
+
parentFolderBuilder.Append(defaultValue);
return parentFolderBuilder;
}
@@ -183,13 +191,14 @@ private static string RemoveAsteriskFromString(StringBuilder input)
private void CheckStructureFormat()
{
if ( !string.IsNullOrEmpty(_structure) &&
- _structure.StartsWith('/') && _structure.EndsWith(".ext") &&
- _structure != "/.ext" )
+ _structure.StartsWith('/') && _structure.EndsWith(".ext") &&
+ _structure != "/.ext" )
{
return;
}
- throw new FieldAccessException("Structure is not right formatted, please read the documentation");
+ throw new FieldAccessException(
+ "Structure is not right formatted, please read the documentation");
}
///
@@ -197,7 +206,8 @@ private void CheckStructureFormat()
/// @see: https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings
/// Not escaped regex: \\?(d{1,4}|f{1,6}|F{1,6}|g{1,2}|h{1,2}|H{1,2}|K|m{1,2}|M{1,4}|s{1,2}|t{1,2}|y{1,5}|z{1,3})
///
- const string DateRegexPattern = "\\\\?(d{1,4}|f{1,6}|F{1,6}|g{1,2}|h{1,2}|H{1,2}|K|m{1,2}|M{1,4}|s{1,2}|t{1,2}|y{1,5}|z{1,3})";
+ const string DateRegexPattern =
+ "\\\\?(d{1,4}|f{1,6}|F{1,6}|g{1,2}|h{1,2}|H{1,2}|K|m{1,2}|M{1,4}|s{1,2}|t{1,2}|y{1,5}|z{1,3})";
///
/// Parse the dateTime structure input to the dateTime provided
@@ -207,7 +217,8 @@ private void CheckStructureFormat()
/// source name, can be used in the options
/// fileExtension without dot
/// Object with Structure Range output
- private static List> ParseStructure(string structure, DateTime dateTime,
+ private static List> ParseStructure(string structure,
+ DateTime dateTime,
string fileNameBase = "", string extensionWithoutDot = "")
{
var structureList = structure.Split('/');
@@ -219,8 +230,8 @@ private static List> ParseStructure(string structure, DateT
var matchCollection = new
Regex(DateRegexPattern + "|{filenamebase}|\\*|.ext|.",
- RegexOptions.None, TimeSpan.FromMilliseconds(100))
- .Matches(structureItem);
+ RegexOptions.None, TimeSpan.FromMilliseconds(200))
+ .Matches(structureItem);
var matchList = new List();
foreach ( Match match in matchCollection )
@@ -230,7 +241,8 @@ private static List> ParseStructure(string structure, DateT
Pattern = match.Value,
Start = match.Index,
End = match.Index + match.Length,
- Output = OutputStructureRangeItemParser(match.Value, dateTime, fileNameBase, extensionWithoutDot)
+ Output = OutputStructureRangeItemParser(match.Value, dateTime,
+ fileNameBase, extensionWithoutDot)
});
}
@@ -252,13 +264,14 @@ private static string OutputStructureRangeItemParser(string pattern, DateTime da
string fileNameBase, string extensionWithoutDot = "")
{
// allow only full word matches (so .ext is no match)
- MatchCollection matchCollection = new Regex(DateRegexPattern,
+ var matchCollection = new Regex(DateRegexPattern,
RegexOptions.None, TimeSpan.FromMilliseconds(100)).Matches(pattern);
foreach ( Match match in matchCollection )
{
// Ignore escaped items
- if ( !match.Value.StartsWith('\\') && match.Index == 0 && match.Length == pattern.Length )
+ if ( !match.Value.StartsWith('\\') && match.Index == 0 &&
+ match.Length == pattern.Length )
{
return dateTime.ToString(pattern, CultureInfo.InvariantCulture);
}
@@ -270,7 +283,9 @@ private static string OutputStructureRangeItemParser(string pattern, DateTime da
case "{filenamebase}":
return fileNameBase;
case ".ext":
- return string.IsNullOrEmpty(extensionWithoutDot) ? ".unknown" : $".{extensionWithoutDot}";
+ return string.IsNullOrEmpty(extensionWithoutDot)
+ ? ".unknown"
+ : $".{extensionWithoutDot}";
default:
return pattern.Replace("\\", string.Empty);
}
diff --git a/starsky/starsky.foundation.sync/SyncServices/SyncFolder.cs b/starsky/starsky.foundation.sync/SyncServices/SyncFolder.cs
index 98339af010..f44ea093c3 100644
--- a/starsky/starsky.foundation.sync/SyncServices/SyncFolder.cs
+++ b/starsky/starsky.foundation.sync/SyncServices/SyncFolder.cs
@@ -145,6 +145,7 @@ internal async Task CompareFolderListAndFixMissingFolders(List subPaths,
await _query.AddItemAsync(new FileIndexItem(path)
{
IsDirectory = true,
+ ImageFormat = ExtensionRolesHelper.ImageFormat.directory,
AddToDatabase = DateTime.UtcNow,
ColorClass = ColorClassParser.Color.None
});
diff --git a/starsky/starsky.foundation.sync/WatcherHelpers/SyncWatcherConnector.cs b/starsky/starsky.foundation.sync/WatcherHelpers/SyncWatcherConnector.cs
index 39d619fcff..97039e6441 100644
--- a/starsky/starsky.foundation.sync/WatcherHelpers/SyncWatcherConnector.cs
+++ b/starsky/starsky.foundation.sync/WatcherHelpers/SyncWatcherConnector.cs
@@ -13,6 +13,7 @@
using starsky.foundation.database.Models;
using starsky.foundation.database.Query;
using starsky.foundation.platform.Enums;
+using starsky.foundation.platform.Helpers;
using starsky.foundation.platform.Interfaces;
using starsky.foundation.platform.JsonConverter;
using starsky.foundation.platform.Models;
@@ -114,6 +115,7 @@ private async Task> SyncTaskInternal(
syncData.Add(new FileIndexItem(_appSettings.FullPathToDatabaseStyle(fullFilePath))
{
IsDirectory = true,
+ ImageFormat = ExtensionRolesHelper.ImageFormat.directory,
Status = FileIndexItem.ExifStatus.NotFoundSourceMissing
});
diff --git a/starsky/starsky.foundation.platform/Helpers/EnumHelper.cs b/starsky/starsky.foundation.writemeta/Helpers/EnumHelper.cs
similarity index 92%
rename from starsky/starsky.foundation.platform/Helpers/EnumHelper.cs
rename to starsky/starsky.foundation.writemeta/Helpers/EnumHelper.cs
index e329bd7fa3..b799c3daaf 100644
--- a/starsky/starsky.foundation.platform/Helpers/EnumHelper.cs
+++ b/starsky/starsky.foundation.writemeta/Helpers/EnumHelper.cs
@@ -3,7 +3,7 @@
using System.Linq;
using System.Reflection;
-namespace starsky.foundation.platform.Helpers
+namespace starsky.foundation.writemeta.Helpers
{
public static class EnumHelper
{
diff --git a/starsky/starsky.project.web/Attributes/ExcludeFromCoverageAttribute.cs b/starsky/starsky.project.web/Attributes/ExcludeFromCoverageAttribute.cs
new file mode 100644
index 0000000000..583bf9205a
--- /dev/null
+++ b/starsky/starsky.project.web/Attributes/ExcludeFromCoverageAttribute.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace starsky.project.web.Attributes
+{
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor)]
+ public sealed class ExcludeFromCoverageAttribute : Attribute
+ {
+ }
+}
diff --git a/starsky/starskycore/Helpers/MimeHelper.cs b/starsky/starsky.project.web/Helpers/MimeHelper.cs
similarity index 99%
rename from starsky/starskycore/Helpers/MimeHelper.cs
rename to starsky/starsky.project.web/Helpers/MimeHelper.cs
index 8d3c9ac17a..d1bcb9363c 100644
--- a/starsky/starskycore/Helpers/MimeHelper.cs
+++ b/starsky/starsky.project.web/Helpers/MimeHelper.cs
@@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.IO;
-namespace starskycore.Helpers
+namespace starsky.project.web.Helpers
{
public static class MimeHelper
{
diff --git a/starsky/starsky.foundation.platform/Helpers/PortProgramHelper.cs b/starsky/starsky.project.web/Helpers/PortProgramHelper.cs
similarity index 69%
rename from starsky/starsky.foundation.platform/Helpers/PortProgramHelper.cs
rename to starsky/starsky.project.web/Helpers/PortProgramHelper.cs
index 292e1e5b75..7b64e6c1d7 100644
--- a/starsky/starsky.foundation.platform/Helpers/PortProgramHelper.cs
+++ b/starsky/starsky.project.web/Helpers/PortProgramHelper.cs
@@ -2,13 +2,18 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Threading.Tasks;
+using starsky.foundation.platform.Helpers;
-namespace starsky.foundation.platform.Helpers;
+[assembly: InternalsVisibleTo("starskytest")]
+
+namespace starsky.project.web.Helpers;
public static class PortProgramHelper
{
- public static async Task SetEnvPortAspNetUrlsAndSetDefault(string[] args, string appSettingsPath)
+ public static async Task SetEnvPortAspNetUrlsAndSetDefault(string[] args,
+ string appSettingsPath)
{
if ( await SkipForAppSettingsJsonFile(appSettingsPath) )
{
@@ -23,14 +28,14 @@ internal static async Task SkipForAppSettingsJsonFile(string appSettingsPa
{
var appContainer = await ReadAppSettings.Read(appSettingsPath);
if ( appContainer?.Kestrel?.Endpoints?.Http?.Url == null &&
- appContainer?.Kestrel?.Endpoints?.Https?.Url == null )
+ appContainer?.Kestrel?.Endpoints?.Https?.Url == null )
{
return false;
}
Console.WriteLine("Kestrel Endpoints are set in appsettings.json, " +
- "this results in skip setting the PORT and default " +
- "ASPNETCORE_URLS environment variable");
+ "this results in skip setting the PORT and default " +
+ "ASPNETCORE_URLS environment variable");
return true;
}
@@ -40,7 +45,7 @@ internal static void SetEnvPortAspNetUrls(IEnumerable args)
var portString = Environment.GetEnvironmentVariable("PORT");
if ( args.Contains("--urls") || string.IsNullOrEmpty(portString)
- || !int.TryParse(portString, out var port) ) return;
+ || !int.TryParse(portString, out var port) ) return;
SetEnvironmentVariableForPort(port);
}
@@ -49,7 +54,7 @@ internal static void SetEnvPortAspNetUrls(IEnumerable args)
private static void SetEnvironmentVariableForPort(int port)
{
Console.WriteLine($"Set port from environment variable: {port} " +
- $"\nPro tip: Its recommended to use a https proxy like nginx or traefik");
+ $"\nPro tip: Its recommended to use a https proxy like nginx or traefik");
Environment.SetEnvironmentVariable("ASPNETCORE_URLS", $"http://*:{port}");
}
@@ -57,6 +62,7 @@ internal static void SetDefaultAspNetCoreUrls(IEnumerable args)
{
var aspNetCoreUrls = Environment.GetEnvironmentVariable("ASPNETCORE_URLS");
if ( args.Contains("--urls") || !string.IsNullOrEmpty(aspNetCoreUrls) ) return;
- Environment.SetEnvironmentVariable("ASPNETCORE_URLS", "http://localhost:4000;https://localhost:4001");
+ Environment.SetEnvironmentVariable("ASPNETCORE_URLS",
+ "http://localhost:4000;https://localhost:4001");
}
}
diff --git a/starsky/starskycore/Helpers/ToSQL.cs_debug b/starsky/starsky.project.web/Helpers/ToSQL.cs_debug
similarity index 96%
rename from starsky/starskycore/Helpers/ToSQL.cs_debug
rename to starsky/starsky.project.web/Helpers/ToSQL.cs_debug
index 1c687c6264..f8af8d6248 100644
--- a/starsky/starskycore/Helpers/ToSQL.cs_debug
+++ b/starsky/starsky.project.web/Helpers/ToSQL.cs_debug
@@ -4,7 +4,7 @@ using System.Reflection;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
-namespace starskycore.Helpers
+namespace starsky.project.web.Helpers
{
public static class ToSqlExtension
{
diff --git a/starsky/starskycore/ViewModels/ArchiveViewModel.cs b/starsky/starsky.project.web/ViewModels/ArchiveViewModel.cs
similarity index 97%
rename from starsky/starskycore/ViewModels/ArchiveViewModel.cs
rename to starsky/starsky.project.web/ViewModels/ArchiveViewModel.cs
index 2c86a3a9b0..e9fccb8eff 100644
--- a/starsky/starskycore/ViewModels/ArchiveViewModel.cs
+++ b/starsky/starsky.project.web/ViewModels/ArchiveViewModel.cs
@@ -3,7 +3,7 @@
using starsky.foundation.database.Models;
using starsky.foundation.platform.Helpers;
-namespace starskycore.ViewModels
+namespace starsky.project.web.ViewModels
{
[SuppressMessage("Performance", "CA1822:Mark members as static")]
public sealed class ArchiveViewModel
diff --git a/starsky/starskycore/ViewModels/EnvFeaturesViewModel.cs b/starsky/starsky.project.web/ViewModels/EnvFeaturesViewModel.cs
similarity index 53%
rename from starsky/starskycore/ViewModels/EnvFeaturesViewModel.cs
rename to starsky/starsky.project.web/ViewModels/EnvFeaturesViewModel.cs
index b52a03b918..34f731c7fd 100644
--- a/starsky/starskycore/ViewModels/EnvFeaturesViewModel.cs
+++ b/starsky/starsky.project.web/ViewModels/EnvFeaturesViewModel.cs
@@ -1,8 +1,7 @@
-namespace starskycore.ViewModels;
+namespace starsky.project.web.ViewModels;
public class EnvFeaturesViewModel
{
-
///
/// Trash is very dependent on the OS
///
@@ -11,5 +10,10 @@ public class EnvFeaturesViewModel
///
/// Enable or disable some features on the frontend
///
- public bool UseLocalDesktopUi { get; set; }
+ public bool UseLocalDesktop { get; set; }
+
+ ///
+ /// Is supported and enabled in the feature toggle
+ ///
+ public bool OpenEditorEnabled { get; set; }
}
diff --git a/starsky/starskycore/ViewModels/HealthView.cs b/starsky/starsky.project.web/ViewModels/HealthView.cs
similarity index 93%
rename from starsky/starskycore/ViewModels/HealthView.cs
rename to starsky/starsky.project.web/ViewModels/HealthView.cs
index e248dd584e..32929e176f 100644
--- a/starsky/starskycore/ViewModels/HealthView.cs
+++ b/starsky/starsky.project.web/ViewModels/HealthView.cs
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
-namespace starskycore.ViewModels
+namespace starsky.project.web.ViewModels
{
public sealed class HealthView
{
diff --git a/starsky/starskycore/ViewModels/MetricsDebugViewModel.cs b/starsky/starsky.project.web/ViewModels/MetricsDebugViewModel.cs
similarity index 66%
rename from starsky/starskycore/ViewModels/MetricsDebugViewModel.cs
rename to starsky/starsky.project.web/ViewModels/MetricsDebugViewModel.cs
index 83123d93bd..93293b5085 100644
--- a/starsky/starskycore/ViewModels/MetricsDebugViewModel.cs
+++ b/starsky/starsky.project.web/ViewModels/MetricsDebugViewModel.cs
@@ -1,4 +1,4 @@
-namespace starskycore.ViewModels;
+namespace starsky.project.web.ViewModels;
public class MetricsDebugViewModel
{
diff --git a/starsky/starskycore/ViewModels/SyncViewModel.cs b/starsky/starsky.project.web/ViewModels/SyncViewModel.cs
similarity index 87%
rename from starsky/starskycore/ViewModels/SyncViewModel.cs
rename to starsky/starsky.project.web/ViewModels/SyncViewModel.cs
index 2f33ba4bf9..0e3c341b7c 100644
--- a/starsky/starskycore/ViewModels/SyncViewModel.cs
+++ b/starsky/starsky.project.web/ViewModels/SyncViewModel.cs
@@ -1,7 +1,7 @@
-using starsky.foundation.database.Models;
using System.Text.Json.Serialization;
+using starsky.foundation.database.Models;
-namespace starskycore.ViewModels
+namespace starsky.project.web.ViewModels
{
public sealed class SyncViewModel
{
diff --git a/starsky/starskycore/starskycore.csproj b/starsky/starsky.project.web/starsky.project.web.csproj
similarity index 93%
rename from starsky/starskycore/starskycore.csproj
rename to starsky/starsky.project.web/starsky.project.web.csproj
index 04ae30444c..91466ff82f 100644
--- a/starsky/starskycore/starskycore.csproj
+++ b/starsky/starsky.project.web/starsky.project.web.csproj
@@ -10,6 +10,7 @@
SYSTEM_TEXT_ENABLEDFullenable
+ starsky.project.web
diff --git a/starsky/starsky.sln b/starsky/starsky.sln
index 5623f89bd0..ef8b5bbfa1 100644
--- a/starsky/starsky.sln
+++ b/starsky/starsky.sln
@@ -13,7 +13,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "starskywebhtmlcli", "starsk
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "starskygeocli", "starskygeocli\starskygeocli.csproj", "{EF96F7C8-4832-4606-8F5C-B1423FEE83B8}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "starskycore", "starskycore\starskycore.csproj", "{A90751E7-2F4D-44AA-8507-DDE5F980DBBB}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "starsky.project.web", "starsky.project.web\starsky.project.web.csproj", "{A90751E7-2F4D-44AA-8507-DDE5F980DBBB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "starskywebftpcli", "starskywebftpcli\starskywebftpcli.csproj", "{F8CE092D-F296-4B04-B013-EE5FCD4A8B3B}"
EndProject
@@ -164,6 +164,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "starsky.feature.trash", "st
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "starsky.feature.settings", "starsky.feature.settings\starsky.feature.settings.csproj", "{F2C4C9DE-22A1-4B34-AC1D-0F08353E0742}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "starsky.feature.desktop", "starsky.feature.desktop\starsky.feature.desktop.csproj", "{B88C2815-D154-4C6D-AE37-2E150AEBF73D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -362,6 +364,10 @@ Global
{F2C4C9DE-22A1-4B34-AC1D-0F08353E0742}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2C4C9DE-22A1-4B34-AC1D-0F08353E0742}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F2C4C9DE-22A1-4B34-AC1D-0F08353E0742}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B88C2815-D154-4C6D-AE37-2E150AEBF73D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B88C2815-D154-4C6D-AE37-2E150AEBF73D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B88C2815-D154-4C6D-AE37-2E150AEBF73D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B88C2815-D154-4C6D-AE37-2E150AEBF73D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -421,5 +427,6 @@ Global
{0072F697-4E18-4B5F-80DF-530361D3E847} = {1C1EB4A5-08D0-4014-AE1F-962642A4E5D3}
{A62C129C-5D0C-4A0A-B5AA-261E041FF55D} = {4B9276C3-651E-48D3-B3A7-3F4D74F3D01A}
{F2C4C9DE-22A1-4B34-AC1D-0F08353E0742} = {4B9276C3-651E-48D3-B3A7-3F4D74F3D01A}
+ {B88C2815-D154-4C6D-AE37-2E150AEBF73D} = {4B9276C3-651E-48D3-B3A7-3F4D74F3D01A}
EndGlobalSection
EndGlobal
diff --git a/starsky/starsky/Controllers/AccountController.cs b/starsky/starsky/Controllers/AccountController.cs
index c5e26c60c2..b723387ccb 100644
--- a/starsky/starsky/Controllers/AccountController.cs
+++ b/starsky/starsky/Controllers/AccountController.cs
@@ -23,12 +23,14 @@ public sealed class AccountController : Controller
private readonly IAntiforgery _antiForgery;
private readonly IStorage _storageHostFullPathFilesystem;
- public AccountController(IUserManager userManager, AppSettings appSettings, IAntiforgery antiForgery, ISelectorStorage selectorStorage)
+ public AccountController(IUserManager userManager, AppSettings appSettings,
+ IAntiforgery antiForgery, ISelectorStorage selectorStorage)
{
_userManager = userManager;
_appSettings = appSettings;
_antiForgery = antiForgery;
- _storageHostFullPathFilesystem = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem);
+ _storageHostFullPathFilesystem =
+ selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem);
}
///
@@ -76,9 +78,7 @@ public async Task Status()
var model = new UserIdentifierStatusModel
{
- Name = currentUser.Name,
- Id = currentUser.Id,
- Created = currentUser.Created,
+ Name = currentUser.Name, Id = currentUser.Id, Created = currentUser.Created,
};
var credentials = _userManager.GetCredentialsByUserId(currentUser.Id);
@@ -88,9 +88,13 @@ public async Task Status()
model.CredentialTypeIds = null;
return Json(model);
}
-
+
model.CredentialsIdentifiers?.Add(credentials.Identifier!);
model.CredentialTypeIds?.Add(credentials.CredentialTypeId);
+
+ var role = await _userManager.GetRoleAsync(currentUser.Id);
+ model.RoleCode = role?.Code;
+
return Json(model);
}
@@ -105,6 +109,7 @@ public async Task Status()
[ProducesResponseType(200)]
[Produces("text/html")]
[SuppressMessage("ReSharper", "UnusedParameter.Global")]
+ [SuppressMessage("Usage", "IDE0060:Remove unused parameter")]
[AllowAnonymous]
public IActionResult LoginGet(string? returnUrl = null, bool? fromLogout = null)
{
@@ -112,7 +117,8 @@ public IActionResult LoginGet(string? returnUrl = null, bool? fromLogout = null)
var clientApp = Path.Combine(_appSettings.BaseDirectoryProject,
"clientapp", "build", "index.html");
- if ( !_storageHostFullPathFilesystem.ExistFile(clientApp) ) return Content("Please check if the client code exist");
+ if ( !_storageHostFullPathFilesystem.ExistFile(clientApp) )
+ return Content("Please check if the client code exist");
return PhysicalFile(clientApp, "text/html");
}
@@ -137,7 +143,8 @@ public IActionResult LoginGet(string? returnUrl = null, bool? fromLogout = null)
[AllowAnonymous]
public async Task LoginPost(LoginViewModel model)
{
- ValidateResult validateResult = await _userManager.ValidateAsync("Email", model.Email, model.Password);
+ ValidateResult validateResult =
+ await _userManager.ValidateAsync("Email", model.Email, model.Password);
if ( !validateResult.Success )
{
@@ -146,6 +153,7 @@ public async Task LoginPost(LoginViewModel model)
{
Response.StatusCode = 423;
}
+
return Json("Login failed");
}
@@ -186,7 +194,8 @@ public IActionResult Logout(string? returnUrl = null)
{
_userManager.SignOut(HttpContext);
// fromLogout is used in middleware
- return RedirectToAction(nameof(LoginGet), new { ReturnUrl = returnUrl, fromLogout = true });
+ return RedirectToAction(nameof(LoginGet),
+ new { ReturnUrl = returnUrl, fromLogout = true });
}
///
@@ -211,7 +220,7 @@ public async Task ChangeSecret(ChangePasswordViewModel model)
}
if ( !ModelState.IsValid ||
- model.ChangedPassword != model.ChangedConfirmPassword )
+ model.ChangedPassword != model.ChangedConfirmPassword )
{
return BadRequest("Model is not correct");
}
@@ -282,7 +291,8 @@ public async Task Register(RegisterViewModel model)
private async Task IsAccountRegisterClosed(bool userIdentityIsAuthenticated)
{
if ( userIdentityIsAuthenticated ) return false;
- return _appSettings.IsAccountRegisterOpen != true && ( await _userManager.AllUsersAsync() ).Users.Count != 0;
+ return _appSettings.IsAccountRegisterOpen != true &&
+ ( await _userManager.AllUsersAsync() ).Users.Count != 0;
}
///
@@ -305,7 +315,7 @@ public async Task RegisterStatus()
}
if ( !await IsAccountRegisterClosed(
- User.Identity?.IsAuthenticated == true) )
+ User.Identity?.IsAuthenticated == true) )
{
return Json("RegisterStatus open");
}
@@ -329,6 +339,5 @@ public IActionResult Permissions()
var claims = User.Claims.Where(p => p.Type == "Permission").Select(p => p.Value);
return Json(claims);
}
-
}
}
diff --git a/starsky/starsky/Controllers/AllowedTypesController.cs b/starsky/starsky/Controllers/AllowedTypesController.cs
index a695ef484a..b60eb91e9f 100644
--- a/starsky/starsky/Controllers/AllowedTypesController.cs
+++ b/starsky/starsky/Controllers/AllowedTypesController.cs
@@ -3,14 +3,13 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using starsky.foundation.platform.Helpers;
-using starskycore.Helpers;
+using starsky.project.web.Helpers;
namespace starsky.Controllers
{
[Authorize]
public sealed class AllowedTypesController : Controller
{
-
///
/// A (string) list of allowed MIME-types ExtensionSyncSupportedList
///
@@ -22,7 +21,8 @@ public sealed class AllowedTypesController : Controller
[Produces("application/json")]
public IActionResult AllowedTypesMimetypeSync()
{
- var mimeTypes = ExtensionRolesHelper.ExtensionSyncSupportedList.Select(MimeHelper.GetMimeType).ToHashSet();
+ var mimeTypes = ExtensionRolesHelper.ExtensionSyncSupportedList
+ .Select(MimeHelper.GetMimeType).ToHashSet();
return Json(mimeTypes);
}
@@ -38,7 +38,8 @@ public IActionResult AllowedTypesMimetypeSync()
[Produces("application/json")]
public IActionResult AllowedTypesMimetypeSyncThumb()
{
- var mimeTypes = ExtensionRolesHelper.ExtensionThumbSupportedList.Select(MimeHelper.GetMimeType).ToHashSet();
+ var mimeTypes = ExtensionRolesHelper.ExtensionThumbSupportedList
+ .Select(MimeHelper.GetMimeType).ToHashSet();
return Json(mimeTypes);
}
diff --git a/starsky/starsky/Controllers/AppSettingsController.cs b/starsky/starsky/Controllers/AppSettingsController.cs
index 783f8c4b03..ad18e0b25b 100644
--- a/starsky/starsky/Controllers/AppSettingsController.cs
+++ b/starsky/starsky/Controllers/AppSettingsController.cs
@@ -38,6 +38,8 @@ public AppSettingsController(AppSettings appSettings,
public IActionResult Env()
{
var appSettings = _appSettings.CloneToDisplay();
+
+ // For end-to-end testing
if ( Request != null! && Request.Headers.Any(p => p.Key == "x-force-html") )
{
Response.Headers.ContentType = "text/html; charset=utf-8";
diff --git a/starsky/starsky/Controllers/AppSettingsFeaturesController.cs b/starsky/starsky/Controllers/AppSettingsFeaturesController.cs
index fd7d9dfb06..a41dcee8c6 100644
--- a/starsky/starsky/Controllers/AppSettingsFeaturesController.cs
+++ b/starsky/starsky/Controllers/AppSettingsFeaturesController.cs
@@ -1,8 +1,9 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+using starsky.feature.desktop.Interfaces;
using starsky.feature.trash.Interfaces;
using starsky.foundation.platform.Models;
-using starskycore.ViewModels;
+using starsky.project.web.ViewModels;
namespace starsky.Controllers;
@@ -10,10 +11,14 @@ public class AppSettingsFeaturesController : Controller
{
private readonly IMoveToTrashService _moveToTrashService;
private readonly AppSettings _appSettings;
+ private readonly IOpenEditorDesktopService _openEditorDesktopService;
- public AppSettingsFeaturesController(IMoveToTrashService moveToTrashService, AppSettings appSettings)
+ public AppSettingsFeaturesController(IMoveToTrashService moveToTrashService,
+ IOpenEditorDesktopService openEditorDesktopService,
+ AppSettings appSettings)
{
_moveToTrashService = moveToTrashService;
+ _openEditorDesktopService = openEditorDesktopService;
_appSettings = appSettings;
}
@@ -36,7 +41,8 @@ public IActionResult FeaturesView()
var shortAppSettings = new EnvFeaturesViewModel
{
SystemTrashEnabled = _moveToTrashService.IsEnabled(),
- UseLocalDesktopUi = _appSettings.UseLocalDesktopUi == true
+ UseLocalDesktop = _appSettings.UseLocalDesktop == true,
+ OpenEditorEnabled = _openEditorDesktopService.IsEnabled()
};
return Json(shortAppSettings);
diff --git a/starsky/starsky/Controllers/DesktopEditorController.cs b/starsky/starsky/Controllers/DesktopEditorController.cs
new file mode 100644
index 0000000000..0cfdbbd1ce
--- /dev/null
+++ b/starsky/starsky/Controllers/DesktopEditorController.cs
@@ -0,0 +1,72 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using starsky.feature.desktop.Interfaces;
+using starsky.feature.desktop.Models;
+
+namespace starsky.Controllers;
+
+[Authorize]
+public class DesktopEditorController : Controller
+{
+ private readonly IOpenEditorDesktopService _openEditorDesktopService;
+
+ public DesktopEditorController(IOpenEditorDesktopService openEditorDesktopService)
+ {
+ _openEditorDesktopService = openEditorDesktopService;
+ }
+
+ ///
+ /// Open a file in the default editor or a specific editor on the desktop
+ ///
+ /// single or multiple subPaths
+ /// to combine files with the same name before the extension
+ ///
+ /// returns a list of items from the database
+ /// list with no content
+ /// subPath not found in the database
+ /// User unauthorized
+ [HttpPost("/api/desktop-editor/open")]
+ [Produces("application/json")]
+ [ProducesResponseType(typeof(List), 200)]
+ [ProducesResponseType(typeof(List), 206)]
+ [ProducesResponseType(typeof(string), 400)]
+ [ProducesResponseType(401)]
+ public async Task OpenAsync(
+ string f = "",
+ bool collections = true)
+ {
+ var (success, status, list) =
+ await _openEditorDesktopService.OpenAsync(f, collections);
+
+ switch ( success )
+ {
+ case null:
+ return BadRequest(status);
+ case false:
+ HttpContext.Response.StatusCode = 206;
+ break;
+ }
+
+ return Json(list);
+ }
+
+
+ ///
+ /// Check the amount of files to open before
+ ///
+ /// single or multiple subPaths
+ ///
+ /// bool, true is no confirmation, false is ask confirmation
+ /// User unauthorized
+ [HttpPost("/api/desktop-editor/amount-confirmation")]
+ [Produces("application/json")]
+ [ProducesResponseType(typeof(bool), 200)]
+ [ProducesResponseType(401)]
+ public IActionResult OpenAmountConfirmationChecker(string f)
+ {
+ var result = _openEditorDesktopService.OpenAmountConfirmationChecker(f);
+ return Json(result);
+ }
+}
diff --git a/starsky/starsky/Controllers/DiskController.cs b/starsky/starsky/Controllers/DiskController.cs
index 855b1b3fa0..e8c8666dc4 100644
--- a/starsky/starsky/Controllers/DiskController.cs
+++ b/starsky/starsky/Controllers/DiskController.cs
@@ -13,7 +13,7 @@
using starsky.foundation.realtime.Interfaces;
using starsky.foundation.storage.Interfaces;
using starsky.foundation.storage.Storage;
-using starskycore.ViewModels;
+using starsky.project.web.ViewModels;
namespace starsky.Controllers
{
@@ -62,11 +62,9 @@ public async Task Mkdir(string f)
foreach ( var subPath in inputFilePaths.Select(PathHelper.RemoveLatestSlash) )
{
-
var toAddStatus = new SyncViewModel
{
- FilePath = subPath,
- Status = FileIndexItem.ExifStatus.Ok
+ FilePath = subPath, Status = FileIndexItem.ExifStatus.Ok
};
if ( _iStorage.ExistFolder(subPath) )
@@ -78,7 +76,7 @@ public async Task Mkdir(string f)
await _query.AddItemAsync(new FileIndexItem(subPath)
{
- IsDirectory = true
+ IsDirectory = true, ImageFormat = ExtensionRolesHelper.ImageFormat.directory
});
// add to fs
@@ -102,12 +100,12 @@ await _query.AddItemAsync(new FileIndexItem(subPath)
/// SyncViewModel
/// optional debug name
/// Completed send of Socket SendToAllAsync
- private async Task SyncMessageToSocket(IEnumerable syncResultsList, ApiNotificationType type = ApiNotificationType.Unknown)
+ private async Task SyncMessageToSocket(IEnumerable syncResultsList,
+ ApiNotificationType type = ApiNotificationType.Unknown)
{
var list = syncResultsList.Select(t => new FileIndexItem(t.FilePath)
{
- Status = t.Status,
- IsDirectory = true
+ Status = t.Status, IsDirectory = true
}).ToList();
var webSocketResponse = new ApiNotificationResponseModel<
@@ -132,7 +130,8 @@ private async Task SyncMessageToSocket(IEnumerable syncResultsLis
[ProducesResponseType(typeof(List), 404)]
[HttpPost("/api/disk/rename")]
[Produces("application/json")]
- public async Task Rename(string f, string to, bool collections = true, bool currentStatus = true)
+ public async Task Rename(string f, string to, bool collections = true,
+ bool currentStatus = true)
{
if ( string.IsNullOrEmpty(f) )
{
@@ -146,14 +145,16 @@ public async Task Rename(string f, string to, bool collections =
return NotFound(rename);
var webSocketResponse =
- new ApiNotificationResponseModel>(rename, ApiNotificationType.Rename);
+ new ApiNotificationResponseModel>(rename,
+ ApiNotificationType.Rename);
await _notificationQuery.AddNotification(webSocketResponse);
await _connectionsService.SendToAllAsync(webSocketResponse, CancellationToken.None);
- return Json(currentStatus ? rename.Where(p => p.Status
- != FileIndexItem.ExifStatus.NotFoundSourceMissing).ToList() : rename);
+ return Json(currentStatus
+ ? rename.Where(p => p.Status
+ != FileIndexItem.ExifStatus.NotFoundSourceMissing).ToList()
+ : rename);
}
-
}
}
diff --git a/starsky/starsky/Controllers/DownloadPhotoController.cs b/starsky/starsky/Controllers/DownloadPhotoController.cs
index ee409f1f02..509b7f9924 100644
--- a/starsky/starsky/Controllers/DownloadPhotoController.cs
+++ b/starsky/starsky/Controllers/DownloadPhotoController.cs
@@ -10,7 +10,7 @@
using starsky.foundation.storage.Storage;
using starsky.foundation.thumbnailgeneration.Interfaces;
using starsky.Helpers;
-using starskycore.Helpers;
+using starsky.project.web.Helpers;
namespace starsky.Controllers
{
diff --git a/starsky/starsky/Controllers/ExportController.cs b/starsky/starsky/Controllers/ExportController.cs
index 0dd68b2d14..22d2d98d64 100644
--- a/starsky/starsky/Controllers/ExportController.cs
+++ b/starsky/starsky/Controllers/ExportController.cs
@@ -10,7 +10,7 @@
using starsky.foundation.storage.Interfaces;
using starsky.foundation.storage.Storage;
using starsky.foundation.worker.Interfaces;
-using starskycore.Helpers;
+using starsky.project.web.Helpers;
namespace starsky.Controllers
{
diff --git a/starsky/starsky/Controllers/HealthController.cs b/starsky/starsky/Controllers/HealthController.cs
index ceb03fb6e6..98f630648a 100644
--- a/starsky/starsky/Controllers/HealthController.cs
+++ b/starsky/starsky/Controllers/HealthController.cs
@@ -10,7 +10,7 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using starsky.foundation.platform.Extensions;
using starsky.foundation.platform.VersionHelpers;
-using starskycore.ViewModels;
+using starsky.project.web.ViewModels;
[assembly: InternalsVisibleTo("starskytest")]
@@ -60,9 +60,9 @@ internal async Task CheckHealthAsyncWithTimeout(int timeoutTime =
try
{
if ( _cache != null &&
- _cache.TryGetValue(healthControllerCacheKey, out var objectHealthStatus) &&
- objectHealthStatus is HealthReport healthStatus &&
- healthStatus.Status == HealthStatus.Healthy )
+ _cache.TryGetValue(healthControllerCacheKey, out var objectHealthStatus) &&
+ objectHealthStatus is HealthReport healthStatus &&
+ healthStatus.Status == HealthStatus.Healthy )
{
return healthStatus;
}
@@ -132,7 +132,7 @@ private static HealthView CreateHealthEntryLog(HealthReport result)
Name = key,
IsHealthy = value.Status == HealthStatus.Healthy,
Description = value.Description + value.Exception?.Message +
- value.Exception?.StackTrace
+ value.Exception?.StackTrace
}
);
}
diff --git a/starsky/starsky/Controllers/HomeController.cs b/starsky/starsky/Controllers/HomeController.cs
index 9d77106361..2b14477c3a 100644
--- a/starsky/starsky/Controllers/HomeController.cs
+++ b/starsky/starsky/Controllers/HomeController.cs
@@ -10,6 +10,7 @@
using starsky.Helpers;
[assembly: InternalsVisibleTo("starskytest")]
+
namespace starsky.Controllers
{
[Authorize]
@@ -35,6 +36,7 @@ public HomeController(AppSettings appSettings, IAntiforgery antiForgery)
[Produces("text/html")]
[ProducesResponseType(200)]
[ProducesResponseType(401)]
+ [SuppressMessage("Usage", "IDE0060:Remove unused parameter")]
public IActionResult Index(string f = "")
{
new AntiForgeryCookie(_antiForgery).SetAntiForgeryCookie(HttpContext);
@@ -63,10 +65,11 @@ public IActionResult SearchPost(string t = "", int p = 0)
// Added filter to prevent redirects based on tainted, user-controlled data
// unescaped: ^[a-zA-Z0-9_\-+"'/=:,\.>< ]+$
if ( !Regex.IsMatch(t, "^[a-zA-Z0-9_\\-+\"'/=:,\\.>< ]+$",
- RegexOptions.None, TimeSpan.FromMilliseconds(100)) )
+ RegexOptions.None, TimeSpan.FromMilliseconds(100)) )
{
return BadRequest("`t` is not allowed");
}
+
return Redirect(AppendPathBasePrefix(Request.PathBase.Value, $"/search?t={t}&p={p}"));
}
@@ -102,10 +105,11 @@ public IActionResult Search(string t = "", int p = 0)
// Added filter to prevent redirects based on tainted, user-controlled data
// unescaped: ^[a-zA-Z0-9_\-+"'/=:>< ]+$
if ( !Regex.IsMatch(t, "^[a-zA-Z0-9_\\-+\"'/=:>< ]+$",
- RegexOptions.None, TimeSpan.FromMilliseconds(100)) )
+ RegexOptions.None, TimeSpan.FromMilliseconds(100)) )
{
return BadRequest("`t` is not allowed");
}
+
return Redirect(AppendPathBasePrefix(Request.PathBase.Value, $"/search?t={t}&p={p}"));
}
@@ -128,6 +132,7 @@ public IActionResult Trash(int p = 0)
{
return Redirect(AppendPathBasePrefix(Request.PathBase.Value, $"/trash?p={p}"));
}
+
return PhysicalFile(_clientApp, "text/html");
}
@@ -147,6 +152,7 @@ public IActionResult Import()
{
return Redirect(AppendPathBasePrefix(Request.PathBase.Value, $"/import"));
}
+
return PhysicalFile(_clientApp, "text/html");
}
@@ -166,6 +172,7 @@ public IActionResult Preferences()
{
return Redirect(AppendPathBasePrefix(Request.PathBase.Value, $"/preferences"));
}
+
return PhysicalFile(_clientApp, "text/html");
}
@@ -181,6 +188,7 @@ public IActionResult Preferences()
[Produces("text/html")]
[ProducesResponseType(200)]
[SuppressMessage("ReSharper", "UnusedParameter.Global")]
+ [SuppressMessage("Usage", "IDE0060:Remove unused parameter")]
public IActionResult Register(string? returnUrl = null)
{
new AntiForgeryCookie(_antiForgery).SetAntiForgeryCookie(HttpContext);
@@ -190,10 +198,13 @@ public IActionResult Register(string? returnUrl = null)
internal static string AppendPathBasePrefix(string? requestPathBase, string url)
{
return requestPathBase?.Equals("/starsky",
- StringComparison.InvariantCultureIgnoreCase) == true ? $"/starsky{url}" : url;
+ StringComparison.InvariantCultureIgnoreCase) == true
+ ? $"/starsky{url}"
+ : url;
}
- internal static bool IsCaseSensitiveRedirect(string? expectedRequestPath, string? requestPathValue)
+ internal static bool IsCaseSensitiveRedirect(string? expectedRequestPath,
+ string? requestPathValue)
{
return expectedRequestPath != requestPathValue;
}
diff --git a/starsky/starsky/Controllers/IndexController.cs b/starsky/starsky/Controllers/IndexController.cs
index b240aee0db..0536625a81 100644
--- a/starsky/starsky/Controllers/IndexController.cs
+++ b/starsky/starsky/Controllers/IndexController.cs
@@ -6,7 +6,7 @@
using starsky.foundation.database.Models;
using starsky.foundation.platform.Helpers;
using starsky.foundation.platform.Models;
-using starskycore.ViewModels;
+using starsky.project.web.ViewModels;
namespace starsky.Controllers
{
diff --git a/starsky/starsky/Controllers/MetricsDebugController.cs b/starsky/starsky/Controllers/MetricsDebugController.cs
index 9108760e2a..8318079bc3 100644
--- a/starsky/starsky/Controllers/MetricsDebugController.cs
+++ b/starsky/starsky/Controllers/MetricsDebugController.cs
@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using starsky.foundation.worker.CpuEventListener.Interfaces;
-using starskycore.ViewModels;
+using starsky.project.web.ViewModels;
namespace starsky.Controllers;
@@ -18,9 +18,6 @@ public MetricsDebugController(ICpuUsageListener cpuUsageListener)
public IActionResult Index()
{
- return Json(new MetricsDebugViewModel
- {
- CpuUsageMean = _cpuUsageListener.CpuUsageMean,
- });
+ return Json(new MetricsDebugViewModel { CpuUsageMean = _cpuUsageListener.CpuUsageMean, });
}
}
diff --git a/starsky/starsky/Controllers/ThumbnailController.cs b/starsky/starsky/Controllers/ThumbnailController.cs
index e00ab4c645..8c97aaf858 100644
--- a/starsky/starsky/Controllers/ThumbnailController.cs
+++ b/starsky/starsky/Controllers/ThumbnailController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
@@ -13,7 +14,7 @@
using starsky.foundation.storage.Interfaces;
using starsky.foundation.storage.Models;
using starsky.foundation.storage.Storage;
-using starskycore.Helpers;
+using starsky.project.web.Helpers;
namespace starsky.Controllers
{
@@ -53,22 +54,29 @@ public IActionResult ThumbnailSmallOrTinyMeta(string f)
// Restrict the fileHash to letters and digits only
// I/O function calls should not be vulnerable to path injection attacks
if ( !Regex.IsMatch(f, "^[a-zA-Z0-9_-]+$",
- RegexOptions.None, TimeSpan.FromMilliseconds(200)) )
+ RegexOptions.None, TimeSpan.FromMilliseconds(200)) )
{
return BadRequest();
}
if ( _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, ThumbnailSize.Small)) )
{
- var stream = _thumbnailStorage.ReadStream(ThumbnailNameHelper.Combine(f, ThumbnailSize.Small));
- Response.Headers.TryAdd("x-image-size", new StringValues(ThumbnailSize.Small.ToString()));
+ var stream =
+ _thumbnailStorage.ReadStream(
+ ThumbnailNameHelper.Combine(f, ThumbnailSize.Small));
+ Response.Headers.TryAdd("x-image-size",
+ new StringValues(ThumbnailSize.Small.ToString()));
return File(stream, "image/jpeg");
}
- if ( _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, ThumbnailSize.TinyMeta)) )
+ if ( _thumbnailStorage.ExistFile(
+ ThumbnailNameHelper.Combine(f, ThumbnailSize.TinyMeta)) )
{
- var stream = _thumbnailStorage.ReadStream(ThumbnailNameHelper.Combine(f, ThumbnailSize.TinyMeta));
- Response.Headers.TryAdd("x-image-size", new StringValues(ThumbnailSize.TinyMeta.ToString()));
+ var stream =
+ _thumbnailStorage.ReadStream(
+ ThumbnailNameHelper.Combine(f, ThumbnailSize.TinyMeta));
+ Response.Headers.TryAdd("x-image-size",
+ new StringValues(ThumbnailSize.TinyMeta.ToString()));
return File(stream, "image/jpeg");
}
@@ -78,8 +86,10 @@ public IActionResult ThumbnailSmallOrTinyMeta(string f)
return NotFound("hash not found");
}
- var streamDefaultThumbnail = _thumbnailStorage.ReadStream(ThumbnailNameHelper.Combine(f, ThumbnailSize.Large));
- Response.Headers.TryAdd("x-image-size", new StringValues(ThumbnailSize.Large.ToString()));
+ var streamDefaultThumbnail =
+ _thumbnailStorage.ReadStream(ThumbnailNameHelper.Combine(f, ThumbnailSize.Large));
+ Response.Headers.TryAdd("x-image-size",
+ new StringValues(ThumbnailSize.Large.ToString()));
return File(streamDefaultThumbnail, "image/jpeg");
}
@@ -101,7 +111,6 @@ public IActionResult ThumbnailSmallOrTinyMeta(string f)
[ProducesResponseType(
400)] // string (f) input not allowed to avoid path injection attacks
[ProducesResponseType(404)] // not found
-
public async Task ListSizesByHash(string f)
{
// For serving jpeg files
@@ -110,17 +119,25 @@ public async Task ListSizesByHash(string f)
// Restrict the fileHash to letters and digits only
// I/O function calls should not be vulnerable to path injection attacks
if ( !Regex.IsMatch(f, "^[a-zA-Z0-9_-]+$",
- RegexOptions.None, TimeSpan.FromMilliseconds(100)) )
+ RegexOptions.None, TimeSpan.FromMilliseconds(100)) )
{
return BadRequest();
}
var data = new ThumbnailSizesExistStatusModel
{
- TinyMeta = _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, ThumbnailSize.TinyMeta)),
- Small = _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, ThumbnailSize.Small)),
- Large = _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, ThumbnailSize.Large)),
- ExtraLarge = _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, ThumbnailSize.ExtraLarge))
+ TinyMeta =
+ _thumbnailStorage.ExistFile(
+ ThumbnailNameHelper.Combine(f, ThumbnailSize.TinyMeta)),
+ Small =
+ _thumbnailStorage.ExistFile(
+ ThumbnailNameHelper.Combine(f, ThumbnailSize.Small)),
+ Large =
+ _thumbnailStorage.ExistFile(
+ ThumbnailNameHelper.Combine(f, ThumbnailSize.Large)),
+ ExtraLarge =
+ _thumbnailStorage.ExistFile(
+ ThumbnailNameHelper.Combine(f, ThumbnailSize.ExtraLarge))
};
// Success has all items (except tinyMeta)
@@ -137,11 +154,11 @@ public async Task ListSizesByHash(string f)
return Json(data);
case false when !string.IsNullOrEmpty(sourcePath):
Response.StatusCode = 210; // A conflict, that the thumb is not generated yet
- return Json("Thumbnail is not supported; for example you try to view a raw or video file");
+ return Json(
+ "Thumbnail is not supported; for example you try to view a raw or video file");
default:
return NotFound("not in index");
}
-
}
private IActionResult ReturnThumbnailResult(string f, bool json, ThumbnailSize size)
@@ -160,10 +177,11 @@ private IActionResult ReturnThumbnailResult(string f, bool json, ThumbnailSize s
if ( json ) return Json("OK");
stream = _thumbnailStorage.ReadStream(
- ThumbnailNameHelper.Combine(f, size));
+ ThumbnailNameHelper.Combine(f, size));
// thumbs are always in jpeg
- Response.Headers.Append("x-filename", new StringValues(FilenamesHelper.GetFileName(f + ".jpg")));
+ Response.Headers.Append("x-filename",
+ new StringValues(FilenamesHelper.GetFileName(f + ".jpg")));
return File(stream, "image/jpeg");
}
@@ -217,7 +235,7 @@ public async Task Thumbnail(
// Restrict the fileHash to letters and digits only
// I/O function calls should not be vulnerable to path injection attacks
if ( !Regex.IsMatch(f, "^[a-zA-Z0-9_-]+$",
- RegexOptions.None, TimeSpan.FromMilliseconds(100)) )
+ RegexOptions.None, TimeSpan.FromMilliseconds(100)) )
{
return BadRequest();
}
@@ -248,7 +266,8 @@ public async Task Thumbnail(
// remove from cache
_query.ResetItemByHash(f);
- if ( string.IsNullOrEmpty(filePath) || await _query.GetObjectByFilePathAsync(filePath) == null )
+ if ( string.IsNullOrEmpty(filePath) ||
+ await _query.GetObjectByFilePathAsync(filePath) == null )
{
SetExpiresResponseHeadersToZero();
return NotFound("not in index");
@@ -260,7 +279,7 @@ public async Task Thumbnail(
if ( !_iStorage.ExistFile(sourcePath) )
{
return NotFound("There is no thumbnail image " + f + " and no source image " +
- sourcePath);
+ sourcePath);
}
if ( !isSingleItem )
@@ -303,6 +322,7 @@ public async Task Thumbnail(
[ProducesResponseType(400)] // string (f) input not allowed to avoid path injection attacks
[ProducesResponseType(404)] // not found
[ProducesResponseType(210)] // raw
+ [SuppressMessage("Usage", "IDE0060:Remove unused parameter")]
public async Task ByZoomFactorAsync(
string f,
int z = 0,
@@ -314,7 +334,7 @@ public async Task ByZoomFactorAsync(
// Restrict the fileHash to letters and digits only
// I/O function calls should not be vulnerable to path injection attacks
if ( !Regex.IsMatch(f, "^[a-zA-Z0-9_-]+$",
- RegexOptions.None, TimeSpan.FromMilliseconds(100)) )
+ RegexOptions.None, TimeSpan.FromMilliseconds(100)) )
{
return BadRequest();
}
@@ -327,6 +347,7 @@ public async Task ByZoomFactorAsync(
{
return NotFound("not in index");
}
+
sourcePath = filePath;
}
@@ -349,7 +370,8 @@ public async Task ByZoomFactorAsync(
public void SetExpiresResponseHeadersToZero()
{
Request.HttpContext.Response.Headers.Remove("Cache-Control");
- Request.HttpContext.Response.Headers.Append("Cache-Control", "no-cache, no-store, must-revalidate");
+ Request.HttpContext.Response.Headers.Append("Cache-Control",
+ "no-cache, no-store, must-revalidate");
Request.HttpContext.Response.Headers.Remove("Pragma");
Request.HttpContext.Response.Headers.Append("Pragma", "no-cache");
diff --git a/starsky/starsky/Controllers/TrashController.cs b/starsky/starsky/Controllers/TrashController.cs
index 555674fc3d..d4d7bc2bbc 100644
--- a/starsky/starsky/Controllers/TrashController.cs
+++ b/starsky/starsky/Controllers/TrashController.cs
@@ -20,21 +20,7 @@ public TrashController(IMoveToTrashService moveToTrashService)
}
///
- /// Is the system trash supported
- ///
- /// bool with json (IActionResult Result)
- /// the item including the updated content
- /// User unauthorized
- [ProducesResponseType(typeof(bool), 200)]
- [HttpGet("/api/trash/detect-to-use-system-trash")]
- [Produces("application/json")]
- public IActionResult DetectToUseSystemTrash()
- {
- return Json(_moveToTrashService.DetectToUseSystemTrash());
- }
-
- ///
- /// (beta) Move a file to the trash
+ /// Move a file to the trash
///
/// subPath filepath to file, split by dot comma (;)
/// stack collections
@@ -56,7 +42,8 @@ public async Task TrashMoveAsync(string f, bool collections = fal
return BadRequest("No input files");
}
- var fileIndexResultsList = await _moveToTrashService.MoveToTrashAsync(inputFilePaths.ToList(), collections);
+ var fileIndexResultsList =
+ await _moveToTrashService.MoveToTrashAsync(inputFilePaths.ToList(), collections);
return Json(fileIndexResultsList);
}
diff --git a/starsky/starsky/Program.cs b/starsky/starsky/Program.cs
index 98863a6f10..bf1c2ff7c3 100644
--- a/starsky/starsky/Program.cs
+++ b/starsky/starsky/Program.cs
@@ -5,17 +5,16 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
-using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
-using starsky.foundation.platform.Helpers;
using starsky.foundation.platform.Models;
+using starsky.project.web.Helpers;
namespace starsky
{
public static class Program
{
[SuppressMessage("Usage", "S6603: The collection-specific TrueForAll " +
- "method should be used instead of the All extension")]
+ "method should be used instead of the All extension")]
public static async Task Main(string[] args)
{
var appSettingsPath = Path.Join(
@@ -29,8 +28,7 @@ public static async Task Main(string[] args)
builder.Host.UseWindowsService();
var app = builder.Build();
- var hostLifetime = app.Services.GetRequiredService();
- startup.Configure(app, builder.Environment, hostLifetime);
+ startup.Configure(app, builder.Environment);
await RunAsync(app, args.All(p => p != "--do-not-start"));
}
@@ -51,6 +49,7 @@ internal static async Task RunAsync(WebApplication webApplication,
{
return false;
}
+
return true;
}
@@ -67,7 +66,7 @@ private static WebApplicationBuilder CreateWebHostBuilder(string[] args)
builder.WebHost.ConfigureKestrel(k =>
{
k.Limits.MaxRequestLineSize = 65536; //64Kb
- // AddServerHeader removes the header: Server: Kestrel
+ // AddServerHeader removes the header: Server: Kestrel
k.AddServerHeader = false;
});
diff --git a/starsky/starsky/Properties/default-init-launchSettings.json b/starsky/starsky/Properties/default-init-launchSettings.json
index 898032daae..36ab404143 100644
--- a/starsky/starsky/Properties/default-init-launchSettings.json
+++ b/starsky/starsky/Properties/default-init-launchSettings.json
@@ -25,6 +25,7 @@
"app__EnablePackageTelemetryDebug": "true",
"app__DemoUnsafeDeleteStorageFolder": "false",
"app__useSystemTrash": "true",
+ "app__UseLocalDesktop": "true",
"app__accountRolesByEmailRegisterOverwrite__demo@qdraw.nl": "Administrator",
"app__ThumbnailGenerationIntervalInMinutes": "15",
"___app__OpenTelemetry__TracesEndpoint": "http://localhost:4318/v1/traces",
diff --git a/starsky/starsky/Startup.cs b/starsky/starsky/Startup.cs
index a7dc9f69cd..52eb3e16ae 100644
--- a/starsky/starsky/Startup.cs
+++ b/starsky/starsky/Startup.cs
@@ -57,7 +57,7 @@ public Startup(string[]? args = null)
if ( !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("app__appsettingspath")) )
{
Console.WriteLine("app__appSettingsPath: " +
- Environment.GetEnvironmentVariable("app__appsettingspath"));
+ Environment.GetEnvironmentVariable("app__appsettingspath"));
}
_configuration = SetupAppSettings.AppSettingsToBuilder(args).ConfigureAwait(false)
@@ -256,9 +256,7 @@ private static Func, Task> ReplaceR
///
/// ApplicationBuilder
/// Hosting Env
- /// application Lifetime
- public void Configure(IApplicationBuilder app, IHostEnvironment env,
- IHostApplicationLifetime applicationLifetime)
+ public void Configure(IApplicationBuilder app, IHostEnvironment env)
{
app.UseResponseCompression();
@@ -289,7 +287,7 @@ public void Configure(IApplicationBuilder app, IHostEnvironment env,
app.UseAuthentication();
app.UseBasicAuthentication();
app.UseNoAccount(_appSettings?.NoAccountLocalhost == true ||
- _appSettings?.DemoUnsafeDeleteStorageFolder == true);
+ _appSettings?.DemoUnsafeDeleteStorageFolder == true);
app.UseCheckIfAccountExist();
app.UseAuthorization();
@@ -322,7 +320,7 @@ public void Configure(IApplicationBuilder app, IHostEnvironment env,
internal (bool, bool, bool) SetupStaticFiles(IApplicationBuilder app,
string assetsName = "assets")
{
- var result = (false, false, false);
+ var result = ( false, false, false );
// Allow Current Directory and wwwroot in Base Directory
// AppSettings can be null when running tests
@@ -351,19 +349,19 @@ public void Configure(IApplicationBuilder app, IHostEnvironment env,
// Check if clientapp is build and use the assets folder
if ( !Directory.Exists(Path.Combine(
- _appSettings.BaseDirectoryProject, "clientapp", "build", assetsName)) )
+ _appSettings.BaseDirectoryProject, "clientapp", "build", assetsName)) )
{
return result;
}
app.UseStaticFiles(new StaticFileOptions
- {
- OnPrepareResponse = PrepareResponse,
- FileProvider = new PhysicalFileProvider(
+ {
+ OnPrepareResponse = PrepareResponse,
+ FileProvider = new PhysicalFileProvider(
Path.Combine(_appSettings.BaseDirectoryProject, "clientapp", "build",
assetsName)),
- RequestPath = $"/assets",
- }
+ RequestPath = $"/assets",
+ }
);
result.Item3 = true;
return result;
diff --git a/starsky/starsky/appsettings.json b/starsky/starsky/appsettings.json
index c8358f69b0..d9ea300166 100644
--- a/starsky/starsky/appsettings.json
+++ b/starsky/starsky/appsettings.json
@@ -57,10 +57,14 @@
"SyncOnStartup": "true",
"DemoUnsafeDeleteStorageFolder": "false",
"useSystemTrash": "true",
+ "UseLocalDesktop": "false",
"OpenTelemetry": {
"TracesEndpoint": null,
"MetricsEndpoint": null,
- "LogsEndpoint": null
+ "LogsEndpoint": null,
+ "TracesHeader": null,
+ "MetricsHeader": null,
+ "LogsHeader": null
},
"publishProfiles": {
"_default": [
diff --git a/starsky/starsky/clientapp/.vscode/launch.json b/starsky/starsky/clientapp/.vscode/launch.json
index c8df8623f5..0ed1589c17 100644
--- a/starsky/starsky/clientapp/.vscode/launch.json
+++ b/starsky/starsky/clientapp/.vscode/launch.json
@@ -5,16 +5,24 @@
"version": "0.2.0",
"configurations": [
{
- "type": "node",
- "name": "vscode-jest-tests",
+ "name": "Jest file",
+ "type": "pwa-node",
"request": "launch",
- "args": ["test", "--runInBand"],
- "cwd": "${workspaceFolder}",
+ "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/jest",
+ "args": [
+ "${fileBasenameNoExtension}",
+ "--runInBand",
+ "--watch",
+ "--coverage=false",
+ "--no-cache"
+ ],
+ "cwd": "${workspaceRoot}",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
- "disableOptimisticBPs": true,
- "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/react-scripts",
- "protocol": "inspector"
+ "sourceMaps": true,
+ "windows": {
+ "program": "${workspaceFolder}/node_modules/jest/bin/jest"
+ }
}
]
}
diff --git a/starsky/starsky/clientapp/.vscode/settings.json b/starsky/starsky/clientapp/.vscode/settings.json
index 9bb732eb5a..30dd5ec2f5 100644
--- a/starsky/starsky/clientapp/.vscode/settings.json
+++ b/starsky/starsky/clientapp/.vscode/settings.json
@@ -4,5 +4,8 @@
"eslint.alwaysShowStatus": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
+ },
+ "[jsonc]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
diff --git a/starsky/starsky/clientapp/package.json b/starsky/starsky/clientapp/package.json
index 45ea6fb6fa..d74b2c9bee 100644
--- a/starsky/starsky/clientapp/package.json
+++ b/starsky/starsky/clientapp/package.json
@@ -102,7 +102,7 @@
"projectRoot": "../../"
}
],
- "json",
+ "html",
"cobertura"
],
"coverageThreshold": {
diff --git a/starsky/starsky/clientapp/src/components/atoms/form-control/form-control.tsx b/starsky/starsky/clientapp/src/components/atoms/form-control/form-control.tsx
index 7c9722f343..247facd03d 100644
--- a/starsky/starsky/clientapp/src/components/atoms/form-control/form-control.tsx
+++ b/starsky/starsky/clientapp/src/components/atoms/form-control/form-control.tsx
@@ -1,5 +1,6 @@
import React, { useState } from "react";
import useGlobalSettings from "../../../hooks/use-global-settings";
+import localization from "../../../localization/localization.json";
import { Language } from "../../../shared/language";
import { LimitLength } from "./limit-length";
@@ -27,11 +28,8 @@ const FormControl: React.FunctionComponent = ({ onBlur, ...pr
// content
const settings = useGlobalSettings();
const language = new Language(settings.language);
- const MessageFieldMaxLength = language.token(
- language.text(
- "Het onderstaande veld mag maximaal {maxlength} tekens hebben",
- "The field below can have a maximum of {maxlength} characters"
- ),
+ const MessageFieldMaxLength = language.key(
+ localization.MessageFieldMaxLength,
["{maxlength}"],
[maxlength.toString()]
);
diff --git a/starsky/starsky/clientapp/src/components/atoms/menu-option-modal/menu-option-modal.spec.tsx b/starsky/starsky/clientapp/src/components/atoms/menu-option-modal/menu-option-modal.spec.tsx
index b6609b511d..a6aebc150c 100644
--- a/starsky/starsky/clientapp/src/components/atoms/menu-option-modal/menu-option-modal.spec.tsx
+++ b/starsky/starsky/clientapp/src/components/atoms/menu-option-modal/menu-option-modal.spec.tsx
@@ -1,4 +1,5 @@
import { fireEvent, render, screen } from "@testing-library/react";
+import { LanguageLocalizationExample } from "../../../interfaces/ILanguageLocalization.ts";
import MenuOptionModal from "./menu-option-modal.tsx";
describe("MenuOption component", () => {
@@ -7,7 +8,7 @@ describe("MenuOption component", () => {
const setEnableMoreMenuMock = jest.fn();
render(
{
);
expect(screen.getByTestId("test")).toBeTruthy();
- expect(screen.getByTestId("test").innerHTML).toBe("Content");
+ expect(screen.getByTestId("test").innerHTML).toBe(LanguageLocalizationExample.en);
});
it("expect child no localisation field", () => {
@@ -44,7 +45,7 @@ describe("MenuOption component", () => {
const setEnableMoreMenuMock = jest.fn();
render(
{
const setEnableMoreMenuMock = jest.fn();
render(
>;
- localization?: { nl: string; en: string };
+ localization?: ILanguageLocalization;
setEnableMoreMenu?: React.Dispatch>;
children?: React.ReactNode;
}
diff --git a/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.spec.tsx b/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.spec.tsx
index b4a5872ac6..e41bffc886 100644
--- a/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.spec.tsx
+++ b/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.spec.tsx
@@ -1,11 +1,12 @@
import { fireEvent, render, screen } from "@testing-library/react";
+import { LanguageLocalizationExample } from "../../../interfaces/ILanguageLocalization";
import MenuOption from "./menu-option";
describe("MenuOption component", () => {
it("expect content", () => {
render(
{}}
testName="test"
isReadOnly={false}
@@ -13,7 +14,7 @@ describe("MenuOption component", () => {
);
expect(screen.getByTestId("test")).toBeTruthy();
- expect(screen.getByTestId("test").innerHTML).toBe("Content");
+ expect(screen.getByTestId("test").innerHTML).toBe(LanguageLocalizationExample.en);
});
it("expect child no localisation field", () => {
@@ -30,7 +31,7 @@ describe("MenuOption component", () => {
it("renders correctly with default props", () => {
render(
{}}
testName="test-menu-option"
isReadOnly={false}
@@ -44,7 +45,7 @@ describe("MenuOption component", () => {
it("renders correctly with custom props", () => {
render(
{}}
testName="test-menu-option1"
isReadOnly={false}
@@ -59,7 +60,7 @@ describe("MenuOption component", () => {
render(
@@ -74,7 +75,7 @@ describe("MenuOption component", () => {
render(
diff --git a/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.tsx b/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.tsx
index d7621712c2..bd180bd4b7 100644
--- a/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.tsx
+++ b/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.tsx
@@ -1,12 +1,13 @@
import React, { memo } from "react";
import useGlobalSettings from "../../../hooks/use-global-settings";
+import { ILanguageLocalization } from "../../../interfaces/ILanguageLocalization";
import { Language } from "../../../shared/language";
interface IMenuOptionProps {
isReadOnly: boolean;
testName: string;
onClickKeydown: () => void;
- localization?: { nl: string; en: string };
+ localization?: ILanguageLocalization;
children?: React.ReactNode;
}
diff --git a/starsky/starsky/clientapp/src/components/atoms/more-menu/more-menu.tsx b/starsky/starsky/clientapp/src/components/atoms/more-menu/more-menu.tsx
index 2abd358c76..bd115587d0 100644
--- a/starsky/starsky/clientapp/src/components/atoms/more-menu/more-menu.tsx
+++ b/starsky/starsky/clientapp/src/components/atoms/more-menu/more-menu.tsx
@@ -1,5 +1,6 @@
import React, { useEffect } from "react";
import useGlobalSettings from "../../../hooks/use-global-settings";
+import localization from "../../../localization/localization.json";
import { Language } from "../../../shared/language";
type MoreMenuPropTypes = {
@@ -17,7 +18,7 @@ const MoreMenu: React.FunctionComponent = ({
}) => {
const settings = useGlobalSettings();
const language = new Language(settings.language);
- const MessageMore = language.text("Meer", "More");
+ const MessageMore = language.key(localization.MessageMore);
const offMoreMenu = () => setEnableMoreMenu(false);
@@ -44,7 +45,8 @@ const MoreMenu: React.FunctionComponent = ({
>
{MessageMore}
-
+
>
);
};
diff --git a/starsky/starsky/clientapp/src/components/molecules/archive-pagination/archive-pagination.tsx b/starsky/starsky/clientapp/src/components/molecules/archive-pagination/archive-pagination.tsx
index e310646146..bb4aec9542 100644
--- a/starsky/starsky/clientapp/src/components/molecules/archive-pagination/archive-pagination.tsx
+++ b/starsky/starsky/clientapp/src/components/molecules/archive-pagination/archive-pagination.tsx
@@ -2,6 +2,7 @@ import React, { memo } from "react";
import useGlobalSettings from "../../../hooks/use-global-settings";
import useLocation from "../../../hooks/use-location/use-location";
import { IRelativeObjects } from "../../../interfaces/IDetailView";
+import localization from "../../../localization/localization.json";
import { Language } from "../../../shared/language";
import { UrlQuery } from "../../../shared/url-query";
import Link from "../../atoms/link/link";
@@ -17,8 +18,8 @@ const ArchivePagination: React.FunctionComponent = memo((props) =
// content
const settings = useGlobalSettings();
const language = new Language(settings.language);
- const MessagePrevious = language.text("Vorige", "Previous");
- const MessageNext = language.text("Volgende", "Next");
+ const MessagePrevious = language.key(localization.MessagePrevious);
+ const MessageNext = language.key(localization.MessageNext);
// used for reading current location
const history = useLocation();
diff --git a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-add-overwrite.tsx b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-add-overwrite.tsx
index 2251879c1f..59bb977abe 100644
--- a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-add-overwrite.tsx
+++ b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-add-overwrite.tsx
@@ -6,6 +6,7 @@ import useLocation from "../../../hooks/use-location/use-location";
import { PageType } from "../../../interfaces/IDetailView";
import { IExifStatus } from "../../../interfaces/IExifStatus";
import { ISidebarUpdate } from "../../../interfaces/ISidebarUpdate";
+import localization from "../../../localization/localization.json";
import { CastToInterface } from "../../../shared/cast-to-interface";
import FetchPost from "../../../shared/fetch/fetch-post";
import { Keyboard } from "../../../shared/keyboard";
@@ -20,24 +21,14 @@ import Preloader from "../../atoms/preloader/preloader";
const ArchiveSidebarLabelEditAddOverwrite: React.FunctionComponent = () => {
const settings = useGlobalSettings();
- const MessageAddName = new Language(settings.language).text("Toevoegen", "Add to");
- const MessageOverwriteName = new Language(settings.language).text("Overschrijven", "Overwrite");
- const MessageTitleName = new Language(settings.language).text("Titel", "Title");
- const MessageErrorReadOnly = new Language(settings.language).text(
- "Eén of meerdere bestanden zijn alleen lezen. " +
- "Alleen de bestanden met schrijfrechten zijn geupdate.",
- "One or more files are read only. " + "Only the files with write permissions have been updated."
- );
- const MessageErrorGenericFail = new Language(settings.language).text(
- "Er is iets misgegaan met het updaten. Probeer het opnieuw",
- "Something went wrong with the update. Please try again"
- );
-
- const MessageErrorNotFoundSourceMissing = new Language(settings.language).text(
- "Eén of meerdere bestanden zijn al verdwenen. " +
- "Alleen de bestanden die wel aanwezig zijn geupdate. Draai een handmatige sync",
- "One or more files are already gone. " +
- "Only the files that are present are updated. Run a manual sync"
+ const language = new Language(settings.language);
+ const MessageAddName = language.key(localization.MessageAddName);
+ const MessageOverwriteName = language.key(localization.MessageOverwriteName);
+ const MessageTitleName = language.key(localization.MessageTitleName);
+ const MessageWriteErrorReadOnly = language.key(localization.MessageWriteErrorReadOnly);
+ const MessageErrorGenericFail = language.key(localization.MessageErrorGenericFail);
+ const MessageErrorNotFoundSourceMissingRunSync = language.key(
+ localization.MessageErrorNotFoundSourceMissingRunSync
);
const history = useLocation();
@@ -107,9 +98,9 @@ const ArchiveSidebarLabelEditAddOverwrite: React.FunctionComponent = () => {
.then((anyData) => {
const result = new CastToInterface().InfoFileIndexArray(anyData.data);
result.forEach((element) => {
- if (element.status === IExifStatus.ReadOnly) setIsError(MessageErrorReadOnly);
+ if (element.status === IExifStatus.ReadOnly) setIsError(MessageWriteErrorReadOnly);
if (element.status === IExifStatus.NotFoundSourceMissing)
- setIsError(MessageErrorNotFoundSourceMissing);
+ setIsError(MessageErrorNotFoundSourceMissingRunSync);
if (element.status === IExifStatus.Ok || element.status === IExifStatus.Deleted) {
dispatch({
type: "update",
diff --git a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-search-replace.tsx b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-search-replace.tsx
index 161ce16fb9..18708c71c7 100644
--- a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-search-replace.tsx
+++ b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-search-replace.tsx
@@ -5,6 +5,7 @@ import useLocation from "../../../hooks/use-location/use-location";
import { PageType } from "../../../interfaces/IDetailView";
import { IExifStatus } from "../../../interfaces/IExifStatus";
import { ISidebarUpdate } from "../../../interfaces/ISidebarUpdate";
+import localization from "../../../localization/localization.json";
import { CastToInterface } from "../../../shared/cast-to-interface";
import FetchPost from "../../../shared/fetch/fetch-post";
import { Language } from "../../../shared/language";
@@ -19,22 +20,14 @@ import Preloader from "../../atoms/preloader/preloader";
const ArchiveSidebarLabelEditSearchReplace: React.FunctionComponent = () => {
const settings = useGlobalSettings();
const language = new Language(settings.language);
- const MessageSearchAndReplaceName = language.text("Zoeken en vervangen", "Search and replace");
- const MessageTitleName = language.text("Titel", "Title");
- const MessageErrorReadOnly = new Language(settings.language).text(
- "Eén of meerdere bestanden zijn alleen lezen. " +
- "Alleen de bestanden met schrijfrechten zijn geupdate.",
- "One or more files are read only. " + "Only the files with write permissions have been updated."
+ const MessageSearchAndReplaceNameLong = language.key(
+ localization.MessageSearchAndReplaceNameLong
);
- const MessageErrorNotFoundSourceMissing = new Language(settings.language).text(
- "Eén of meerdere bestanden zijn al verdwenen. " +
- "Alleen de bestanden die wel aanwezig zijn geupdate. Draai een handmatige sync",
- "One or more files are already gone. " +
- "Only the files that are present are updated. Run a manual sync"
- );
- const MessageErrorGenericFail = new Language(settings.language).text(
- "Er is iets misgegaan met het updaten. Probeer het opnieuw",
- "Something went wrong with the update. Please try again"
+ const MessageTitleName = language.key(localization.MessageTitleName);
+ const MessageWriteErrorReadOnly = language.key(localization.MessageWriteErrorReadOnly);
+ const MessageErrorGenericFail = language.key(localization.MessageErrorGenericFail);
+ const MessageErrorNotFoundSourceMissingRunSync = language.key(
+ localization.MessageErrorNotFoundSourceMissingRunSync
);
const history = useLocation();
@@ -94,9 +87,9 @@ const ArchiveSidebarLabelEditSearchReplace: React.FunctionComponent = () => {
function handleFetchPostResponse(anyData: any) {
const result = new CastToInterface().InfoFileIndexArray(anyData.data);
result.forEach((element) => {
- if (element.status === IExifStatus.ReadOnly) setIsError(MessageErrorReadOnly);
+ if (element.status === IExifStatus.ReadOnly) setIsError(MessageWriteErrorReadOnly);
if (element.status === IExifStatus.NotFoundSourceMissing)
- setIsError(MessageErrorNotFoundSourceMissing);
+ setIsError(MessageErrorNotFoundSourceMissingRunSync);
if (element.status === IExifStatus.Ok || element.status === IExifStatus.Deleted) {
dispatch({
type: "update",
@@ -238,11 +231,11 @@ const ArchiveSidebarLabelEditSearchReplace: React.FunctionComponent = () => {
data-test="replace-button"
onClick={() => pushSearchAndReplace()}
>
- {MessageSearchAndReplaceName}
+ {MessageSearchAndReplaceNameLong}
) : (
)}
>
diff --git a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit.tsx b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit.tsx
index c24f5032f3..177532dea4 100644
--- a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit.tsx
+++ b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit.tsx
@@ -1,6 +1,7 @@
import { useContext, useState } from "react";
import { ArchiveContext } from "../../../contexts/archive-context";
import useGlobalSettings from "../../../hooks/use-global-settings";
+import localization from "../../../localization/localization.json";
import { CastToInterface } from "../../../shared/cast-to-interface";
import { Language } from "../../../shared/language";
import SwitchButton from "../../atoms/switch-button/switch-button";
@@ -10,8 +11,10 @@ import ArchiveSidebarLabelEditSearchReplace from "./archive-sidebar-label-edit-s
const ArchiveSidebarLabelEdit: React.FunctionComponent = () => {
// Content
const settings = useGlobalSettings();
- const MessageModifyName = new Language(settings.language).text("Wijzigen", "Modify");
- const MessageSearchAndReplaceName = new Language(settings.language).text("Vervangen", "Replace");
+ const MessageModifyName = new Language(settings.language).key(localization.MessageModifyName);
+ const MessageSearchAndReplaceName = new Language(settings.language).key(
+ localization.MessageSearchAndReplaceNameShort
+ );
// Toggle
const [replaceMode, setReplaceMode] = useState(false);
diff --git a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-selection-list.tsx b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-selection-list.tsx
index 5ba0e6219a..297af8a6d7 100644
--- a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-selection-list.tsx
+++ b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-selection-list.tsx
@@ -3,6 +3,7 @@ import useGlobalSettings from "../../../hooks/use-global-settings";
import useLocation from "../../../hooks/use-location/use-location";
import { IArchiveProps } from "../../../interfaces/IArchiveProps";
import { IFileIndexItem } from "../../../interfaces/IFileIndexItem";
+import localization from "../../../localization/localization.json";
import { Language } from "../../../shared/language";
import { Select } from "../../../shared/select";
import { URLPath } from "../../../shared/url-path";
@@ -16,8 +17,8 @@ const ArchiveSidebarSelectionList: React.FunctionComponent
setIsDone("");
}, [props.filePath]);
- const MessageColorClassIsUpdated = new Language(settings.language).text(
- "Colorclass is bijgewerkt",
- "Colorclass is updated"
+ const MessageColorClassIsUpdated = new Language(settings.language).key(
+ localization.MessageColorClassIsUpdated
);
useKeyboardEvent(/[0-8]/, (event: KeyboardEvent) => {
diff --git a/starsky/starsky/clientapp/src/components/molecules/color-class-select/color-class-select.tsx b/starsky/starsky/clientapp/src/components/molecules/color-class-select/color-class-select.tsx
index 3f1cb65539..a35d65da5a 100644
--- a/starsky/starsky/clientapp/src/components/molecules/color-class-select/color-class-select.tsx
+++ b/starsky/starsky/clientapp/src/components/molecules/color-class-select/color-class-select.tsx
@@ -1,6 +1,7 @@
import "core-js/modules/es.array.find";
import React, { useEffect, useState } from "react";
import useGlobalSettings from "../../../hooks/use-global-settings";
+import localization from "../../../localization/localization.json";
import { Language } from "../../../shared/language";
import Notification, { NotificationType } from "../../atoms/notification/notification";
import Portal from "../../atoms/portal/portal";
@@ -25,15 +26,15 @@ const ColorClassSelect: React.FunctionComponent = (props
const language = new Language(settings.language);
const colorContent: Array = [
- language.text("Kleurloos", "Colorless"),
- language.text("Roze", "Pink"),
- language.text("Rood", "Red"),
- language.text("Oranje", "Orange"),
- language.text("Geel", "Yellow"),
- language.text("Groen", "Green"),
- language.text("Azuur", "Azure"),
- language.text("Blauw", "Blue"),
- language.text("Grijs", "Grey")
+ language.key(localization.ColorClassColour0),
+ language.key(localization.ColorClassColour1),
+ language.key(localization.ColorClassColour2),
+ language.key(localization.ColorClassColour3),
+ language.key(localization.ColorClassColour4),
+ language.key(localization.ColorClassColour5),
+ language.key(localization.ColorClassColour6),
+ language.key(localization.ColorClassColour7),
+ language.key(localization.ColorClassColour8)
];
const [currentColorClass, setCurrentColorClass] = React.useState(props.currentColorClass);
diff --git a/starsky/starsky/clientapp/src/components/molecules/color-class-select/color-class-update-single.ts b/starsky/starsky/clientapp/src/components/molecules/color-class-select/color-class-update-single.ts
index 0881cc4b3b..deea83a8bb 100644
--- a/starsky/starsky/clientapp/src/components/molecules/color-class-select/color-class-update-single.ts
+++ b/starsky/starsky/clientapp/src/components/molecules/color-class-select/color-class-update-single.ts
@@ -1,5 +1,6 @@
import { IGlobalSettings } from "../../../hooks/use-global-settings";
import { IExifStatus } from "../../../interfaces/IExifStatus";
+import localization from "../../../localization/localization.json";
import { CastToInterface } from "../../../shared/cast-to-interface";
import FetchPost from "../../../shared/fetch/fetch-post";
import { Language, SupportedLanguages } from "../../../shared/language";
@@ -38,13 +39,8 @@ export class ColorClassUpdateSingle {
this.clearAfter = clearAfter;
}
- private getMessageErrorReadOnly() {
- return new Language(this.language).text(
- "Eén of meerdere bestanden zijn alleen lezen. " +
- "Alleen de bestanden met schrijfrechten zijn geupdate.",
- "One or more files are read only. " +
- "Only the files with write permissions have been updated."
- );
+ private getMessageWriteErrorReadOnly() {
+ return new Language(this.language).key(localization.MessageWriteErrorReadOnly);
}
public Update(colorClass: number) {
@@ -67,7 +63,7 @@ export class ColorClassUpdateSingle {
return item.status === IExifStatus.ReadOnly;
})
) {
- this.setIsError(this.getMessageErrorReadOnly());
+ this.setIsError(this.getMessageWriteErrorReadOnly());
return;
}
this.setCurrentColorClass(colorClass);
diff --git a/starsky/starsky/clientapp/src/components/molecules/force-sync-wait-button/force-sync-wait-button.tsx b/starsky/starsky/clientapp/src/components/molecules/force-sync-wait-button/force-sync-wait-button.tsx
index e03eba18f6..cb360e0a1b 100644
--- a/starsky/starsky/clientapp/src/components/molecules/force-sync-wait-button/force-sync-wait-button.tsx
+++ b/starsky/starsky/clientapp/src/components/molecules/force-sync-wait-button/force-sync-wait-button.tsx
@@ -3,6 +3,7 @@ import { ArchiveAction } from "../../../contexts/archive-context";
import useGlobalSettings from "../../../hooks/use-global-settings";
import { IArchiveProps } from "../../../interfaces/IArchiveProps";
import { IConnectionDefault } from "../../../interfaces/IConnectionDefault";
+import localization from "../../../localization/localization.json";
import { CastToInterface } from "../../../shared/cast-to-interface";
import FetchGet from "../../../shared/fetch/fetch-get";
import FetchPost from "../../../shared/fetch/fetch-post";
@@ -64,10 +65,7 @@ const ForceSyncWaitButton: React.FunctionComponent
const settings = useGlobalSettings();
const language = new Language(settings.language);
- const MessageForceSync = language.text(
- "Handmatig synchroniseren van huidige map",
- "Synchronize current directory manually"
- );
+ const MessageForceSyncCurrentFolder = language.key(localization.MessageForceSyncCurrentFolder);
const [startCounter, setStartCounter] = useState(0);
// preloading icon
@@ -101,7 +99,7 @@ const ForceSyncWaitButton: React.FunctionComponent
<>
{isLoading ? : ""}
>
);
diff --git a/starsky/starsky/clientapp/src/components/molecules/health-check-for-updates/health-check-for-updates.tsx b/starsky/starsky/clientapp/src/components/molecules/health-check-for-updates/health-check-for-updates.tsx
index ae333a17c6..d3f7218264 100644
--- a/starsky/starsky/clientapp/src/components/molecules/health-check-for-updates/health-check-for-updates.tsx
+++ b/starsky/starsky/clientapp/src/components/molecules/health-check-for-updates/health-check-for-updates.tsx
@@ -1,5 +1,6 @@
import useFetch from "../../../hooks/use-fetch";
import useGlobalSettings from "../../../hooks/use-global-settings";
+import localization from "../../../localization/localization.json";
import { BrowserDetect } from "../../../shared/browser-detect";
import { DifferenceInDate } from "../../../shared/date";
import { Language } from "../../../shared/language";
@@ -35,24 +36,15 @@ const HealthCheckForUpdates: React.FunctionComponent = () => {
const language = new Language(settings.language);
- const ReleasesUrlToken =
- " {releasesToken}";
- let WhereToFindRelease = language.token(
- ReleasesUrlToken,
+ let WhereToFindRelease = language.key(
+ localization.MessageWhereToFindReleaseReleasesUrlTokenHtml,
["{releasesToken}"],
- [language.text("Ga naar het release overzicht", "Go to the release overview")]
+ [language.key(localization.MessageWhereToFindReleaseReleasesUrlTokenContent)]
);
if (new BrowserDetect().IsElectronApp())
- WhereToFindRelease = language.text(
- "Ga naar het Help menu en dan release overzicht",
- "Go to the release overview"
- );
+ WhereToFindRelease = language.key(localization.WhereToFindReleaseElectronApp);
- const MessageNewVersionUpdateToken = language.text(
- "Er is een nieuwe versie beschikbaar {WhereToFindRelease}",
- "A new version is available {WhereToFindRelease}"
- );
+ const MessageNewVersionUpdateToken = language.key(localization.MessageNewVersionUpdateToken);
const MessageNewVersionUpdateHtml = language.token(
MessageNewVersionUpdateToken,
diff --git a/starsky/starsky/clientapp/src/components/molecules/health-status-error/health-status-error.tsx b/starsky/starsky/clientapp/src/components/molecules/health-status-error/health-status-error.tsx
index 7449cea9bf..bb2e197cda 100644
--- a/starsky/starsky/clientapp/src/components/molecules/health-status-error/health-status-error.tsx
+++ b/starsky/starsky/clientapp/src/components/molecules/health-status-error/health-status-error.tsx
@@ -1,6 +1,7 @@
import useFetch from "../../../hooks/use-fetch";
import useGlobalSettings from "../../../hooks/use-global-settings";
import { IHealthEntry } from "../../../interfaces/IHealthEntry";
+import localization from "../../../localization/localization.json";
import { Language } from "../../../shared/language";
import { UrlQuery } from "../../../shared/url-query";
import Notification, { NotificationType } from "../../atoms/notification/notification";
@@ -9,9 +10,8 @@ const HealthStatusError: React.FunctionComponent = () => {
const healthCheck = useFetch(new UrlQuery().UrlHealthDetails(), "get");
const settings = useGlobalSettings();
- const MessageCriticalErrors = new Language(settings.language).text(
- "Er zijn kritieke fouten in de volgende onderdelen:",
- "There are critical errors in the following components:"
+ const MessageHealthStatusCriticalErrors = new Language(settings.language).key(
+ localization.MessageHealthStatusCriticalErrorsWithTheFollowingComponents
);
if (
@@ -21,7 +21,9 @@ const HealthStatusError: React.FunctionComponent = () => {
)
return null;
- const content: React.JSX.Element[] = [{MessageCriticalErrors}];
+ const content: React.JSX.Element[] = [
+ {MessageHealthStatusCriticalErrors}
+ ];
if (!healthCheck.data?.entries) {
content.push(
diff --git a/starsky/starsky/clientapp/src/components/molecules/item-text-list-view/item-text-list-view.tsx b/starsky/starsky/clientapp/src/components/molecules/item-text-list-view/item-text-list-view.tsx
index 9f6d50b93c..442b209c1e 100644
--- a/starsky/starsky/clientapp/src/components/molecules/item-text-list-view/item-text-list-view.tsx
+++ b/starsky/starsky/clientapp/src/components/molecules/item-text-list-view/item-text-list-view.tsx
@@ -1,6 +1,7 @@
import useGlobalSettings from "../../../hooks/use-global-settings";
import { IExifStatus } from "../../../interfaces/IExifStatus";
import { IFileIndexItem } from "../../../interfaces/IFileIndexItem";
+import localization from "../../../localization/localization.json";
import { Language } from "../../../shared/language";
interface ItemListProps {
@@ -27,7 +28,7 @@ const ItemTextListView: React.FunctionComponent = (props) => {
// Content
const settings = useGlobalSettings();
const language = new Language(settings.language);
- const MessageNoPhotos = language.text("Er zijn geen foto's", "There are no pictures");
+ const MessageNoPhotos = language.key(localization.MessageNoPhotos);
if (!props.fileIndexItems)
return (
diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/internal/inline-search-suggest.spec.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/internal/inline-search-suggest.spec.tsx
index 305d7b0723..3d42704dcd 100644
--- a/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/internal/inline-search-suggest.spec.tsx
+++ b/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/internal/inline-search-suggest.spec.tsx
@@ -69,7 +69,7 @@ describe("inline-search-suggest", () => {
expect(queryByText("Trash")).toBeNull();
});
- it("hides logout menu item when useLocalDesktopUi is enabled", () => {
+ it("hides logout menu item when useLocalDesktop is enabled", () => {
const props2 = {
suggest: [],
setFormFocus: jest.fn(),
@@ -79,7 +79,7 @@ describe("inline-search-suggest", () => {
} as any
},
featuresResult: {
- data: { useLocalDesktopUi: true },
+ data: { useLocalDesktop: true },
statusCode: 200
} as IConnectionDefault,
defaultText: "default text",
diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/internal/inline-search-suggest.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/internal/inline-search-suggest.tsx
index eecc32f94e..bc46cf9607 100644
--- a/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/internal/inline-search-suggest.tsx
+++ b/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/internal/inline-search-suggest.tsx
@@ -24,12 +24,12 @@ const InlineSearchSuggest: React.FunctionComponent =
useEffect(() => {
const dataFeatures = props.featuresResult?.data as IEnvFeatures | undefined;
- if (dataFeatures?.systemTrashEnabled || dataFeatures?.useLocalDesktopUi) {
+ if (dataFeatures?.systemTrashEnabled || dataFeatures?.useLocalDesktop) {
let newMenu = [...defaultMenu];
if (dataFeatures?.systemTrashEnabled) {
newMenu = newMenu.filter((item) => item.key !== "trash");
}
- if (dataFeatures?.useLocalDesktopUi) {
+ if (dataFeatures?.useLocalDesktop) {
newMenu = newMenu.filter((item) => item.key !== "logout");
}
setDefaultMenu([...newMenu]);
@@ -63,6 +63,7 @@ const InlineSearchSuggest: React.FunctionComponent =
name: language.key(localization.MessagePreferences),
url: new UrlQuery().UrlPreferencesPage(),
key: "preferences"
+ // command + shift + k -> see GlobalShortcuts
},
{
name: language.key(localization.MessageLogout),
diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/menu-inline-search.spec.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/menu-inline-search.spec.tsx
index 7b8b724c1b..9c24da2e9e 100644
--- a/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/menu-inline-search.spec.tsx
+++ b/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/menu-inline-search.spec.tsx
@@ -103,12 +103,12 @@ describe("menu-inline-search", () => {
statusCode: 200,
data: {
systemTrashEnabled: true,
- useLocalDesktopUi: false
+ useLocalDesktop: false
} as IEnvFeatures
} as IConnectionDefault;
it("default menu should show logout and trash in default mode", () => {
- dataFeaturesExample.data.useLocalDesktopUi = false;
+ dataFeaturesExample.data.useLocalDesktop = false;
dataFeaturesExample.data.systemTrashEnabled = false;
// usage ==> import * as useFetch from '../hooks/use-fetch';
diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection-no-select-warning/menu-option-desktop-editor-open-selection-no-select-warning.spec.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection-no-select-warning/menu-option-desktop-editor-open-selection-no-select-warning.spec.tsx
new file mode 100644
index 0000000000..2f9293b4fe
--- /dev/null
+++ b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection-no-select-warning/menu-option-desktop-editor-open-selection-no-select-warning.spec.tsx
@@ -0,0 +1,205 @@
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import * as useFetch from "../../../hooks/use-fetch";
+import useGlobalSettings from "../../../hooks/use-global-settings";
+import * as useHotKeys from "../../../hooks/use-keyboard/use-hotkeys";
+import { IConnectionDefault } from "../../../interfaces/IConnectionDefault";
+import { IEnvFeatures } from "../../../interfaces/IEnvFeatures";
+import localization from "../../../localization/localization.json";
+import { Language } from "../../../shared/language";
+import * as Notification from "../../atoms/notification/notification";
+import MenuOptionDesktopEditorOpenSelectionNoSelectWarning from "./menu-option-desktop-editor-open-selection-no-select-warning";
+
+describe("MenuOptionDesktopEditorOpenSelectionNoSelectWarning", () => {
+ it("should render without crashing", () => {
+ render();
+ });
+
+ it("should show error notification when trying to open editor without selecting anything", () => {
+ const notificationSpy = jest.spyOn(Notification, "default").mockImplementationOnce((event) => {
+ return