1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-17 21:26:13 -04:00

Compare commits

...

93 Commits

Author SHA1 Message Date
Weblate
578f95546b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: marudosurdo <marudosurdo@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ja/
Translation: Servarr/Sonarr
2024-07-24 21:34:30 -07:00
ManiMatter
9a613afa35 Treat forcedMetaDL from qBit as queued instead of downloading 2024-07-25 00:33:08 -04:00
Mark McDowall
5ad3d2efcc Fixed: Don't treat SubFrench as French audio language
Closes #6995
2024-07-24 21:32:18 -07:00
Bogdan
1ad722acda Fixed: Improve performance in Select Series Modal 2024-07-25 00:32:09 -04:00
Bogdan
bde5f68142 Refresh series with recently aired episodes with TBA titles
Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>
2024-07-24 21:31:46 -07:00
Bogdan
fbda2d54c7 New: Display original language on series details and search results page
Closes #6984
2024-07-25 00:31:29 -04:00
Bogdan
2a26c6722a New: Ignore Litestream tables in Database 2024-07-25 00:30:56 -04:00
Bogdan
b7dfb8999d Improve tooltip for Next Airing on series Overview 2024-07-24 21:30:27 -07:00
Bogdan
1662521d40 Fixed: Display tag list when sort by tags on series Posters 2024-07-24 21:30:27 -07:00
Weblate
f8d75d174a Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dream <seth.gecko.rr@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translation: Servarr/Sonarr
2024-07-24 21:30:00 -07:00
Bogdan
80ca1a6ac2 Fixed: Editing Quality Profiles 2024-07-18 08:10:43 -07:00
Weblate
f59c0b16ca Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dream <seth.gecko.rr@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/
Translation: Servarr/Sonarr
2024-07-16 21:42:50 -07:00
Mark McDowall
0e95ba2021 New: Allow major version updates to be installed 2024-07-16 21:39:49 -07:00
Mark McDowall
c023fc7008 New: Show update settings on all platforms 2024-07-16 21:39:49 -07:00
Mark McDowall
19466aa290 Fixed: Assume category path from qBittorent starting with '//' is a Windows UNC path
Radarr/Radarr#10162
2024-07-17 00:39:40 -04:00
Mark McDowall
4b5ef4907b Set default value for CustomColonReplacementFormat if not provided 2024-07-16 21:38:26 -07:00
Stevie Robinson
7b8d606a1b New: Wrap specifications in Custom Format and Auto Tagging modals 2024-07-17 00:38:15 -04:00
diamondpete
6a4824c029 Fixed: Remove apostrophe, backtick in contractions 2024-07-17 00:36:29 -04:00
Mark McDowall
1a1c8e6c08 New: Use natural sorting for lists of items in the UI
Closes #6955
2024-07-17 00:34:43 -04:00
Mark McDowall
e35b39b4b1 New: Add option to show tags on series Poster and Overview
Closes #6946
2024-07-16 21:34:25 -07:00
Mark McDowall
d3f14d5f5e Fixed: Parse Chinese anime formats with reverse title order 2024-07-16 21:34:11 -07:00
Mark McDowall
06936c4f22 Fixed: Parsing of Chinese anime with ordinal number in English title 2024-07-16 21:34:11 -07:00
Mark McDowall
0a28ff84e8 Fixed: Parsing of anime with 3 digit number in middle of title 2024-07-16 21:34:11 -07:00
Bogdan
703dee9383 New: Rating votes tooltip and series filter 2024-07-16 21:33:49 -07:00
Marc Carbonell
dca5239420 Remove extraneous indentation in RemoveFileExtension 2024-07-17 00:33:34 -04:00
Mark McDowall
1aaa9a14bc Bump version to 4.0.8 2024-07-15 21:36:26 -07:00
Weblate
c6c37a408a Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dream <seth.gecko.rr@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/
Translation: Servarr/Sonarr
2024-07-15 21:36:15 -07:00
Weblate
ae4a97b4ae Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dream <seth.gecko.rr@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/
Translation: Servarr/Sonarr
2024-07-15 12:07:12 -07:00
Mark McDowall
3afae968eb Fixed: Import queue not processing after incomplete import 2024-07-15 12:03:53 -07:00
Mark McDowall
c01abbf3b5 Add 'On Import Complete' for Discord notifications 2024-07-15 12:03:31 -07:00
Mark McDowall
f5ccf98162 Rename 'On Upgrade' to 'On File Upgrade' 2024-07-15 12:03:31 -07:00
Mark McDowall
6afd3bd344 Bump version to 4.0.7 2024-07-14 16:44:07 -07:00
Weblate
acaf5cd353 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Rauniik <raunerjakub@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translation: Servarr/Sonarr
2024-07-14 16:43:56 -07:00
Sonarr
e97e5bfe8f Automated API Docs update
ignore-downstream
2024-07-09 22:22:48 -07:00
martylukyy
678872b879 Fixed: Parsing of some Web releases 2024-07-10 01:19:07 -04:00
Mark McDowall
10e9735c1c New: Update AutoTags on series update
Closes #6783
2024-07-10 01:02:23 -04:00
Mark McDowall
293a1bc618 New: Custom colon replacement option
Closes #6898
2024-07-10 01:02:04 -04:00
Bogdan
0c883f7886 Fixed: Removing pending release without blocklisting 2024-07-09 22:01:23 -07:00
Mark McDowall
46c7de379c New: Group updates for the same series for Kodi and Emby / Jellyfin 2024-07-09 22:00:57 -07:00
Mark McDowall
a83b521766 New: 'On Import Complete' notification when all episodes in a release are imported
Closes #363
2024-07-09 22:00:57 -07:00
Bogdan
1d06e40acb New: Queued episode count for seasons in series details 2024-07-09 21:59:41 -07:00
Weblate
bfcdc89f6a Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Serhii Matrunchyk <serhii@digitalidea.studio>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/uk/
Translation: Servarr/Sonarr
2024-07-09 21:59:25 -07:00
Sonarr
67943edfbc Automated API Docs update
ignore-downstream
2024-07-05 16:35:21 -07:00
Mark McDowall
04f8595498 Custom Import List improvements
Fixed: Add placeholder title for Custom Import List title
New: Support 'title' property for Custom Import List
2024-07-05 16:35:08 -07:00
Bogdan
81ac73299a Fixed: Bulk series deletion for unmonitored series
Closes #6933
2024-07-05 19:34:56 -04:00
Mark McDowall
a779a5fad2 Fixed: Parsing of anime season releases with 3-digit number in title 2024-07-05 16:34:06 -07:00
Mark McDowall
bfe6a740fa Fixed: Parsing of anime releases using standard numbering
Closes #6925
2024-07-05 16:34:06 -07:00
Mark McDowall
c9ea40b874 New: Parse VFI as French
Closes #6927
2024-07-05 16:34:00 -07:00
Bogdan
4ee0ae1418 Fixed: History with unknown series 2024-07-05 16:33:50 -07:00
Bogdan
ac1da45ecd Fixed: Calculate Custom Formats after user specified options in Manual Import 2024-07-05 19:33:33 -04:00
Weblate
5c327d5be3 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: quek76 <quek@libertysurf.fr>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translation: Servarr/Sonarr
2024-07-05 16:31:56 -07:00
Mark McDowall
55c1ce2e3d Bump version to 4.0.6 2024-06-30 15:42:47 -07:00
Weblate
fd7f0ea973 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Kshitij Burman <kburman6@gmail.com>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: damienmillet <contact@damien-millet.dev>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translation: Servarr/Sonarr
2024-06-30 15:42:41 -07:00
Bogdan
d5dff8e8d6 Fixed: Trimming disabled logs database
Closes #6918
2024-06-30 13:49:41 -04:00
Bogdan
8099ba10af Fixed: Already imported downloads appearing in Queue briefly 2024-06-30 13:47:00 -04:00
Mark McDowall
143ccb1e2a Remove seriesTitle from EpisodeResource
Closes #6841
2024-06-28 06:22:10 -07:00
Mark McDowall
29480d9544 Fixed: Don't use cleaned up release title for release title 2024-06-28 06:22:04 -07:00
Mark McDowall
6de536a7ad Fixed: Limit Queue maximum page size to 200
Closes #6899
2024-06-26 09:45:43 -07:00
Mark McDowall
bce848facf Fixed: Reprocessing items that were previously blocked during importing 2024-06-26 09:45:28 -07:00
Mark McDowall
ea4fe392a0 New: Remove websites in parentheses before parsing 2024-06-25 15:52:24 -07:00
Mark McDowall
45fe585944 Fixed: Prevent errors parsing releases in unexpected formats 2024-06-25 15:52:24 -07:00
Mark McDowall
a0d2933134 New: Ignore Deluge torrents without a title
Closes #6885
2024-06-25 18:52:12 -04:00
Mark McDowall
4c622fd412 New: Ability to select Plex Media Server from plex.tv
Closes #6887
2024-06-25 15:51:57 -07:00
Bogdan
fb060730c7 Fixed: Exclude invalid releases from Newznab and Torznab parsers 2024-06-25 15:51:41 -07:00
Mark McDowall
6d5ff9c4d6 New: Improve UI status when downloads cannot be imported automatically
Closes #6873
2024-06-25 18:51:20 -04:00
Mark McDowall
63bed3e670 New: Parse anime seasons with trailing number in title
Closes #6883
2024-06-25 15:51:03 -07:00
Weblate
e684c10432 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Taylan Tatlı <taylantatli90@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translation: Servarr/Sonarr
2024-06-25 15:50:56 -07:00
Bogdan
d2509798e9 New: Display stats for delete multiple series modal 2024-06-17 20:40:04 -07:00
Bogdan
6c39855ebe Fix UpdatePackageProviderFixture for v4
ignore-downstream
2024-06-17 20:39:33 -07:00
Bogdan
a30e9da767 New: Ignore inaccessible folders when getting folders 2024-06-17 20:39:33 -07:00
Mark McDowall
f8e81396d4 Fixed: Importing from IMDb list 2024-06-17 20:39:22 -07:00
Mark McDowall
7fccf590a8 Fixed: Adding series with unknown items in queue 2024-06-17 23:39:13 -04:00
Stephan Sundermann
e1b937e8d5 New: Add TMDB ID support
Closes #6866
2024-06-17 23:38:41 -04:00
Bogdan
c331c8bd11 Ignore Grabbed from API docs
Run application in docs.sh specific to platform
2024-06-10 20:30:26 -07:00
Mark McDowall
52b72925f9 Fixed: Improve error messaging if config file isn't formatted correctly
Closes #6860
2024-06-10 20:30:13 -07:00
Mark McDowall
378fedcd9d Fixed: Skip invalid series paths during validation 2024-06-10 23:30:03 -04:00
Bogdan
a90ab1a8fd Fixed: Ignore case when resolving indexer by name in release push 2024-06-10 20:29:46 -07:00
Bogdan
0edc5ba99a Fixed: Ignore case for name validation in providers 2024-06-10 20:29:46 -07:00
Bogdan
ea54ade9bf New: Refresh cache for tracked queue on series add 2024-06-10 20:29:41 -07:00
Weblate
e07eb05e8b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: AlbertCoolGuy <Albert.rosenstand@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: xuzhihui <5894940@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-06-10 20:29:28 -07:00
Bogdan
d9b771ab0b Fixed: Error sending Manual Interaction Required when series is unknown 2024-05-31 20:11:31 -04:00
Mark McDowall
6b08e849b8 Search for raw and clean titles for Newznab/Torznab indexers that support raw title searching 2024-05-31 17:10:32 -07:00
Mark McDowall
9c1f48ebc9 Fixed: Include full series title in episode search 2024-05-31 17:10:32 -07:00
Mark McDowall
fd3dd1ab7d New: Genres and Images for Webhooks and Notifiarr
Closes #6822
2024-05-31 17:10:13 -07:00
yammes08
11e5c5a11b Fixed: SDR Files Being Parsed As HLG 2024-05-31 20:09:53 -04:00
Weblate
48f0291884 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Ano10 <arnaudthommeray+github@ik.me>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: r0bertreh <Robert.reh@live.de>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translation: Servarr/Sonarr
2024-05-31 17:09:11 -07:00
Mark McDowall
af0e55aef4 Bump version to 4.0.5 2024-05-29 16:23:16 -07:00
Weblate
39a439eb4c Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Bao Trinh <servarr@baodtrinh.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: mm519897405 <baiya@vip.qq.com>
Co-authored-by: thegamingcat13 <sandervanbeek2004@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-05-29 16:23:04 -07:00
Weblate
66940b283b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: mm519897405 <baiya@vip.qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-05-24 06:21:13 -07:00
Mark McDowall
2a662afaef Fixed: Time for episodes airing today being blank 2024-05-24 06:19:38 -07:00
Weblate
62a9c2519b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: mm519897405 <baiya@vip.qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-05-22 20:09:18 -07:00
Mark McDowall
ca372bee25 Fixed: Queue and Calendar not loading 2024-05-22 20:07:31 -07:00
Sonarr
0904a0737e Automated API Docs update
ignore-downstream
2024-05-21 17:09:20 -07:00
263 changed files with 6286 additions and 1617 deletions

View File

@@ -6,13 +6,13 @@ on:
- develop
- main
paths-ignore:
- 'src/Sonarr.Api.*/openapi.json'
- "src/Sonarr.Api.*/openapi.json"
pull_request:
branches:
- develop
paths-ignore:
- 'src/NzbDrone.Core/Localization/Core/**'
- 'src/Sonarr.Api.*/openapi.json'
- "src/NzbDrone.Core/Localization/Core/**"
- "src/Sonarr.Api.*/openapi.json"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -22,7 +22,7 @@ env:
FRAMEWORK: net6.0
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
SONARR_MAJOR_VERSION: 4
VERSION: 4.0.4
VERSION: 4.0.8
jobs:
backend:
@@ -32,105 +32,105 @@ jobs:
major_version: ${{ steps.variables.outputs.major_version }}
version: ${{ steps.variables.outputs.version }}
steps:
- name: Check out
uses: actions/checkout@v4
- name: Check out
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
- name: Setup Environment Variables
id: variables
shell: bash
run: |
# Add 800 to the build number because GitHub won't let us pick an arbitrary starting point
SONARR_VERSION="${{ env.VERSION }}.$((${{ github.run_number }}+800))"
DOTNET_VERSION=$(jq -r '.sdk.version' global.json)
- name: Setup Environment Variables
id: variables
shell: bash
run: |
# Add 800 to the build number because GitHub won't let us pick an arbitrary starting point
SONARR_VERSION="${{ env.VERSION }}.$((${{ github.run_number }}+800))"
DOTNET_VERSION=$(jq -r '.sdk.version' global.json)
echo "SDK_PATH=${{ env.DOTNET_ROOT }}/sdk/${DOTNET_VERSION}" >> "$GITHUB_ENV"
echo "SONARR_VERSION=$SONARR_VERSION" >> "$GITHUB_ENV"
echo "BRANCH=${RAW_BRANCH_NAME//\//-}" >> "$GITHUB_ENV"
echo "SDK_PATH=${{ env.DOTNET_ROOT }}/sdk/${DOTNET_VERSION}" >> "$GITHUB_ENV"
echo "SONARR_VERSION=$SONARR_VERSION" >> "$GITHUB_ENV"
echo "BRANCH=${RAW_BRANCH_NAME//\//-}" >> "$GITHUB_ENV"
echo "framework=${{ env.FRAMEWORK }}" >> "$GITHUB_OUTPUT"
echo "major_version=${{ env.SONARR_MAJOR_VERSION }}" >> "$GITHUB_OUTPUT"
echo "version=$SONARR_VERSION" >> "$GITHUB_OUTPUT"
echo "framework=${{ env.FRAMEWORK }}" >> "$GITHUB_OUTPUT"
echo "major_version=${{ env.SONARR_MAJOR_VERSION }}" >> "$GITHUB_OUTPUT"
echo "version=$SONARR_VERSION" >> "$GITHUB_OUTPUT"
- name: Enable Extra Platforms In SDK
shell: bash
run: ./build.sh --enable-extra-platforms-in-sdk
- name: Enable Extra Platforms In SDK
shell: bash
run: ./build.sh --enable-extra-platforms-in-sdk
- name: Build Backend
shell: bash
run: ./build.sh --backend --enable-extra-platforms --packages
- name: Build Backend
shell: bash
run: ./build.sh --backend --enable-extra-platforms --packages
# Test Artifacts
# Test Artifacts
- name: Publish win-x64 Test Artifact
uses: ./.github/actions/publish-test-artifact
with:
framework: ${{ env.FRAMEWORK }}
runtime: win-x64
- name: Publish win-x64 Test Artifact
uses: ./.github/actions/publish-test-artifact
with:
framework: ${{ env.FRAMEWORK }}
runtime: win-x64
- name: Publish linux-x64 Test Artifact
uses: ./.github/actions/publish-test-artifact
with:
framework: ${{ env.FRAMEWORK }}
runtime: linux-x64
- name: Publish linux-x64 Test Artifact
uses: ./.github/actions/publish-test-artifact
with:
framework: ${{ env.FRAMEWORK }}
runtime: linux-x64
- name: Publish osx-arm64 Test Artifact
uses: ./.github/actions/publish-test-artifact
with:
framework: ${{ env.FRAMEWORK }}
runtime: osx-arm64
- name: Publish osx-arm64 Test Artifact
uses: ./.github/actions/publish-test-artifact
with:
framework: ${{ env.FRAMEWORK }}
runtime: osx-arm64
# Build Artifacts (grouped by OS)
- name: Publish FreeBSD Artifact
uses: actions/upload-artifact@v4
with:
name: build_freebsd
path: _artifacts/freebsd-*/**/*
- name: Publish Linux Artifact
uses: actions/upload-artifact@v4
with:
name: build_linux
path: _artifacts/linux-*/**/*
- name: Publish macOS Artifact
uses: actions/upload-artifact@v4
with:
name: build_macos
path: _artifacts/osx-*/**/*
- name: Publish Windows Artifact
uses: actions/upload-artifact@v4
with:
name: build_windows
path: _artifacts/win-*/**/*
# Build Artifacts (grouped by OS)
- name: Publish FreeBSD Artifact
uses: actions/upload-artifact@v4
with:
name: build_freebsd
path: _artifacts/freebsd-*/**/*
- name: Publish Linux Artifact
uses: actions/upload-artifact@v4
with:
name: build_linux
path: _artifacts/linux-*/**/*
- name: Publish macOS Artifact
uses: actions/upload-artifact@v4
with:
name: build_macos
path: _artifacts/osx-*/**/*
- name: Publish Windows Artifact
uses: actions/upload-artifact@v4
with:
name: build_windows
path: _artifacts/win-*/**/*
frontend:
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v4
- name: Check out
uses: actions/checkout@v4
- name: Volta
uses: volta-cli/action@v4
- name: Volta
uses: volta-cli/action@v4
- name: Yarn Install
run: yarn install
- name: Yarn Install
run: yarn install
- name: Lint
run: yarn lint
- name: Lint
run: yarn lint
- name: Stylelint
run: yarn stylelint -f github
- name: Stylelint
run: yarn stylelint -f github
- name: Build
run: yarn build --env production
- name: Build
run: yarn build --env production
- name: Publish UI Artifact
uses: actions/upload-artifact@v4
with:
name: build_ui
path: _output/UI/**/*
- name: Publish UI Artifact
uses: actions/upload-artifact@v4
with:
name: build_ui
path: _output/UI/**/*
unit_test:
needs: backend
@@ -150,32 +150,32 @@ jobs:
filter: TestCategory!=ManualTest&TestCategory!=LINUX&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
runs-on: ${{ matrix.os }}
steps:
- name: Check out
uses: actions/checkout@v4
- name: Check out
uses: actions/checkout@v4
- name: Test
uses: ./.github/actions/test
with:
os: ${{ matrix.os }}
artifact: ${{ matrix.artifact }}
pattern: Sonarr.*.Test.dll
filter: ${{ matrix.filter }}
- name: Test
uses: ./.github/actions/test
with:
os: ${{ matrix.os }}
artifact: ${{ matrix.artifact }}
pattern: Sonarr.*.Test.dll
filter: ${{ matrix.filter }}
unit_test_postgres:
needs: backend
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v4
- name: Check out
uses: actions/checkout@v4
- name: Test
uses: ./.github/actions/test
with:
os: ubuntu-latest
artifact: tests-linux-x64
pattern: Sonarr.*.Test.dll
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
use_postgres: true
- name: Test
uses: ./.github/actions/test
with:
os: ubuntu-latest
artifact: tests-linux-x64
pattern: Sonarr.*.Test.dll
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
use_postgres: true
integration_test:
needs: backend
@@ -201,19 +201,19 @@ jobs:
binary_path: win-x64/${{ needs.backend.outputs.framework }}/Sonarr
runs-on: ${{ matrix.os }}
steps:
- name: Check out
uses: actions/checkout@v4
- name: Check out
uses: actions/checkout@v4
- name: Test
uses: ./.github/actions/test
with:
os: ${{ matrix.os }}
artifact: ${{ matrix.artifact }}
pattern: Sonarr.*.Test.dll
filter: ${{ matrix.filter }}
integration_tests: true
binary_artifact: ${{ matrix.binary_artifact }}
binary_path: ${{ matrix.binary_path }}
- name: Test
uses: ./.github/actions/test
with:
os: ${{ matrix.os }}
artifact: ${{ matrix.artifact }}
pattern: Sonarr.*.Test.dll
filter: ${{ matrix.filter }}
integration_tests: true
binary_artifact: ${{ matrix.binary_artifact }}
binary_path: ${{ matrix.binary_path }}
deploy:
if: ${{ github.ref_name == 'develop' || github.ref_name == 'main' }}
@@ -228,7 +228,15 @@ jobs:
notify:
name: Discord Notification
needs: [backend, frontend, unit_test, unit_test_postgres, integration_test, deploy]
needs:
[
backend,
frontend,
unit_test,
unit_test_postgres,
integration_test,
deploy,
]
if: ${{ !cancelled() && (github.ref_name == 'develop' || github.ref_name == 'main') }}
env:
STATUS: ${{ contains(needs.*.result, 'failure') && 'failure' || 'success' }}
@@ -239,10 +247,10 @@ jobs:
uses: tsickert/discord-webhook@v6.0.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
username: 'GitHub Actions'
avatar-url: 'https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png'
username: "GitHub Actions"
avatar-url: "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"
embed-title: "${{ github.workflow }}: ${{ env.STATUS == 'success' && 'Success' || 'Failure' }}"
embed-url: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}'
embed-url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
embed-description: |
**Branch** ${{ github.ref }}
**Build** ${{ needs.backend.outputs.version }}

12
docs.sh
View File

@@ -25,17 +25,23 @@ slnFile=src/Sonarr.sln
platform=Posix
if [ "$PLATFORM" = "Windows" ]; then
application=Sonarr.Console.dll
else
application=Sonarr.dll
fi
dotnet clean $slnFile -c Debug
dotnet clean $slnFile -c Release
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
dotnet new tool-manifest
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
dotnet tool run swagger tofile --output ./src/Sonarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/Sonarr.dll" v3 &
dotnet tool run swagger tofile --output ./src/Sonarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v3 &
sleep 30
sleep 45
kill %1

View File

@@ -77,7 +77,7 @@ class HistoryRow extends Component {
onMarkAsFailedPress
} = this.props;
if (!episode) {
if (!series || !episode) {
return null;
}

View File

@@ -217,6 +217,7 @@ class Queue extends Component {
>
<TableOptionsModalWrapper
columns={columns}
maxPageSize={200}
{...otherProps}
optionsComponent={QueueOptionsConnector}
>

View File

@@ -70,6 +70,11 @@ function QueueStatus(props) {
iconName = icons.DOWNLOADED;
title = translate('Downloaded');
if (trackedDownloadState === 'importBlocked') {
title += ` - ${translate('UnableToImportAutomatically')}`;
iconKind = kinds.WARNING;
}
if (trackedDownloadState === 'importPending') {
title += ` - ${translate('WaitingToImport')}`;
iconKind = kinds.PURPLE;

View File

@@ -118,6 +118,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
{
key: 'blocklistAndSearch',
value: translate('BlocklistAndSearch'),
isDisabled: isPending,
hint: multipleSelected
? translate('BlocklistAndSearchMultipleHint')
: translate('BlocklistAndSearchHint'),
@@ -130,7 +131,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
: translate('BlocklistOnlyHint'),
},
];
}, [multipleSelected]);
}, [isPending, multipleSelected]);
const handleRemovalMethodChange = useCallback(
({ value }: { value: RemovalMethod }) => {

View File

@@ -24,7 +24,11 @@ function TimeleftCell(props) {
} = props;
if (status === 'delay') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
const date = getRelativeDate({
date: estimatedCompletionTime,
shortDateFormat,
showRelativeDates
});
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return (
@@ -40,7 +44,11 @@ function TimeleftCell(props) {
}
if (status === 'downloadClientUnavailable') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
const date = getRelativeDate({
date: estimatedCompletionTime,
shortDateFormat,
showRelativeDates
});
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return (

View File

@@ -69,6 +69,11 @@
height: 55px;
}
.originalLanguageName,
.network {
margin-left: 8px;
}
.tvdbLink {
composes: link from '~Components/Link/Link.css';

View File

@@ -4,6 +4,8 @@ interface CssExports {
'alreadyExistsIcon': string;
'content': string;
'icons': string;
'network': string;
'originalLanguageName': string;
'overlay': string;
'overview': string;
'poster': string;

View File

@@ -55,6 +55,7 @@ class AddNewSeriesSearchResult extends Component {
titleSlug,
year,
network,
originalLanguage,
status,
overview,
statistics,
@@ -145,14 +146,37 @@ class AddNewSeriesSearchResult extends Component {
<Label size={sizes.LARGE}>
<HeartRating
rating={ratings.value}
votes={ratings.votes}
iconSize={13}
/>
</Label>
{
originalLanguage?.name ?
<Label size={sizes.LARGE}>
<Icon
name={icons.LANGUAGE}
size={13}
/>
<span className={styles.originalLanguageName}>
{originalLanguage.name}
</span>
</Label> :
null
}
{
network ?
<Label size={sizes.LARGE}>
{network}
<Icon
name={icons.NETWORK}
size={13}
/>
<span className={styles.network}>
{network}
</span>
</Label> :
null
}
@@ -218,6 +242,7 @@ AddNewSeriesSearchResult.propTypes = {
titleSlug: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
network: PropTypes.string,
originalLanguage: PropTypes.object,
status: PropTypes.string.isRequired,
overview: PropTypes.string,
statistics: PropTypes.object.isRequired,

View File

@@ -30,7 +30,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
import Logs from 'System/Logs/Logs';
import Status from 'System/Status/Status';
import Tasks from 'System/Tasks/Tasks';
import UpdatesConnector from 'System/Updates/UpdatesConnector';
import Updates from 'System/Updates/Updates';
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
import MissingConnector from 'Wanted/Missing/MissingConnector';
@@ -248,7 +248,7 @@ function AppRoutes(props) {
<Route
path="/system/updates"
component={UpdatesConnector}
component={Updates}
/>
<Route

View File

@@ -46,6 +46,7 @@ export interface CustomFilter {
}
export interface AppSectionState {
version: string;
dimensions: {
isSmallScreen: boolean;
width: number;

View File

@@ -20,6 +20,7 @@ export interface SeriesIndexAppState {
showTitle: boolean;
showMonitored: boolean;
showQualityProfile: boolean;
showTags: boolean;
showSearchAction: boolean;
};
@@ -34,6 +35,7 @@ export interface SeriesIndexAppState {
showSeasonCount: boolean;
showPath: boolean;
showSizeOnDisk: boolean;
showTags: boolean;
showSearchAction: boolean;
};

View File

@@ -14,13 +14,16 @@ import Indexer from 'typings/Indexer';
import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification';
import QualityProfile from 'typings/QualityProfile';
import { UiSettings } from 'typings/UiSettings';
import General from 'typings/Settings/General';
import UiSettings from 'typings/Settings/UiSettings';
export interface DownloadClientAppState
extends AppSectionState<DownloadClient>,
AppSectionDeleteState,
AppSectionSaveState {}
export type GeneralAppState = AppSectionItemState<General>;
export interface ImportListAppState
extends AppSectionState<ImportList>,
AppSectionDeleteState,
@@ -58,6 +61,7 @@ export type UiSettingsAppState = AppSectionItemState<UiSettings>;
interface SettingsAppState {
advancedSettings: boolean;
downloadClients: DownloadClientAppState;
general: GeneralAppState;
importListExclusions: ImportListExclusionsSettingsAppState;
importListOptions: ImportListOptionsSettingsAppState;
importLists: ImportListAppState;

View File

@@ -1,9 +1,12 @@
import SystemStatus from 'typings/SystemStatus';
import { AppSectionItemState } from './AppSectionState';
import Update from 'typings/Update';
import AppSectionState, { AppSectionItemState } from './AppSectionState';
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
export type UpdateAppState = AppSectionState<Update>;
interface SystemAppState {
updates: UpdateAppState;
status: SystemStatusAppState;
}

View File

@@ -28,7 +28,7 @@ class DayOfWeek extends Component {
if (view === calendarViews.WEEK) {
formatedDate = momentDate.format(calendarWeekColumnHeader);
} else if (view === calendarViews.FORECAST) {
formatedDate = getRelativeDate(date, shortDateFormat, showRelativeDates);
formatedDate = getRelativeDate({ date, shortDateFormat, showRelativeDates });
}
return (

View File

@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import SelectInput from 'Components/Form/SelectInput';
import IconButton from 'Components/Link/IconButton';
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
import sortByProp from 'Utilities/Array/sortByProp';
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
@@ -224,7 +225,7 @@ class FilterBuilderRow extends Component {
key: name,
value: typeof label === 'function' ? label() : label
};
}).sort((a, b) => a.value.localeCompare(b.value));
}).sort(sortByProp('value'));
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);

View File

@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { filterBuilderTypes } from 'Helpers/Props';
import * as filterTypes from 'Helpers/Props/filterTypes';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import FilterBuilderRowValue from './FilterBuilderRowValue';
function createTagListSelector() {
@@ -38,7 +38,7 @@ function createTagListSelector() {
}
return acc;
}, []).sort(sortByName);
}, []).sort(sortByProp('name'));
}
return _.uniqBy(items, 'id');

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { useSelector } from 'react-redux';
import Series from 'Series/Series';
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import FilterBuilderRowValue from './FilterBuilderRowValue';
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
@@ -11,7 +11,7 @@ function SeriesFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
const tagList = allSeries
.map((series) => ({ id: series.id, name: series.title }))
.sort(sortByName);
.sort(sortByProp('name'));
return <FilterBuilderRowValue {...props} tagList={tagList} />;
}

View File

@@ -5,6 +5,7 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import CustomFilter from './CustomFilter';
import styles from './CustomFiltersModalContent.css';
@@ -31,7 +32,7 @@ function CustomFiltersModalContent(props) {
<ModalBody>
{
customFilters
.sort((a, b) => a.label.localeCompare(b.label))
.sort((a, b) => sortByProp(a, b, 'label'))
.map((customFilter) => {
return (
<CustomFilter

View File

@@ -4,7 +4,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import EnhancedSelectInput from './EnhancedSelectInput';
@@ -23,7 +23,7 @@ function createMapStateToProps() {
const filteredItems = items.filter((item) => item.protocol === protocolFilter);
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
const values = _.map(filteredItems.sort(sortByProp('name')), (downloadClient) => {
return {
key: downloadClient.id,
value: downloadClient.name,

View File

@@ -271,26 +271,32 @@ class EnhancedSelectInput extends Component {
this.setState({ isOpen: !this.state.isOpen });
};
onSelect = (value) => {
if (Array.isArray(this.props.value)) {
let newValue = null;
const index = this.props.value.indexOf(value);
onSelect = (newValue) => {
const { name, value, values, onChange } = this.props;
const additionalProperties = values.find((v) => v.key === newValue)?.additionalProperties;
if (Array.isArray(value)) {
let arrayValue = null;
const index = value.indexOf(newValue);
if (index === -1) {
newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v));
arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v));
} else {
newValue = [...this.props.value];
newValue.splice(index, 1);
arrayValue = [...value];
arrayValue.splice(index, 1);
}
this.props.onChange({
name: this.props.name,
value: newValue
onChange({
name,
value: arrayValue,
additionalProperties
});
} else {
this.setState({ isOpen: false });
this.props.onChange({
name: this.props.name,
value
onChange({
name,
value: newValue,
additionalProperties
});
}
};
@@ -485,7 +491,7 @@ class EnhancedSelectInput extends Component {
values.map((v, index) => {
const hasParent = v.parentKey !== undefined;
const depth = hasParent ? 1 : 0;
const parentSelected = hasParent && value.includes(v.parentKey);
const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey);
return (
<OptionComponent
key={v.key}

View File

@@ -9,7 +9,8 @@ import EnhancedSelectInput from './EnhancedSelectInput';
const importantFieldNames = [
'baseUrl',
'apiPath',
'apiKey'
'apiKey',
'authToken'
];
function getProviderDataKey(providerData) {
@@ -34,7 +35,9 @@ function getSelectOptions(items) {
key: option.value,
value: option.name,
hint: option.hint,
parentKey: option.parentValue
parentKey: option.parentValue,
isDisabled: option.isDisabled,
additionalProperties: option.additionalProperties
};
});
}
@@ -147,7 +150,7 @@ EnhancedSelectInputConnector.propTypes = {
provider: PropTypes.string.isRequired,
providerData: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
selectOptionsProviderAction: PropTypes.string,
onChange: PropTypes.func.isRequired,

View File

@@ -4,7 +4,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchIndexers } from 'Store/Actions/settingsActions';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import EnhancedSelectInput from './EnhancedSelectInput';
@@ -20,7 +20,7 @@ function createMapStateToProps() {
items
} = indexers;
const values = _.map(items.sort(sortByName), (indexer) => {
const values = _.map(items.sort(sortByProp('name')), (indexer) => {
return {
key: indexer.id,
value: indexer.name

View File

@@ -4,13 +4,13 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.qualityProfiles', sortByName),
createSortedSectionSelector('settings.qualityProfiles', sortByProp('name')),
(state, { includeNoChange }) => includeNoChange,
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
(state, { includeMixed }) => includeMixed,

View File

@@ -1,29 +1,40 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import { icons } from 'Helpers/Props';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './HeartRating.css';
function HeartRating({ rating, iconSize }) {
function HeartRating({ rating, votes, iconSize }) {
return (
<span className={styles.rating}>
<Icon
className={styles.heart}
name={icons.HEART}
size={iconSize}
/>
<Tooltip
anchor={
<span className={styles.rating}>
<Icon
className={styles.heart}
name={icons.HEART}
size={iconSize}
/>
{rating * 10}%
</span>
{rating * 10}%
</span>
}
tooltip={translate('CountVotes', { votes })}
kind={kinds.INVERSE}
position={tooltipPositions.TOP}
/>
);
}
HeartRating.propTypes = {
rating: PropTypes.number.isRequired,
votes: PropTypes.number.isRequired,
iconSize: PropTypes.number.isRequired
};
HeartRating.defaultProps = {
votes: 0,
iconSize: 14
};

View File

@@ -88,6 +88,15 @@
}
}
.purple {
border-color: var(--purple);
background-color: var(--purple);
&.outline {
color: var(--purple);
}
}
/** Sizes **/
.small {

View File

@@ -11,6 +11,7 @@ interface CssExports {
'medium': string;
'outline': string;
'primary': string;
'purple': string;
'small': string;
'success': string;
'warning': string;

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import FilterMenuItem from './FilterMenuItem';
import MenuContent from './MenuContent';
@@ -47,7 +48,7 @@ class FilterMenuContent extends Component {
{
customFilters
.sort((a, b) => a.label.localeCompare(b.label))
.sort(sortByProp('label'))
.map((filter) => {
return (
<FilterMenuItem

View File

@@ -21,6 +21,7 @@ function createCleanSeriesSelector() {
tvdbId,
tvMazeId,
imdbId,
tmdbId,
tags = []
} = series;
@@ -33,6 +34,7 @@ function createCleanSeriesSelector() {
tvdbId,
tvMazeId,
imdbId,
tmdbId,
firstCharacter: title.charAt(0).toLowerCase(),
tags: tags.reduce((acc, id) => {
const matchingTag = allTags.find((tag) => tag.id === id);

View File

@@ -14,6 +14,7 @@ function SeriesSearchResult(props) {
tvdbId,
tvMazeId,
imdbId,
tmdbId,
tags
} = props;
@@ -73,6 +74,14 @@ function SeriesSearchResult(props) {
null
}
{
match.key === 'tmdbId' && tmdbId ?
<div className={styles.alternateTitle}>
TmdbId: {tmdbId}
</div> :
null
}
{
tag ?
<div className={styles.tagContainer}>
@@ -97,6 +106,7 @@ SeriesSearchResult.propTypes = {
tvdbId: PropTypes.number,
tvMazeId: PropTypes.number,
imdbId: PropTypes.string,
tmdbId: PropTypes.number,
tags: PropTypes.arrayOf(PropTypes.object).isRequired,
match: PropTypes.object.isRequired
};

View File

@@ -13,6 +13,7 @@ const fuseOptions = {
'tvdbId',
'tvMazeId',
'imdbId',
'tmdbId',
'tags.label'
]
};

View File

@@ -40,7 +40,7 @@ class RelativeDateCell extends PureComponent {
title={formatDateTime(date, longDateFormat, timeFormat, { includeSeconds, includeRelativeDay: !showRelativeDates })}
{...otherProps}
>
{getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, includeTime, timeForToday: true })}
{getRelativeDate({ date, shortDateFormat, showRelativeDates, timeFormat, includeSeconds, includeTime, timeForToday: true })}
</Component>
);
}

View File

@@ -49,11 +49,12 @@ class TableOptionsModal extends Component {
onPageSizeChange = ({ value }) => {
let pageSizeError = null;
const maxPageSize = this.props.maxPageSize ?? 250;
if (value < 5) {
pageSizeError = translate('TablePageSizeMinimum', { minimumValue: '5' });
} else if (value > 250) {
pageSizeError = translate('TablePageSizeMaximum', { maximumValue: '250' });
} else if (value > maxPageSize) {
pageSizeError = translate('TablePageSizeMaximum', { maximumValue: `${maxPageSize}` });
} else {
this.props.onTableOptionChange({ pageSize: value });
}
@@ -248,6 +249,7 @@ TableOptionsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
pageSize: PropTypes.number,
maxPageSize: PropTypes.number,
canModifyColumns: PropTypes.bool.isRequired,
optionsComponent: PropTypes.elementType,
onTableOptionChange: PropTypes.func.isRequired,

View File

@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import { kinds } from 'Helpers/Props';
import sortByProp from 'Utilities/Array/sortByProp';
import Label from './Label';
import styles from './TagList.css';
@@ -8,7 +9,7 @@ function TagList({ tags, tagList }) {
const sortedTags = tags
.map((tagId) => tagList.find((tag) => tag.id === tagId))
.filter((tag) => !!tag)
.sort((a, b) => a.label.localeCompare(b.label));
.sort(sortByProp('label'));
return (
<div className={styles.tags}>

View File

@@ -69,6 +69,7 @@ import {
faHistory as fasHistory,
faHome as fasHome,
faInfoCircle as fasInfoCircle,
faLanguage as fasLanguage,
faLaptop as fasLaptop,
faLevelUpAlt as fasLevelUpAlt,
faListCheck as fasListCheck,
@@ -168,6 +169,7 @@ export const IGNORE = fasTimesCircle;
export const INFO = fasInfoCircle;
export const INTERACTIVE = fasUser;
export const KEYBOARD = farKeyboard;
export const LANGUAGE = fasLanguage;
export const LOGOUT = fasSignOutAlt;
export const MANAGE = fasListCheck;
export const MEDIA_INFO = farFileInvoice;

View File

@@ -21,6 +21,7 @@ import { scrollDirections } from 'Helpers/Props';
import Series from 'Series/Series';
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
import dimensions from 'Styles/Variables/dimensions';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import SelectSeriesModalTableHeader from './SelectSeriesModalTableHeader';
import SelectSeriesRow from './SelectSeriesRow';
@@ -162,18 +163,21 @@ function SelectSeriesModalContent(props: SelectSeriesModalContentProps) {
[allSeries, onSeriesSelect]
);
const items = useMemo(() => {
const sorted = [...allSeries].sort((a, b) =>
a.sortTitle.localeCompare(b.sortTitle)
);
const sortedSeries = useMemo(
() => [...allSeries].sort(sortByProp('sortTitle')),
[allSeries]
);
return sorted.filter(
(item) =>
item.title.toLowerCase().includes(filter.toLowerCase()) ||
item.tvdbId.toString().includes(filter) ||
item.imdbId?.includes(filter)
);
}, [allSeries, filter]);
const items = useMemo(
() =>
sortedSeries.filter(
(item) =>
item.title.toLowerCase().includes(filter.toLowerCase()) ||
item.tvdbId.toString().includes(filter) ||
item.imdbId?.includes(filter)
),
[sortedSeries, filter]
);
return (
<ModalContent onModalClose={onModalClose}>

View File

@@ -14,4 +14,9 @@
.deleteFilesMessage {
margin-top: 20px;
color: var(--dangerColor);
.deleteCount {
margin-top: 20px;
color: var(--warningColor);
}
}

View File

@@ -1,6 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'deleteCount': string;
'deleteFilesMessage': string;
'folderPath': string;
'pathContainer': string;

View File

@@ -50,15 +50,15 @@ class DeleteSeriesModalContent extends Component {
const {
title,
path,
statistics,
statistics = {},
deleteOptions,
onModalClose,
onDeleteOptionChange
} = this.props;
const {
episodeFileCount,
sizeOnDisk
episodeFileCount = 0,
sizeOnDisk = 0
} = statistics;
const deleteFiles = this.state.deleteFiles;
@@ -108,16 +108,20 @@ class DeleteSeriesModalContent extends Component {
</FormGroup>
{
deleteFiles &&
deleteFiles ?
<div className={styles.deleteFilesMessage}>
<div><InlineMarkdown data={translate('DeleteSeriesFolderConfirmation', { path })} blockClassName={styles.folderPath} /></div>
{
!!episodeFileCount &&
<div>{translate('DeleteSeriesFolderEpisodeCount', { episodeFileCount, size: formatBytes(sizeOnDisk) })}</div>
}
</div>
}
{
episodeFileCount ?
<div className={styles.deleteCount}>
{translate('DeleteSeriesFolderEpisodeCount', { episodeFileCount, size: formatBytes(sizeOnDisk) })}
</div> :
null
}
</div> :
null
}
</ModalBody>
<ModalFooter>

View File

@@ -0,0 +1,69 @@
import React from 'react';
import { useSelector } from 'react-redux';
import Label from 'Components/Label';
import { kinds, sizes } from 'Helpers/Props';
import createSeriesQueueItemsDetailsSelector, {
SeriesQueueDetails,
} from 'Series/Index/createSeriesQueueDetailsSelector';
function getEpisodeCountKind(
monitored: boolean,
episodeFileCount: number,
episodeCount: number,
isDownloading: boolean
) {
if (isDownloading) {
return kinds.PURPLE;
}
if (episodeFileCount === episodeCount && episodeCount > 0) {
return kinds.SUCCESS;
}
if (!monitored) {
return kinds.WARNING;
}
return kinds.DANGER;
}
interface SeasonProgressLabelProps {
seriesId: number;
seasonNumber: number;
monitored: boolean;
episodeCount: number;
episodeFileCount: number;
}
function SeasonProgressLabel({
seriesId,
seasonNumber,
monitored,
episodeCount,
episodeFileCount,
}: SeasonProgressLabelProps) {
const queueDetails: SeriesQueueDetails = useSelector(
createSeriesQueueItemsDetailsSelector(seriesId, seasonNumber)
);
const newDownloads = queueDetails.count - queueDetails.episodesWithFiles;
const text = newDownloads
? `${episodeFileCount} + ${newDownloads} / ${episodeCount}`
: `${episodeFileCount} / ${episodeCount}`;
return (
<Label
kind={getEpisodeCountKind(
monitored,
episodeFileCount,
episodeCount,
queueDetails.count > 0
)}
size={sizes.LARGE}
>
<span>{text}</span>
</Label>
);
}
export default SeasonProgressLabel;

View File

@@ -129,6 +129,7 @@
.path,
.sizeOnDisk,
.qualityProfileName,
.originalLanguageName,
.network,
.links,
.tags {

View File

@@ -15,6 +15,7 @@ interface CssExports {
'links': string;
'monitorToggleButton': string;
'network': string;
'originalLanguageName': string;
'overview': string;
'path': string;
'poster': string;

View File

@@ -175,6 +175,7 @@ class SeriesDetails extends Component {
tvdbId,
tvMazeId,
imdbId,
tmdbId,
title,
runtime,
ratings,
@@ -184,6 +185,7 @@ class SeriesDetails extends Component {
monitored,
status,
network,
originalLanguage,
overview,
images,
seasons,
@@ -411,10 +413,12 @@ class SeriesDetails extends Component {
ratings.value ?
<HeartRating
rating={ratings.value}
votes={ratings.votes}
iconSize={20}
/> :
null
}
<SeriesGenres genres={genres} />
<span>
@@ -428,7 +432,6 @@ class SeriesDetails extends Component {
className={styles.detailsLabel}
size={sizes.LARGE}
>
<div>
<Icon
name={icons.FOLDER}
@@ -446,7 +449,6 @@ class SeriesDetails extends Component {
className={styles.detailsLabel}
size={sizes.LARGE}
>
<div>
<Icon
name={icons.DRIVE}
@@ -474,7 +476,6 @@ class SeriesDetails extends Component {
title={translate('QualityProfile')}
size={sizes.LARGE}
>
<div>
<Icon
name={icons.PROFILE}
@@ -494,7 +495,6 @@ class SeriesDetails extends Component {
className={styles.detailsLabel}
size={sizes.LARGE}
>
<div>
<Icon
name={monitored ? icons.MONITORED : icons.UNMONITORED}
@@ -511,7 +511,6 @@ class SeriesDetails extends Component {
title={statusDetails.message}
size={sizes.LARGE}
>
<div>
<Icon
name={statusDetails.icon}
@@ -524,23 +523,43 @@ class SeriesDetails extends Component {
</Label>
{
!!network &&
originalLanguage?.name ?
<Label
className={styles.detailsLabel}
title={translate('OriginalLanguage')}
size={sizes.LARGE}
>
<div>
<Icon
name={icons.LANGUAGE}
size={17}
/>
<span className={styles.originalLanguageName}>
{originalLanguage.name}
</span>
</div>
</Label> :
null
}
{
network ?
<Label
className={styles.detailsLabel}
title={translate('Network')}
size={sizes.LARGE}
>
<div>
<Icon
name={icons.NETWORK}
size={17}
/>
<span className={styles.qualityProfileName}>
<span className={styles.network}>
{network}
</span>
</div>
</Label>
</Label> :
null
}
<Tooltip
@@ -549,7 +568,6 @@ class SeriesDetails extends Component {
className={styles.detailsLabel}
size={sizes.LARGE}
>
<div>
<Icon
name={icons.EXTERNAL_LINK}
@@ -566,6 +584,7 @@ class SeriesDetails extends Component {
tvdbId={tvdbId}
tvMazeId={tvMazeId}
imdbId={imdbId}
tmdbId={tmdbId}
/>
}
kind={kinds.INVERSE}
@@ -719,6 +738,7 @@ SeriesDetails.propTypes = {
tvdbId: PropTypes.number.isRequired,
tvMazeId: PropTypes.number,
imdbId: PropTypes.string,
tmdbId: PropTypes.number,
title: PropTypes.string.isRequired,
runtime: PropTypes.number.isRequired,
ratings: PropTypes.object.isRequired,
@@ -729,6 +749,7 @@ SeriesDetails.propTypes = {
monitor: PropTypes.string,
status: PropTypes.string.isRequired,
network: PropTypes.string,
originalLanguage: PropTypes.object,
overview: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
seasons: PropTypes.arrayOf(PropTypes.object).isRequired,

View File

@@ -9,7 +9,8 @@ function SeriesDetailsLinks(props) {
const {
tvdbId,
tvMazeId,
imdbId
imdbId,
tmdbId
} = props;
return (
@@ -71,6 +72,22 @@ function SeriesDetailsLinks(props) {
</Label>
</Link>
}
{
!!tmdbId &&
<Link
className={styles.link}
to={`https://www.themoviedb.org/tv/${tmdbId}`}
>
<Label
className={styles.linkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
TMDB
</Label>
</Link>
}
</div>
);
}
@@ -78,7 +95,8 @@ function SeriesDetailsLinks(props) {
SeriesDetailsLinks.propTypes = {
tvdbId: PropTypes.number.isRequired,
tvMazeId: PropTypes.number,
imdbId: PropTypes.string
imdbId: PropTypes.string,
tmdbId: PropTypes.number
};
export default SeriesDetailsLinks;

View File

@@ -2,7 +2,6 @@ import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
@@ -15,7 +14,7 @@ import SpinnerIcon from 'Components/SpinnerIcon';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import Popover from 'Components/Tooltip/Popover';
import { align, icons, kinds, sizes, sortDirections, tooltipPositions } from 'Helpers/Props';
import { align, icons, sortDirections, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import SeriesHistoryModal from 'Series/History/SeriesHistoryModal';
@@ -27,6 +26,7 @@ import translate from 'Utilities/String/translate';
import getToggledRange from 'Utilities/Table/getToggledRange';
import EpisodeRowConnector from './EpisodeRowConnector';
import SeasonInfo from './SeasonInfo';
import SeasonProgressLabel from './SeasonProgressLabel';
import styles from './SeriesDetailsSeason.css';
function getSeasonStatistics(episodes) {
@@ -64,18 +64,6 @@ function getSeasonStatistics(episodes) {
};
}
function getEpisodeCountKind(monitored, episodeFileCount, episodeCount) {
if (episodeFileCount === episodeCount && episodeCount > 0) {
return kinds.SUCCESS;
}
if (!monitored) {
return kinds.WARNING;
}
return kinds.DANGER;
}
class SeriesDetailsSeason extends Component {
//
@@ -265,12 +253,13 @@ class SeriesDetailsSeason extends Component {
className={styles.episodeCountTooltip}
canFlip={true}
anchor={
<Label
kind={getEpisodeCountKind(monitored, episodeFileCount, episodeCount)}
size={sizes.LARGE}
>
<span>{episodeFileCount} / {episodeCount}</span>
</Label>
<SeasonProgressLabel
seriesId={seriesId}
seasonNumber={seasonNumber}
monitored={monitored}
episodeCount={episodeCount}
episodeFileCount={episodeFileCount}
/>
}
title={translate('SeasonInformation')}
body={

View File

@@ -2,6 +2,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByProp from 'Utilities/Array/sortByProp';
import SeriesTags from './SeriesTags';
function createMapStateToProps() {
@@ -12,8 +13,8 @@ function createMapStateToProps() {
const tags = series.tags
.map((tagId) => tagList.find((tag) => tag.id === tagId))
.filter((tag) => !!tag)
.map((tag) => tag.label)
.sort((a, b) => a.localeCompare(b));
.sort(sortByProp('label'))
.map((tag) => tag.label);
return {
tags

View File

@@ -55,6 +55,7 @@ function SeriesIndexOverviewOptionsModalContent(
showSeasonCount,
showPath,
showSizeOnDisk,
showTags,
showSearchAction,
} = useSelector(selectOverviewOptions);
@@ -185,6 +186,17 @@ function SeriesIndexOverviewOptionsModalContent(
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ShowTags')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showTags"
value={showTags}
onChange={onOverviewOptionChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ShowSearch')}</FormLabel>

View File

@@ -73,14 +73,26 @@ $hoverScale: 1.05;
flex: 1 0 auto;
}
.overviewContainer {
display: flex;
justify-content: space-between;
flex: 0 1 1000px;
flex-direction: column;
}
.overview {
composes: link;
flex: 0 1 1000px;
overflow: hidden;
min-height: 0;
}
.tags {
display: flex;
justify-content: space-around;
overflow: hidden;
}
@media only screen and (max-width: $breakpointSmall) {
.overview {
display: none;

View File

@@ -8,8 +8,10 @@ interface CssExports {
'info': string;
'link': string;
'overview': string;
'overviewContainer': string;
'poster': string;
'posterContainer': string;
'tags': string;
'title': string;
'titleRow': string;
}

View File

@@ -5,6 +5,7 @@ import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import TagListConnector from 'Components/TagListConnector';
import { icons } from 'Helpers/Props';
import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal';
import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector';
@@ -70,6 +71,7 @@ function SeriesIndexOverview(props: SeriesIndexOverviewProps) {
overview,
statistics = {} as Statistics,
images,
tags,
network,
} = series;
@@ -205,15 +207,22 @@ function SeriesIndexOverview(props: SeriesIndexOverviewProps) {
</div>
<div className={styles.details}>
<Link className={styles.overview} to={link}>
<TextTruncate
line={Math.floor(
overviewHeight / (defaultFontSize * lineHeight)
)}
text={overview}
/>
</Link>
<div className={styles.overviewContainer}>
<Link className={styles.overview} to={link}>
<TextTruncate
line={Math.floor(
overviewHeight / (defaultFontSize * lineHeight)
)}
text={overview}
/>
</Link>
{overviewOptions.showTags ? (
<div className={styles.tags}>
<TagListConnector tags={tags} />
</div>
) : null}
</div>
<SeriesIndexOverviewInfo
height={overviewHeight}
monitored={monitored}

View File

@@ -5,7 +5,7 @@ import { icons } from 'Helpers/Props';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import dimensions from 'Styles/Variables/dimensions';
import QualityProfile from 'typings/QualityProfile';
import { UiSettings } from 'typings/UiSettings';
import UiSettings from 'typings/Settings/UiSettings';
import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes';
@@ -138,7 +138,10 @@ function getInfoRowProps(
}),
iconName: icons.CALENDAR,
label:
getRelativeDate(previousAiring, shortDateFormat, showRelativeDates, {
getRelativeDate({
date: previousAiring,
shortDateFormat,
showRelativeDates,
timeFormat,
timeForToday: true,
}) ?? '',
@@ -156,7 +159,10 @@ function getInfoRowProps(
}),
iconName: icons.ADD,
label:
getRelativeDate(added, shortDateFormat, showRelativeDates, {
getRelativeDate({
date: added,
shortDateFormat,
showRelativeDates,
timeFormat,
timeForToday: true,
}) ?? '',
@@ -230,17 +236,17 @@ function SeriesIndexOverviewInfo(props: SeriesIndexOverviewInfoProps) {
<div className={styles.infos}>
{!!nextAiring && (
<SeriesIndexOverviewInfoRow
title={formatDateTime(nextAiring, longDateFormat, timeFormat)}
title={translate('NextAiringDate', {
date: formatDateTime(nextAiring, longDateFormat, timeFormat),
})}
iconName={icons.SCHEDULED}
label={getRelativeDate(
nextAiring,
label={getRelativeDate({
date: nextAiring,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: true,
}
)}
timeFormat,
timeForToday: true,
})}
/>
)}

View File

@@ -52,6 +52,7 @@ function SeriesIndexPosterOptionsModalContent(
showTitle,
showMonitored,
showQualityProfile,
showTags,
showSearchAction,
} = posterOptions;
@@ -130,6 +131,18 @@ function SeriesIndexPosterOptionsModalContent(
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ShowTags')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showTags"
value={showTags}
helpText={translate('ShowTagsHelpText')}
onChange={onPosterOptionChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ShowSearch')}</FormLabel>

View File

@@ -57,6 +57,20 @@ $hoverScale: 1.05;
font-size: $smallFontSize;
}
.tags {
display: flex;
align-items: center;
justify-content: space-around;
padding: 0 3px;
height: 21px;
background-color: var(--seriesBackgroundColor);
}
.tagsList {
display: flex;
overflow: hidden;
}
.ended {
position: absolute;
top: 0;

View File

@@ -10,6 +10,8 @@ interface CssExports {
'nextAiring': string;
'overlayTitle': string;
'posterContainer': string;
'tags': string;
'tagsList': string;
'title': string;
}
export const cssExports: CssExports;

View File

@@ -5,6 +5,7 @@ import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import TagListConnector from 'Components/TagListConnector';
import { icons } from 'Helpers/Props';
import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal';
import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector';
@@ -41,6 +42,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
showTitle,
showMonitored,
showQualityProfile,
showTags,
showSearchAction,
} = useSelector(selectPosterOptions);
@@ -60,6 +62,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
added,
statistics = {} as Statistics,
images,
tags,
} = series;
const {
@@ -217,13 +220,24 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
timeFormat
)}`}
>
{getRelativeDate(nextAiring, shortDateFormat, showRelativeDates, {
{getRelativeDate({
date: nextAiring,
shortDateFormat,
showRelativeDates,
timeFormat,
timeForToday: true,
})}
</div>
) : null}
{showTags && tags.length ? (
<div className={styles.tags}>
<div className={styles.tagsList}>
<TagListConnector tags={tags} />
</div>
</div>
) : null}
<SeriesIndexPosterInfo
originalLanguage={originalLanguage}
network={network}
@@ -239,6 +253,8 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
shortDateFormat={shortDateFormat}
longDateFormat={longDateFormat}
timeFormat={timeFormat}
tags={tags}
showTags={showTags}
/>
<EditSeriesModalConnector

View File

@@ -3,3 +3,11 @@
text-align: center;
font-size: $smallFontSize;
}
.tags {
composes: tags from '~./SeriesIndexPoster.css';
}
.tagsList {
composes: tagsList from '~./SeriesIndexPoster.css';
}

View File

@@ -2,6 +2,8 @@
// Please do not change this file!
interface CssExports {
'info': string;
'tags': string;
'tagsList': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,4 +1,5 @@
import React from 'react';
import TagListConnector from 'Components/TagListConnector';
import Language from 'Language/Language';
import QualityProfile from 'typings/QualityProfile';
import formatDateTime from 'Utilities/Date/formatDateTime';
@@ -17,11 +18,13 @@ interface SeriesIndexPosterInfoProps {
seasonCount: number;
path: string;
sizeOnDisk?: number;
tags: number[];
sortKey: string;
showRelativeDates: boolean;
shortDateFormat: string;
longDateFormat: string;
timeFormat: string;
showTags: boolean;
}
function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) {
@@ -35,11 +38,13 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) {
seasonCount,
path,
sizeOnDisk,
tags,
sortKey,
showRelativeDates,
shortDateFormat,
longDateFormat,
timeFormat,
showTags,
} = props;
if (sortKey === 'network' && network) {
@@ -80,7 +85,10 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) {
timeFormat
)}`}
>
{getRelativeDate(previousAiring, shortDateFormat, showRelativeDates, {
{getRelativeDate({
date: previousAiring,
shortDateFormat,
showRelativeDates,
timeFormat,
timeForToday: true,
})}
@@ -89,15 +97,13 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) {
}
if (sortKey === 'added' && added) {
const addedDate = getRelativeDate(
added,
const addedDate = getRelativeDate({
date: added,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: false,
}
);
timeFormat,
timeForToday: false,
});
return (
<div
@@ -121,6 +127,16 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) {
return <div className={styles.info}>{seasons}</div>;
}
if (!showTags && sortKey === 'tags' && tags.length) {
return (
<div className={styles.tags}>
<div className={styles.tagsList}>
<TagListConnector tags={tags} />
</div>
</div>
);
}
if (sortKey === 'path') {
return (
<div className={styles.info} title={translate('Path')}>

View File

@@ -141,6 +141,7 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) {
showTitle,
showMonitored,
showQualityProfile,
showTags,
} = posterOptions;
const nextAiringHeight = 19;
@@ -164,6 +165,10 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) {
heights.push(19);
}
if (showTags) {
heights.push(21);
}
switch (sortKey) {
case 'network':
case 'seasons':
@@ -178,6 +183,11 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) {
heights.push(19);
}
break;
case 'tags':
if (!showTags) {
heights.push(21);
}
break;
default:
// No need to add a height of 0
}

View File

@@ -10,4 +10,15 @@
.path {
margin-left: 5px;
color: var(--dangerColor);
font-weight: bold;
}
.statistics {
margin-left: 5px;
color: var(--warningColor);
}
.deleteFilesMessage {
margin-top: 20px;
color: var(--warningColor);
}

View File

@@ -1,9 +1,11 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'deleteFilesMessage': string;
'message': string;
'path': string;
'pathContainer': string;
'statistics': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -16,6 +16,7 @@ import Series from 'Series/Series';
import { bulkDeleteSeries, setDeleteOption } from 'Store/Actions/seriesActions';
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
import { CheckInputChanged } from 'typings/inputs';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './DeleteSeriesModalContent.css';
@@ -85,6 +86,23 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) {
onModalClose,
]);
const { totalEpisodeFileCount, totalSizeOnDisk } = useMemo(() => {
return series.reduce(
(acc, { statistics = {} }) => {
const { episodeFileCount = 0, sizeOnDisk = 0 } = statistics;
acc.totalEpisodeFileCount += episodeFileCount;
acc.totalSizeOnDisk += sizeOnDisk;
return acc;
},
{
totalEpisodeFileCount: 0,
totalSizeOnDisk: 0,
}
);
}, [series]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('DeleteSelectedSeries')}</ModalHeader>
@@ -136,20 +154,44 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) {
</div>
<ul>
{series.map((s) => {
{series.map(({ title, path, statistics = {} }) => {
const { episodeFileCount = 0, sizeOnDisk = 0 } = statistics;
return (
<li key={s.title}>
<span>{s.title}</span>
<li key={title}>
<span>{title}</span>
{deleteFiles && (
<span className={styles.pathContainer}>
-<span className={styles.path}>{s.path}</span>
<span>
<span className={styles.pathContainer}>
-<span className={styles.path}>{path}</span>
</span>
{!!episodeFileCount && (
<span className={styles.statistics}>
(
{translate('DeleteSeriesFolderEpisodeCount', {
episodeFileCount,
size: formatBytes(sizeOnDisk),
})}
)
</span>
)}
</span>
)}
</li>
);
})}
</ul>
{deleteFiles && !!totalEpisodeFileCount ? (
<div className={styles.deleteFilesMessage}>
{translate('DeleteSeriesFolderEpisodeCount', {
episodeFileCount: totalEpisodeFileCount,
size: formatBytes(totalSizeOnDisk),
})}
</div>
) : null}
</ModalBody>
<ModalFooter>

View File

@@ -401,7 +401,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
if (name === 'ratings') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
<HeartRating rating={ratings.value} />
<HeartRating rating={ratings.value} votes={ratings.votes} />
</VirtualTableRowCell>
);
}

View File

@@ -70,6 +70,7 @@ interface Series extends ModelBase {
tvdbId: number;
tvMazeId: number;
tvRageId: number;
tmdbId: number;
useSceneNumbering: boolean;
year: number;
isSaving?: boolean;

View File

@@ -4,12 +4,12 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { cloneCustomFormat, deleteCustomFormat, fetchCustomFormats } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import CustomFormats from './CustomFormats';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.customFormats', sortByName),
createSortedSectionSelector('settings.customFormats', sortByProp('name')),
(customFormats) => customFormats
);
}

View File

@@ -25,3 +25,8 @@
border-radius: 4px;
background-color: var(--cardCenterBackgroundColor);
}
.customFormats {
display: flex;
flex-wrap: wrap;
}

View File

@@ -3,6 +3,7 @@
interface CssExports {
'addSpecification': string;
'center': string;
'customFormats': string;
'deleteButton': string;
'rightButtons': string;
}

View File

@@ -5,12 +5,12 @@ import { createSelector } from 'reselect';
import { deleteDownloadClient, fetchDownloadClients } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import DownloadClients from './DownloadClients';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.downloadClients', sortByName),
createSortedSectionSelector('settings.downloadClients', sortByProp('name')),
createTagsSelector(),
(downloadClients, tagList) => {
return {

View File

@@ -17,7 +17,6 @@ function UpdateSettings(props) {
const {
advancedSettings,
settings,
isWindows,
packageUpdateMechanism,
onInputChange
} = props;
@@ -68,63 +67,59 @@ function UpdateSettings(props) {
/>
</FormGroup>
{
isWindows ?
null :
<div>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('Automatic')}</FormLabel>
<div>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('Automatic')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="updateAutomatically"
helpText={translate('UpdateAutomaticallyHelpText')}
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker') : undefined}
onChange={onInputChange}
{...updateAutomatically}
/>
</FormGroup>
<FormInputGroup
type={inputTypes.CHECK}
name="updateAutomatically"
helpText={translate('UpdateAutomaticallyHelpText')}
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker') : undefined}
onChange={onInputChange}
{...updateAutomatically}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('Mechanism')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="updateMechanism"
values={updateOptions}
helpText={translate('UpdateMechanismHelpText')}
helpLink="https://wiki.servarr.com/sonarr/settings#updates"
onChange={onInputChange}
{...updateMechanism}
/>
</FormGroup>
{
updateMechanism.value === 'script' &&
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('Mechanism')}</FormLabel>
<FormLabel>{translate('ScriptPath')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="updateMechanism"
values={updateOptions}
helpText={translate('UpdateMechanismHelpText')}
helpLink="https://wiki.servarr.com/sonarr/settings#updates"
type={inputTypes.TEXT}
name="updateScriptPath"
helpText={translate('UpdateScriptPathHelpText')}
onChange={onInputChange}
{...updateMechanism}
{...updateScriptPath}
/>
</FormGroup>
{
updateMechanism.value === 'script' &&
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ScriptPath')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="updateScriptPath"
helpText={translate('UpdateScriptPathHelpText')}
onChange={onInputChange}
{...updateScriptPath}
/>
</FormGroup>
}
</div>
}
}
</div>
</FieldSet>
);
}

View File

@@ -5,12 +5,12 @@ import { createSelector } from 'reselect';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { deleteImportList, fetchImportLists } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import ImportLists from './ImportLists';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.importLists', sortByName),
createSortedSectionSelector('settings.importLists', sortByProp('name')),
(importLists) => importLists
);
}

View File

@@ -5,12 +5,12 @@ import { createSelector } from 'reselect';
import { cloneIndexer, deleteIndexer, fetchIndexers } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import Indexers from './Indexers';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.indexers', sortByName),
createSortedSectionSelector('settings.indexers', sortByProp('name')),
createTagsSelector(),
(indexers, tagList) => {
return {

View File

@@ -138,7 +138,8 @@ class Naming extends Component {
{ key: 1, value: translate('ReplaceWithDash') },
{ key: 2, value: translate('ReplaceWithSpaceDash') },
{ key: 3, value: translate('ReplaceWithSpaceDashSpace') },
{ key: 4, value: translate('SmartReplace'), hint: translate('SmartReplaceHint') }
{ key: 4, value: translate('SmartReplace'), hint: translate('SmartReplaceHint') },
{ key: 5, value: translate('Custom'), hint: translate('CustomColonReplacementFormatHint') }
];
const standardEpisodeFormatHelpTexts = [];
@@ -262,6 +263,22 @@ class Naming extends Component {
null
}
{
replaceIllegalCharacters && settings.colonReplacementFormat.value === 5 ?
<FormGroup>
<FormLabel>{translate('ColonReplacement')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="customColonReplacementFormat"
helpText={translate('CustomColonReplacementFormatHelpText')}
onChange={onInputChange}
{...settings.customColonReplacementFormat}
/>
</FormGroup> :
null
}
{
renameEpisodes &&
<div>

View File

@@ -99,6 +99,7 @@ const seriesTokens = [
const seriesIdTokens = [
{ token: '{ImdbId}', example: 'tt12345' },
{ token: '{TvdbId}', example: '12345' },
{ token: '{TmdbId}', example: '11223' },
{ token: '{TvMazeId}', example: '54321' }
];

View File

@@ -4,12 +4,12 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchMetadata } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import Metadatas from './Metadatas';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.metadata', sortByName),
createSortedSectionSelector('settings.metadata', sortByProp('name')),
(metadata) => metadata
);
}

View File

@@ -4,7 +4,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import {
saveNotification,
setNotificationFieldValue,
setNotificationFieldValues,
setNotificationValue,
testNotification,
toggleAdvancedSettings
@@ -27,7 +27,7 @@ function createMapStateToProps() {
const mapDispatchToProps = {
setNotificationValue,
setNotificationFieldValue,
setNotificationFieldValues,
saveNotification,
testNotification,
toggleAdvancedSettings
@@ -51,8 +51,8 @@ class EditNotificationModalContentConnector extends Component {
this.props.setNotificationValue({ name, value });
};
onFieldChange = ({ name, value }) => {
this.props.setNotificationFieldValue({ name, value });
onFieldChange = ({ name, value, additionalProperties = {} }) => {
this.props.setNotificationFieldValues({ properties: { ...additionalProperties, [name]: value } });
};
onSavePress = () => {
@@ -91,7 +91,7 @@ EditNotificationModalContentConnector.propTypes = {
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
setNotificationValue: PropTypes.func.isRequired,
setNotificationFieldValue: PropTypes.func.isRequired,
setNotificationFieldValues: PropTypes.func.isRequired,
saveNotification: PropTypes.func.isRequired,
testNotification: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,

View File

@@ -59,6 +59,7 @@ class Notification extends Component {
onGrab,
onDownload,
onUpgrade,
onImportComplete,
onRename,
onSeriesAdd,
onSeriesDelete,
@@ -71,6 +72,7 @@ class Notification extends Component {
supportsOnGrab,
supportsOnDownload,
supportsOnUpgrade,
supportsOnImportComplete,
supportsOnRename,
supportsOnSeriesAdd,
supportsOnSeriesDelete,
@@ -105,7 +107,7 @@ class Notification extends Component {
{
supportsOnDownload && onDownload ?
<Label kind={kinds.SUCCESS}>
{translate('OnImport')}
{translate('OnFileImport')}
</Label> :
null
}
@@ -113,7 +115,15 @@ class Notification extends Component {
{
supportsOnUpgrade && onDownload && onUpgrade ?
<Label kind={kinds.SUCCESS}>
{translate('OnUpgrade')}
{translate('OnFileUpgrade')}
</Label> :
null
}
{
supportsOnImportComplete && onImportComplete ?
<Label kind={kinds.SUCCESS}>
{translate('OnImportComplete')}
</Label> :
null
}
@@ -191,7 +201,7 @@ class Notification extends Component {
}
{
!onGrab && !onDownload && !onRename && !onHealthIssue && !onHealthRestored && !onApplicationUpdate && !onSeriesAdd && !onSeriesDelete && !onEpisodeFileDelete && !onManualInteractionRequired ?
!onGrab && !onDownload && !onRename && !onImportComplete && !onHealthIssue && !onHealthRestored && !onApplicationUpdate && !onSeriesAdd && !onSeriesDelete && !onEpisodeFileDelete && !onManualInteractionRequired ?
<Label
kind={kinds.DISABLED}
outline={true}
@@ -233,6 +243,7 @@ Notification.propTypes = {
onGrab: PropTypes.bool.isRequired,
onDownload: PropTypes.bool.isRequired,
onUpgrade: PropTypes.bool.isRequired,
onImportComplete: PropTypes.bool.isRequired,
onRename: PropTypes.bool.isRequired,
onSeriesAdd: PropTypes.bool.isRequired,
onSeriesDelete: PropTypes.bool.isRequired,
@@ -244,6 +255,7 @@ Notification.propTypes = {
onManualInteractionRequired: PropTypes.bool.isRequired,
supportsOnGrab: PropTypes.bool.isRequired,
supportsOnDownload: PropTypes.bool.isRequired,
supportsOnImportComplete: PropTypes.bool.isRequired,
supportsOnSeriesAdd: PropTypes.bool.isRequired,
supportsOnSeriesDelete: PropTypes.bool.isRequired,
supportsOnEpisodeFileDelete: PropTypes.bool.isRequired,

View File

@@ -18,6 +18,7 @@ function NotificationEventItems(props) {
onGrab,
onDownload,
onUpgrade,
onImportComplete,
onRename,
onSeriesAdd,
onSeriesDelete,
@@ -30,6 +31,7 @@ function NotificationEventItems(props) {
supportsOnGrab,
supportsOnDownload,
supportsOnUpgrade,
supportsOnImportComplete,
supportsOnRename,
supportsOnSeriesAdd,
supportsOnSeriesDelete,
@@ -66,7 +68,7 @@ function NotificationEventItems(props) {
<FormInputGroup
type={inputTypes.CHECK}
name="onDownload"
helpText={translate('OnImport')}
helpText={translate('OnFileImport')}
isDisabled={!supportsOnDownload.value}
{...onDownload}
onChange={onInputChange}
@@ -79,7 +81,7 @@ function NotificationEventItems(props) {
<FormInputGroup
type={inputTypes.CHECK}
name="onUpgrade"
helpText={translate('OnUpgrade')}
helpText={translate('OnFileUpgrade')}
isDisabled={!supportsOnUpgrade.value}
{...onUpgrade}
onChange={onInputChange}
@@ -87,6 +89,17 @@ function NotificationEventItems(props) {
</div>
}
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onImportComplete"
helpText={translate('OnImportComplete')}
isDisabled={!supportsOnImportComplete.value}
{...onImportComplete}
onChange={onInputChange}
/>
</div>
<div>
<FormInputGroup
type={inputTypes.CHECK}

View File

@@ -5,12 +5,12 @@ import { createSelector } from 'reselect';
import { deleteNotification, fetchNotifications } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import Notifications from './Notifications';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.notifications', sortByName),
createSortedSectionSelector('settings.notifications', sortByProp('name')),
createTagsSelector(),
(notifications, tagList) => {
return {

View File

@@ -20,7 +20,8 @@ function calcOrder(profileFormatItems) {
if (b.score !== a.score) {
return b.score - a.score;
}
return a.name > b.name ? 1 : -1;
return a.name.localeCompare(b.name, undefined, { numeric: true });
}).map((x) => items[x.format]);
}

View File

@@ -4,12 +4,12 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { cloneQualityProfile, deleteQualityProfile, fetchQualityProfiles } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import QualityProfiles from './QualityProfiles';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.qualityProfiles', sortByName),
createSortedSectionSelector('settings.qualityProfiles', sortByProp('name')),
(qualityProfiles) => qualityProfiles
);
}

View File

@@ -9,7 +9,7 @@ import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { cloneAutoTagging, deleteAutoTagging, fetchAutoTaggings } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import AutoTagging from './AutoTagging';
import EditAutoTaggingModal from './EditAutoTaggingModal';
@@ -23,7 +23,7 @@ export default function AutoTaggings() {
isFetching,
isPopulated
} = useSelector(
createSortedSectionSelector('settings.autoTaggings', sortByName)
createSortedSectionSelector('settings.autoTaggings', sortByProp('name'))
);
const tagList = useSelector(createTagsSelector());

View File

@@ -25,3 +25,8 @@
border-radius: 4px;
background-color: var(--cardCenterBackgroundColor);
}
.autoTaggings {
display: flex;
flex-wrap: wrap;
}

View File

@@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'addSpecification': string;
'autoTaggings': string;
'center': string;
'deleteButton': string;
'rightButtons': string;

View File

@@ -0,0 +1,25 @@
import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState';
function createSetProviderFieldValuesReducer(section) {
return (state, { payload }) => {
if (section === payload.section) {
const { properties } = payload;
const newState = getSectionState(state, section);
newState.pendingChanges = Object.assign({}, newState.pendingChanges);
const fields = Object.assign({}, newState.pendingChanges.fields || {});
Object.keys(properties).forEach((name) => {
fields[name] = properties[name];
});
newState.pendingChanges.fields = fields;
return updateSectionState(state, section, newState);
}
return state;
};
}
export default createSetProviderFieldValuesReducer;

View File

@@ -5,6 +5,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetProviderFieldValuesReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValuesReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
@@ -22,6 +23,7 @@ export const FETCH_NOTIFICATION_SCHEMA = 'settings/notifications/fetchNotificati
export const SELECT_NOTIFICATION_SCHEMA = 'settings/notifications/selectNotificationSchema';
export const SET_NOTIFICATION_VALUE = 'settings/notifications/setNotificationValue';
export const SET_NOTIFICATION_FIELD_VALUE = 'settings/notifications/setNotificationFieldValue';
export const SET_NOTIFICATION_FIELD_VALUES = 'settings/notifications/setNotificationFieldValues';
export const SAVE_NOTIFICATION = 'settings/notifications/saveNotification';
export const CANCEL_SAVE_NOTIFICATION = 'settings/notifications/cancelSaveNotification';
export const DELETE_NOTIFICATION = 'settings/notifications/deleteNotification';
@@ -55,6 +57,13 @@ export const setNotificationFieldValue = createAction(SET_NOTIFICATION_FIELD_VAL
};
});
export const setNotificationFieldValues = createAction(SET_NOTIFICATION_FIELD_VALUES, (payload) => {
return {
section,
...payload
};
});
//
// Details
@@ -99,6 +108,7 @@ export default {
reducers: {
[SET_NOTIFICATION_VALUE]: createSetSettingValueReducer(section),
[SET_NOTIFICATION_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
[SET_NOTIFICATION_FIELD_VALUES]: createSetProviderFieldValuesReducer(section),
[SELECT_NOTIFICATION_SCHEMA]: (state, { payload }) => {
return selectProviderSchema(state, section, payload, (selectedSchema) => {
@@ -106,6 +116,7 @@ export default {
selectedSchema.onGrab = selectedSchema.supportsOnGrab;
selectedSchema.onDownload = selectedSchema.supportsOnDownload;
selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade;
selectedSchema.onImportComplete = selectedSchema.supportsOnImportComplete;
selectedSchema.onRename = selectedSchema.supportsOnRename;
selectedSchema.onSeriesAdd = selectedSchema.supportsOnSeriesAdd;
selectedSchema.onSeriesDelete = selectedSchema.supportsOnSeriesDelete;

View File

@@ -1,7 +1,7 @@
import { createAction } from 'redux-actions';
import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import translate from 'Utilities/String/translate';
import createFetchHandler from './Creators/createFetchHandler';
@@ -232,7 +232,7 @@ export const defaultState = {
return acc;
}, []);
return genreList.sort(sortByName);
return genreList.sort(sortByProp('name'));
}
},
{

View File

@@ -3,7 +3,7 @@ import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate';
import translate from 'Utilities/String/translate';
@@ -128,8 +128,16 @@ export const filterPredicates = {
ratings: function(item, filterValue, type) {
const predicate = filterTypePredicates[type];
const { value = 0 } = item.ratings;
return predicate(item.ratings.value * 10, filterValue);
return predicate(value * 10, filterValue);
},
ratingVotes: function(item, filterValue, type) {
const predicate = filterTypePredicates[type];
const { votes = 0 } = item.ratings;
return predicate(votes, filterValue);
},
originalLanguage: function(item, filterValue, type) {
@@ -246,7 +254,7 @@ export const filterBuilderProps = [
return acc;
}, []);
return tagList.sort(sortByName);
return tagList.sort(sortByProp('name'));
}
},
{
@@ -315,7 +323,7 @@ export const filterBuilderProps = [
return acc;
}, []);
return tagList.sort(sortByName);
return tagList.sort(sortByProp('name'));
}
},
{
@@ -334,7 +342,7 @@ export const filterBuilderProps = [
return acc;
}, []);
return languageList.sort(sortByName);
return languageList.sort(sortByProp('name'));
}
},
{
@@ -347,6 +355,11 @@ export const filterBuilderProps = [
label: () => translate('Rating'),
type: filterBuilderTypes.NUMBER
},
{
name: 'ratingVotes',
label: () => translate('RatingVotes'),
type: filterBuilderTypes.NUMBER
},
{
name: 'certification',
label: () => translate('Certification'),

View File

@@ -29,6 +29,7 @@ export const defaultState = {
showTitle: false,
showMonitored: true,
showQualityProfile: true,
showTags: false,
showSearchAction: false
},
@@ -43,6 +44,7 @@ export const defaultState = {
showSeasonCount: true,
showPath: false,
showSizeOnDisk: false,
showTags: false,
showSearchAction: false
},

View File

@@ -2,13 +2,17 @@ import { createSelector } from 'reselect';
import { DownloadClientAppState } from 'App/State/SettingsAppState';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import DownloadClient from 'typings/DownloadClient';
import sortByProp from 'Utilities/Array/sortByProp';
export default function createEnabledDownloadClientsSelector(
protocol: DownloadProtocol
) {
return createSelector(
createSortedSectionSelector('settings.downloadClients', sortByName),
createSortedSectionSelector<DownloadClient>(
'settings.downloadClients',
sortByProp('name')
),
(downloadClients: DownloadClientAppState) => {
const { isFetching, isPopulated, error, items } = downloadClients;

View File

@@ -2,12 +2,11 @@ import { createSelector } from 'reselect';
import RootFolderAppState from 'App/State/RootFolderAppState';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import RootFolder from 'typings/RootFolder';
import sortByProp from 'Utilities/Array/sortByProp';
export default function createRootFoldersSelector() {
return createSelector(
createSortedSectionSelector('rootFolders', (a: RootFolder, b: RootFolder) =>
a.path.localeCompare(b.path)
),
createSortedSectionSelector<RootFolder>('rootFolders', sortByProp('path')),
(rootFolders: RootFolderAppState) => rootFolders
);
}

View File

@@ -1,14 +1,18 @@
import { createSelector } from 'reselect';
import getSectionState from 'Utilities/State/getSectionState';
function createSortedSectionSelector(section, comparer) {
function createSortedSectionSelector<T>(
section: string,
comparer: (a: T, b: T) => number
) {
return createSelector(
(state) => state,
(state) => {
const sectionState = getSectionState(state, section, true);
return {
...sectionState,
items: [...sectionState.items].sort(comparer)
items: [...sectionState.items].sort(comparer),
};
}
);

View File

@@ -3,6 +3,7 @@ import { useSelector } from 'react-redux';
import { CommandBody } from 'Commands/Command';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import createMultiSeriesSelector from 'Store/Selectors/createMultiSeriesSelector';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import styles from './QueuedTaskRowNameCell.css';
@@ -39,9 +40,7 @@ export default function QueuedTaskRowNameCell(
}
const series = useSelector(createMultiSeriesSelector(seriesIds));
const sortedSeries = series.sort((a, b) =>
a.sortTitle.localeCompare(b.sortTitle)
);
const sortedSeries = series.sort(sortByProp('sortTitle'));
return (
<TableRowCell>

View File

@@ -1,46 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import styles from './UpdateChanges.css';
class UpdateChanges extends Component {
//
// Render
render() {
const {
title,
changes
} = this.props;
if (changes.length === 0) {
return null;
}
return (
<div>
<div className={styles.title}>{title}</div>
<ul>
{
changes.map((change, index) => {
return (
<li key={index}>
<InlineMarkdown data={change} />
</li>
);
})
}
</ul>
</div>
);
}
}
UpdateChanges.propTypes = {
title: PropTypes.string.isRequired,
changes: PropTypes.arrayOf(PropTypes.string)
};
export default UpdateChanges;

View File

@@ -0,0 +1,33 @@
import React from 'react';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import styles from './UpdateChanges.css';
interface UpdateChangesProps {
title: string;
changes: string[];
}
function UpdateChanges(props: UpdateChangesProps) {
const { title, changes } = props;
if (changes.length === 0) {
return null;
}
return (
<div>
<div className={styles.title}>{title}</div>
<ul>
{changes.map((change, index) => {
return (
<li key={index}>
<InlineMarkdown data={change} />
</li>
);
})}
</ul>
</div>
);
}
export default UpdateChanges;

View File

@@ -1,249 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { icons, kinds } from 'Helpers/Props';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime';
import translate from 'Utilities/String/translate';
import UpdateChanges from './UpdateChanges';
import styles from './Updates.css';
class Updates extends Component {
//
// Render
render() {
const {
currentVersion,
isFetching,
isPopulated,
updatesError,
generalSettingsError,
items,
isInstallingUpdate,
updateMechanism,
updateMechanismMessage,
shortDateFormat,
longDateFormat,
timeFormat,
onInstallLatestPress
} = this.props;
const hasError = !!(updatesError || generalSettingsError);
const hasUpdates = isPopulated && !hasError && items.length > 0;
const noUpdates = isPopulated && !hasError && !items.length;
const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true });
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
const externalUpdaterPrefix = translate('UpdateSonarrDirectlyLoadError');
const externalUpdaterMessages = {
external: translate('ExternalUpdater'),
apt: translate('AptUpdater'),
docker: translate('DockerUpdater')
};
return (
<PageContent title={translate('Updates')}>
<PageContentBody>
{
!isPopulated && !hasError &&
<LoadingIndicator />
}
{
noUpdates &&
<Alert kind={kinds.INFO}>
{translate('NoUpdatesAreAvailable')}
</Alert>
}
{
hasUpdateToInstall &&
<div className={styles.messageContainer}>
{
updateMechanism === 'builtIn' || updateMechanism === 'script' ?
<SpinnerButton
className={styles.updateAvailable}
kind={kinds.PRIMARY}
isSpinning={isInstallingUpdate}
onPress={onInstallLatestPress}
>
{translate('InstallLatest')}
</SpinnerButton> :
<Fragment>
<Icon
name={icons.WARNING}
kind={kinds.WARNING}
size={30}
/>
<div className={styles.message}>
{externalUpdaterPrefix} <InlineMarkdown data={updateMechanismMessage || externalUpdaterMessages[updateMechanism] || externalUpdaterMessages.external} />
</div>
</Fragment>
}
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
</div>
}
{
noUpdateToInstall &&
<div className={styles.messageContainer}>
<Icon
className={styles.upToDateIcon}
name={icons.CHECK_CIRCLE}
size={30}
/>
<div className={styles.message}>
{translate('OnLatestVersion')}
</div>
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
</div>
}
{
hasUpdates &&
<div>
{
items.map((update) => {
const hasChanges = !!update.changes;
return (
<div
key={update.version}
className={styles.update}
>
<div className={styles.info}>
<div className={styles.version}>{update.version}</div>
<div className={styles.space}>&mdash;</div>
<div
className={styles.date}
title={formatDateTime(update.releaseDate, longDateFormat, timeFormat)}
>
{formatDate(update.releaseDate, shortDateFormat)}
</div>
{
update.branch === 'main' ?
null :
<Label
className={styles.label}
>
{update.branch}
</Label>
}
{
update.version === currentVersion ?
<Label
className={styles.label}
kind={kinds.SUCCESS}
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
>
{translate('CurrentlyInstalled')}
</Label> :
null
}
{
update.version !== currentVersion && update.installedOn ?
<Label
className={styles.label}
kind={kinds.INVERSE}
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
>
{translate('PreviouslyInstalled')}
</Label> :
null
}
</div>
{
!hasChanges &&
<div>
{translate('MaintenanceRelease')}
</div>
}
{
hasChanges &&
<div className={styles.changes}>
<UpdateChanges
title={translate('New')}
changes={update.changes.new}
/>
<UpdateChanges
title={translate('Fixed')}
changes={update.changes.fixed}
/>
</div>
}
</div>
);
})
}
</div>
}
{
!!updatesError &&
<div>
{translate('FailedToFetchUpdates')}
</div>
}
{
!!generalSettingsError &&
<div>
{translate('FailedToUpdateSettings')}
</div>
}
</PageContentBody>
</PageContent>
);
}
}
Updates.propTypes = {
currentVersion: PropTypes.string.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
updatesError: PropTypes.object,
generalSettingsError: PropTypes.object,
items: PropTypes.array.isRequired,
isInstallingUpdate: PropTypes.bool.isRequired,
updateMechanism: PropTypes.string,
updateMechanismMessage: PropTypes.string,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onInstallLatestPress: PropTypes.func.isRequired
};
export default Updates;

View File

@@ -0,0 +1,305 @@
import React, {
Fragment,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { icons, kinds } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchGeneralSettings } from 'Store/Actions/settingsActions';
import { fetchUpdates } from 'Store/Actions/systemActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { UpdateMechanism } from 'typings/Settings/General';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime';
import translate from 'Utilities/String/translate';
import UpdateChanges from './UpdateChanges';
import styles from './Updates.css';
const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i;
function createUpdatesSelector() {
return createSelector(
(state: AppState) => state.system.updates,
(state: AppState) => state.settings.general,
(updates, generalSettings) => {
const { error: updatesError, items } = updates;
const isFetching = updates.isFetching || generalSettings.isFetching;
const isPopulated = updates.isPopulated && generalSettings.isPopulated;
return {
isFetching,
isPopulated,
updatesError,
generalSettingsError: generalSettings.error,
items,
updateMechanism: generalSettings.item.updateMechanism,
};
}
);
}
function Updates() {
const currentVersion = useSelector((state: AppState) => state.app.version);
const { packageUpdateMechanismMessage } = useSelector(
createSystemStatusSelector()
);
const { shortDateFormat, longDateFormat, timeFormat } = useSelector(
createUISettingsSelector()
);
const isInstallingUpdate = useSelector(
createCommandExecutingSelector(commandNames.APPLICATION_UPDATE)
);
const {
isFetching,
isPopulated,
updatesError,
generalSettingsError,
items,
updateMechanism,
} = useSelector(createUpdatesSelector());
const dispatch = useDispatch();
const [isMajorUpdateModalOpen, setIsMajorUpdateModalOpen] = useState(false);
const hasError = !!(updatesError || generalSettingsError);
const hasUpdates = isPopulated && !hasError && items.length > 0;
const noUpdates = isPopulated && !hasError && !items.length;
const externalUpdaterPrefix = translate('UpdateSonarrDirectlyLoadError');
const externalUpdaterMessages: Partial<Record<UpdateMechanism, string>> = {
external: translate('ExternalUpdater'),
apt: translate('AptUpdater'),
docker: translate('DockerUpdater'),
};
const { isMajorUpdate, hasUpdateToInstall } = useMemo(() => {
const majorVersion = parseInt(
currentVersion.match(VERSION_REGEX)?.[0] ?? '0'
);
const latestVersion = items[0]?.version;
const latestMajorVersion = parseInt(
latestVersion?.match(VERSION_REGEX)?.[0] ?? '0'
);
return {
isMajorUpdate: latestMajorVersion > majorVersion,
hasUpdateToInstall: items.some(
(update) => update.installable && update.latest
),
};
}, [currentVersion, items]);
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
const handleInstallLatestPress = useCallback(() => {
if (isMajorUpdate) {
setIsMajorUpdateModalOpen(true);
} else {
dispatch(executeCommand({ name: commandNames.APPLICATION_UPDATE }));
}
}, [isMajorUpdate, setIsMajorUpdateModalOpen, dispatch]);
const handleInstallLatestMajorVersionPress = useCallback(() => {
setIsMajorUpdateModalOpen(false);
dispatch(
executeCommand({
name: commandNames.APPLICATION_UPDATE,
installMajorUpdate: true,
})
);
}, [setIsMajorUpdateModalOpen, dispatch]);
const handleCancelMajorVersionPress = useCallback(() => {
setIsMajorUpdateModalOpen(false);
}, [setIsMajorUpdateModalOpen]);
useEffect(() => {
dispatch(fetchUpdates());
dispatch(fetchGeneralSettings());
}, [dispatch]);
return (
<PageContent title={translate('Updates')}>
<PageContentBody>
{isPopulated || hasError ? null : <LoadingIndicator />}
{noUpdates ? (
<Alert kind={kinds.INFO}>{translate('NoUpdatesAreAvailable')}</Alert>
) : null}
{hasUpdateToInstall ? (
<div className={styles.messageContainer}>
{updateMechanism === 'builtIn' || updateMechanism === 'script' ? (
<SpinnerButton
kind={kinds.PRIMARY}
isSpinning={isInstallingUpdate}
onPress={handleInstallLatestPress}
>
{translate('InstallLatest')}
</SpinnerButton>
) : (
<Fragment>
<Icon name={icons.WARNING} kind={kinds.WARNING} size={30} />
<div className={styles.message}>
{externalUpdaterPrefix}{' '}
<InlineMarkdown
data={
packageUpdateMechanismMessage ||
externalUpdaterMessages[updateMechanism] ||
externalUpdaterMessages.external
}
/>
</div>
</Fragment>
)}
{isFetching ? (
<LoadingIndicator className={styles.loading} size={20} />
) : null}
</div>
) : null}
{noUpdateToInstall && (
<div className={styles.messageContainer}>
<Icon
className={styles.upToDateIcon}
name={icons.CHECK_CIRCLE}
size={30}
/>
<div className={styles.message}>{translate('OnLatestVersion')}</div>
{isFetching && (
<LoadingIndicator className={styles.loading} size={20} />
)}
</div>
)}
{hasUpdates && (
<div>
{items.map((update) => {
const hasChanges = !!update.changes;
return (
<div key={update.version} className={styles.update}>
<div className={styles.info}>
<div className={styles.version}>{update.version}</div>
<div className={styles.space}>&mdash;</div>
<div
className={styles.date}
title={formatDateTime(
update.releaseDate,
longDateFormat,
timeFormat
)}
>
{formatDate(update.releaseDate, shortDateFormat)}
</div>
{update.branch === 'main' ? null : (
<Label className={styles.label}>{update.branch}</Label>
)}
{update.version === currentVersion ? (
<Label
className={styles.label}
kind={kinds.SUCCESS}
title={formatDateTime(
update.installedOn,
longDateFormat,
timeFormat
)}
>
{translate('CurrentlyInstalled')}
</Label>
) : null}
{update.version !== currentVersion && update.installedOn ? (
<Label
className={styles.label}
kind={kinds.INVERSE}
title={formatDateTime(
update.installedOn,
longDateFormat,
timeFormat
)}
>
{translate('PreviouslyInstalled')}
</Label>
) : null}
</div>
{hasChanges ? (
<div>
<UpdateChanges
title={translate('New')}
changes={update.changes.new}
/>
<UpdateChanges
title={translate('Fixed')}
changes={update.changes.fixed}
/>
</div>
) : (
<div>{translate('MaintenanceRelease')}</div>
)}
</div>
);
})}
</div>
)}
{updatesError ? <div>{translate('FailedToFetchUpdates')}</div> : null}
{generalSettingsError ? (
<div>{translate('FailedToUpdateSettings')}</div>
) : null}
<ConfirmModal
isOpen={isMajorUpdateModalOpen}
kind={kinds.WARNING}
title={translate('InstallMajorVersionUpdate')}
message={
<div>
<div>{translate('InstallMajorVersionUpdateMessage')}</div>
<div>
<InlineMarkdown
data={translate('InstallMajorVersionUpdateMessageLink', {
domain: 'sonarr.tv',
url: 'https://sonarr.tv/#downloads',
})}
/>
</div>
</div>
}
confirmLabel={translate('Install')}
onConfirm={handleInstallLatestMajorVersionPress}
onCancel={handleCancelMajorVersionPress}
/>
</PageContentBody>
</PageContent>
);
}
export default Updates;

View File

@@ -1,98 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchGeneralSettings } from 'Store/Actions/settingsActions';
import { fetchUpdates } from 'Store/Actions/systemActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import Updates from './Updates';
function createMapStateToProps() {
return createSelector(
(state) => state.app.version,
createSystemStatusSelector(),
(state) => state.system.updates,
(state) => state.settings.general,
createUISettingsSelector(),
createCommandExecutingSelector(commandNames.APPLICATION_UPDATE),
(
currentVersion,
status,
updates,
generalSettings,
uiSettings,
isInstallingUpdate
) => {
const {
error: updatesError,
items
} = updates;
const isFetching = updates.isFetching || generalSettings.isFetching;
const isPopulated = updates.isPopulated && generalSettings.isPopulated;
return {
currentVersion,
isFetching,
isPopulated,
updatesError,
generalSettingsError: generalSettings.error,
items,
isInstallingUpdate,
updateMechanism: generalSettings.item.updateMechanism,
updateMechanismMessage: status.packageUpdateMechanismMessage,
shortDateFormat: uiSettings.shortDateFormat,
longDateFormat: uiSettings.longDateFormat,
timeFormat: uiSettings.timeFormat
};
}
);
}
const mapDispatchToProps = {
dispatchFetchUpdates: fetchUpdates,
dispatchFetchGeneralSettings: fetchGeneralSettings,
dispatchExecuteCommand: executeCommand
};
class UpdatesConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchUpdates();
this.props.dispatchFetchGeneralSettings();
}
//
// Listeners
onInstallLatestPress = () => {
this.props.dispatchExecuteCommand({ name: commandNames.APPLICATION_UPDATE });
};
//
// Render
render() {
return (
<Updates
onInstallLatestPress={this.onInstallLatestPress}
{...this.props}
/>
);
}
}
UpdatesConnector.propTypes = {
dispatchFetchUpdates: PropTypes.func.isRequired,
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(UpdatesConnector);

View File

@@ -1,5 +0,0 @@
function sortByName(a, b) {
return a.name.localeCompare(b.name);
}
export default sortByName;

View File

@@ -0,0 +1,13 @@
import { StringKey } from 'typings/Helpers/KeysMatching';
export function sortByProp<
// eslint-disable-next-line no-use-before-define
T extends Record<K, string>,
K extends StringKey<T>
>(sortKey: K) {
return (a: T, b: T) => {
return a[sortKey].localeCompare(b[sortKey], undefined, { numeric: true });
};
}
export default sortByProp;

Some files were not shown because too many files have changed in this diff Show More