Merge upstream up to d4dfa9a2c2

Squashed commit of the following:

commit 49c30b336d
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 26 19:28:32 2023 +0100

    fuck lint

commit 06f3f7e6b4
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 26 19:26:41 2023 +0100

    cleanup

commit 807ea3d1a5
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 26 18:51:04 2023 +0100

    merge41

    Last commit merged: d4dfa9a2c2

commit 2b9fe5c389
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 26 15:18:03 2023 +0100

    fix anime tracker & airing sort

commit eafc64aed0
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 26 14:40:45 2023 +0100

    fuck lint

commit eef4ef7ef2
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 26 14:20:10 2023 +0100

    fix weird "explore tabs" behaviour

commit 4baf786d57
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 26 13:43:13 2023 +0100

    use formatTime function

commit df6a85a944
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 26 13:38:39 2023 +0100

    fix AnimeScreen crash

commit 91de0ed82e
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 26 01:44:41 2023 +0100

    merge40

    Last commit merged: 69aa13bc56

commit 375a252a69
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 25 21:09:34 2023 +0100

    merge39

    Last commit merged: 634ceeec50

commit d7aee03688
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 25 19:42:37 2023 +0100

    merge38

    Last commit merged: 1d144e6767

commit 89777e98b0
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 25 17:54:17 2023 +0100

    merge37

    Last commit merged: aca36f9625

commit 3fba4cbc2b
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 25 15:09:18 2023 +0100

    merge36

    Last commit merged: 64ad25d1b5

commit a60268e61c
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 25 13:39:08 2023 +0100

    lint cleanup

commit c6f81b7fda
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 25 13:38:22 2023 +0100

    merge35

    Last commit merged: eed57b80be

commit 5f782440c6
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 25 00:22:45 2023 +0100

    merge34.5

    Last commit merged: 8e4cedf173

commit e4283fe416
Merge: e4d31057f adb571fcb
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Nov 24 23:21:58 2023 +0100

    Merge branch 'master' into merge_upstream

commit e4d31057fd
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Nov 24 23:07:20 2023 +0100

    cleanup

commit 150d43e325
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Nov 24 23:06:00 2023 +0100

    merge34

    Last commit merged: 489d22720a

commit 76df725cab
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Nov 24 20:16:18 2023 +0100

    merge33

    Last commit merged: b7d282235d

commit c07cc8dc21
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Nov 24 16:59:50 2023 +0100

    merge32

    Last commit merged: 8a8afa46e9

commit a654a54b74
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Thu Nov 23 23:01:52 2023 +0100

    Update build_push.yml

commit 325e625aef
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Thu Nov 23 22:44:56 2023 +0100

    merge31

    Last commit merged: 86edce0d87

commit b8cdb9d55e
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Mon Nov 20 21:47:46 2023 +0100

    small Episode Tracker change

commit f6d430df6c
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Mon Nov 20 21:18:01 2023 +0100

    fuck lint

commit 99eea595c0
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Mon Nov 20 21:15:15 2023 +0100

    crash fix & add forgotten commit

    No idea how I didn't add it yet. Commit: efabe801be

commit 18e04b75df
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 19 19:38:42 2023 +0100

    merge30

    Last commit merged: 1668be8587

commit 3a3115304e
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 19 16:45:34 2023 +0100

    ktlint formt

commit ea2b7fe7c0
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 19 16:05:25 2023 +0100

    ktlint format again

commit 05f91245ac
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 19 15:47:42 2023 +0100

    klint format again

commit 6215528c9d
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 19 15:37:56 2023 +0100

    ktlint format

commit b1f728e54a
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 19 15:24:35 2023 +0100

    fix ConcurrentHashMap crash

commit a34bc764b4
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 19 15:04:24 2023 +0100

    renaming & ktlintCheck

commit f1cd8f69d3
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 19 14:39:59 2023 +0100

    merge29

    Last commit merged: 772db51593

commit 2c4230376c
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 19 13:18:41 2023 +0100

    merge28

    Last commit merged: 6922792ad1

commit fa7b8427a2
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 19 00:36:58 2023 +0100

    merge27

    Last commit merged: 7146913c71

commit e29dc62837
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 18 17:28:12 2023 +0100

    lint cleanup

commit 85d0e49fd4
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 18 17:26:35 2023 +0100

    little fix

commit eeddf17691
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 18 17:20:36 2023 +0100

    merge26

    Last commit merged: c9a1bd86b5

commit a2a445fdc5
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 18 16:01:50 2023 +0100

    merge25

    Last commit merged: 6a558ad119

commit 1b6301cc95
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 18 00:21:17 2023 +0100

    merge24

    Also a little test to see how this whole github team works.

    Last commit merged: fe90546821

commit 4cbf9a813e
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Nov 17 20:12:49 2023 +0100

    merge23.5

    Last commit merged: 6d69caf59e

commit 72a54cbb6e
Merge: a43904531 00fb0a740
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Nov 17 19:52:08 2023 +0100

    Merge branch 'master' into MR

commit a439045319
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Nov 17 19:47:18 2023 +0100

    merge23

    Last commit merged: 8ff0c9d61a

commit 370212c677
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Nov 17 17:00:32 2023 +0100

    merge22

    Last commit merged: 2556e9f08c

commit 717479961c
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Wed Nov 15 21:25:52 2023 +0100

    Update StreamsCatalogSheet.kt

commit 563efef3c9
Merge: 645746aa4 2b8000b81
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Wed Nov 15 21:11:51 2023 +0100

    Merge branch 'master' into MR

commit 645746aa43
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 12 15:57:35 2023 +0100

    merge21

    Last commit merged: 7a4680603d

commit 4709cbedba
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Nov 12 14:40:35 2023 +0100

    merge20

    Last commit merged: ef7b285151

commit 096276cd68
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 11 19:09:44 2023 +0100

    merge19.5

    Last commit merged: 34f7caa0fc

commit 82eb36b628
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 11 18:55:20 2023 +0100

    fuck lint

commit 02717061ba
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 11 18:49:20 2023 +0100

    merge19

    Last commit merged: ec08ba05fc

commit e7b1066e3c
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 11 17:58:02 2023 +0100

    merge18

    Last commit merged: 12e7ee9d0c

commit 61256a22fd
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 11 15:55:09 2023 +0100

    again repositioning

commit b9d42c97d6
Merge: 4732c3893 236849b6e
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 11 15:44:21 2023 +0100

    Merge branch 'master' into MR

commit 4732c3893c
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 11 15:43:57 2023 +0100

    repositioning

commit 83dcd5ea31
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 11 15:42:22 2023 +0100

    merge17

    Last commit merged: 9a817e49be

commit 338b54a39c
Merge: 748d6101c 8b1934fc3
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Thu Nov 9 19:28:19 2023 +0100

    Merge branch 'master' into MR

commit 748d6101c0
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Thu Nov 9 19:24:17 2023 +0100

    small changes

commit 872fc749e9
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Thu Nov 9 18:49:35 2023 +0100

    fix downloader pause

commit bb96174520
Merge: 59b4404c1 fd83a7e14
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Tue Nov 7 19:08:04 2023 +0100

    Merge branch 'master' into MR

commit 59b4404c11
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Nov 4 22:10:05 2023 +0100

    merge16

    Last commit merged: 16cbcecd99

commit 184804f62e
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Nov 3 23:19:24 2023 +0100

    merge15.5

    Last commit merged: d32409bd6e

commit 6bab100f78
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Nov 3 22:42:04 2023 +0100

    Update ReaderActivity.kt

commit 264b0e6127
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Nov 3 22:30:37 2023 +0100

    merge15

    Last commit merged: cf3f2d0380

commit dd69ce5a12
Merge: d2ccb75c6 5fd00d4a1
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Nov 3 20:14:10 2023 +0100

    Merge branch 'master' into MR

commit d2ccb75c60
Merge: 8f08a246a 5567be64f
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Thu Nov 2 22:04:53 2023 +0100

    Merge branch 'master' into MR

commit 8f08a246a2
Merge: 9509d098a 082d9e339
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Thu Nov 2 20:58:42 2023 +0100

    Merge branch 'master' into MR

commit 9509d098a0
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Wed Nov 1 18:51:57 2023 +0100

    Update AnimeDownloader.kt

commit 5415cbbdef
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Wed Nov 1 18:47:21 2023 +0100

    Update AnimeDownloader.kt

commit f0180e0d15
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Wed Nov 1 15:48:14 2023 +0100

    rework anime downloader

commit f3ec9da2da
Merge: fd1c6437a 67a5bccec
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Tue Oct 31 18:37:41 2023 +0100

    Merge branch 'master' into MR

commit fd1c6437a6
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Oct 29 15:12:39 2023 +0100

    merge14

    Last commit merged: f344831d58

commit 9bb1a5da02
Merge: 03ee4bc5f d8327e872
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Oct 29 13:58:41 2023 +0100

    Merge branch 'master' into MR

commit 03ee4bc5f4
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Oct 28 23:05:13 2023 +0200

    merge13

    Last commit merged:  7f0ed58b54

commit 6dd16ca07d
Merge: f0783f534 509ecedb3
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Oct 28 12:03:04 2023 +0200

    Merge branch 'master' into MR

commit f0783f534d
Author: Quickdev <devesh.ratra@gmail.com>
Date:   Fri Oct 27 15:03:11 2023 -0400

    feat(player): Subtitle settings + refactor + crash fixes (#1152)

    Co-authored-by: jmir1 <jhmiramon@gmail.com>
    Co-authored-by: Abdallah <54363735+abdallahmehiz@users.noreply.github.com>

commit 2431d8a858
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Oct 28 00:04:20 2023 +0200

    merge12

    Last commit merged: 0d9f8e8743

commit c6317e9589
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Oct 27 23:50:37 2023 +0200

    Update AdaptiveSheet.kt

commit 692b566f77
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Oct 27 23:26:45 2023 +0200

    revert subs commit

commit 5b1099da9d
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Oct 27 21:39:46 2023 +0200

    Update AdaptiveSheet.kt

commit 7e14b6d2ab
Merge: f51b36a75 7f9255b51
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Oct 27 21:36:43 2023 +0200

    Merge remote-tracking branch 'upstream/master' into MR

commit f51b36a759
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Oct 27 20:27:23 2023 +0200

    merge11

    Last commit merged: 34b9c82cd0

commit 564a959bab
Merge: b9333bedd 928a62c5a
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Oct 27 18:50:18 2023 +0200

    Merge remote-tracking branch 'origin/master' into MR

commit b9333bedd7
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Thu Oct 26 19:39:29 2023 +0200

    fix download button not shown

commit 5d575f1e90
Merge: e282528e6 afb921a5a
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Thu Oct 26 18:56:43 2023 +0200

    Merge remote-tracking branch 'upstream/master' into MR

commit e282528e6e
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Mon Oct 23 18:27:42 2023 +0200

    Revert "Translations update from Hosted Weblate (#9531)"

    This reverts commit 3a8e7d04fc.

commit 3a8e7d04fc
Author: Weblate (bot) <hosted@weblate.org>
Date:   Sat Jun 3 19:10:13 2023 +0200

    Translations update from Hosted Weblate (#9531)

    Weblate translations

    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ar/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ca/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cs/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hi/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hr/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/id/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/it/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/jv/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ko/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ne/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pl/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/th/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uk/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/vi/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/
    Translation: Tachiyomi/Tachiyomi 0.x

    Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
    Co-authored-by: Ali Aljishi <ahj696@hotmail.com>
    Co-authored-by: AntonP <tony.pug.stark@gmail.com>
    Co-authored-by: Christian Elbrianno <crse@protonmail.ch>
    Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.com>
    Co-authored-by: Dan <denqwerta@gmail.com>
    Co-authored-by: Danel Dave Barbuco <barbucodanel@gmail.com>
    Co-authored-by: DatTran MLL <tranthanhdat1142003@gmail.com>
    Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
    Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
    Co-authored-by: FateXBlood <zecrofelix@gmail.com>
    Co-authored-by: Ferran <ferrancette@gmail.com>
    Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
    Co-authored-by: ID-86 <id86dev@gmail.com>
    Co-authored-by: Igor <zerrxs@gmail.com>
    Co-authored-by: Izxmi <heltherrivas05@gmail.com>
    Co-authored-by: Leonardo Falcoski <leonardo.falcoski@gmail.com>
    Co-authored-by: Lyfja <yassinelaoud@gmail.com>
    Co-authored-by: Milo Ivir <mail@milotype.de>
    Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
    Co-authored-by: Rostyslav Haitkulov <info@ubilling.net.ua>
    Co-authored-by: Swyter <swyterzone@gmail.com>
    Co-authored-by: TheKingTermux <achmadmaulana0233@gmail.com>
    Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
    Co-authored-by: altinat <altinat@duck.com>
    Co-authored-by: stevenlele <stevenlele@outlook.com>

commit 974d3b56f7
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Mon Oct 23 17:51:27 2023 +0200

    Revert "Translations update from Hosted Weblate (#9531)"

    This reverts commit d78159bda8.

commit d78159bda8
Author: Weblate (bot) <hosted@weblate.org>
Date:   Sat Jun 3 19:10:13 2023 +0200

    Translations update from Hosted Weblate (#9531)

    Weblate translations

    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ar/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ca/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cs/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hi/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hr/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/id/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/it/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/jv/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ko/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ne/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pl/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/th/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uk/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/vi/
    Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/
    Translation: Tachiyomi/Tachiyomi 0.x

    Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
    Co-authored-by: Ali Aljishi <ahj696@hotmail.com>
    Co-authored-by: AntonP <tony.pug.stark@gmail.com>
    Co-authored-by: Christian Elbrianno <crse@protonmail.ch>
    Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.com>
    Co-authored-by: Dan <denqwerta@gmail.com>
    Co-authored-by: Danel Dave Barbuco <barbucodanel@gmail.com>
    Co-authored-by: DatTran MLL <tranthanhdat1142003@gmail.com>
    Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
    Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
    Co-authored-by: FateXBlood <zecrofelix@gmail.com>
    Co-authored-by: Ferran <ferrancette@gmail.com>
    Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
    Co-authored-by: ID-86 <id86dev@gmail.com>
    Co-authored-by: Igor <zerrxs@gmail.com>
    Co-authored-by: Izxmi <heltherrivas05@gmail.com>
    Co-authored-by: Leonardo Falcoski <leonardo.falcoski@gmail.com>
    Co-authored-by: Lyfja <yassinelaoud@gmail.com>
    Co-authored-by: Milo Ivir <mail@milotype.de>
    Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
    Co-authored-by: Rostyslav Haitkulov <info@ubilling.net.ua>
    Co-authored-by: Swyter <swyterzone@gmail.com>
    Co-authored-by: TheKingTermux <achmadmaulana0233@gmail.com>
    Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
    Co-authored-by: altinat <altinat@duck.com>
    Co-authored-by: stevenlele <stevenlele@outlook.com>

commit dfb5ca4f8e
Merge: c19b4b5cf 92f9ab276
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Mon Oct 23 17:11:37 2023 +0200

    Merge remote-tracking branch 'upstream/master' into MR

commit c19b4b5cff
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Oct 22 18:25:03 2023 +0200

    fuck lint (again)

commit febf0460c0
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Oct 22 18:16:03 2023 +0200

    fuck lint

commit da3265fb7a
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Oct 22 18:08:27 2023 +0200

    merge10

    Last Commit Merged: 531e1c62bb

commit c85576e06b
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sun Oct 22 14:11:21 2023 +0200

    merge9

    Last Commit Merged: 4c65c2311e

commit 67d3043533
Merge: c62f86f6c 63e95d9cb
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Oct 21 14:54:03 2023 +0200

    Merge branch 'aniyomiorg:master' into MR

commit c62f86f6cc
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Sat Oct 21 12:45:41 2023 +0200

    merge8

    Last Commit Merged: ed5a56be60

commit e99e4c2f41
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Oct 6 18:47:44 2023 +0200

    merge7

    Last Commit Merged: 152fdec855

commit b3c911ea28
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Fri Oct 6 16:51:34 2023 +0200

    merge6

    Last Commit Merged: 22a4372583

commit f705e19182
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Thu Oct 5 14:16:41 2023 +0200

    merge5

    Last Commit Merged: b4bb855675

commit 2ebf477bd1
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Wed Oct 4 21:44:23 2023 +0200

    merge4

    Last Commit Merged: 44383ff950

commit 14a8aebc60
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Wed Oct 4 21:33:37 2023 +0200

    merge3

    I hate Weblate commits!
    Last Commit Merged: e15b945e16

commit 5ceae3116b
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Wed Oct 4 20:24:51 2023 +0200

    merge2

    Last Commit Merged: 9a10656bf0

commit 5a2a3fd080
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Wed Oct 4 13:19:33 2023 +0200

    fuck lint

commit c3062d2ed7
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Wed Oct 4 13:08:36 2023 +0200

    merge1

    Last Commit Merged: f63573f25f

commit 928a62c5a3
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Tue Jul 25 00:25:30 2023 +0200

    Update README.md

commit a883c90cd6
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Tue Jul 25 00:24:31 2023 +0200

    Delete ic_launcher.png

commit c1b7b89c63
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Tue Jul 25 00:23:44 2023 +0200

    Update README.md

commit c18830714b
Author: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com>
Date:   Tue Jul 25 00:22:16 2023 +0200

    Add files via upload
This commit is contained in:
LuftVerbot 2023-11-27 23:08:16 +01:00 committed by jmir1
parent adb571fcbd
commit fd830ea2d0
No known key found for this signature in database
GPG key ID: 7B3B624787A072BD
1062 changed files with 66171 additions and 40416 deletions

View file

@ -1,7 +1,8 @@
[*.{kt,kts}] [*.{kt,kts}]
indent_size=4 max_line_length = 120
insert_final_newline=true indent_size = 4
ij_kotlin_allow_trailing_comma=true insert_final_newline = true
ij_kotlin_allow_trailing_comma_on_call_site=true ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_name_count_to_use_star_import = 2147483647 ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 ij_kotlin_name_count_to_use_star_import_for_members = 2147483647

View file

@ -5,7 +5,7 @@ I acknowledge that:
- I have updated: - I have updated:
- To the latest version of the app (stable is v0.12.3.10) - To the latest version of the app (stable is v0.12.3.10)
- All extensions - All extensions
- I have gone through the FAQ (https://aniyomi.org/help/faq/) and troubleshooting guide (https://aniyomi.org/help/guides/troubleshooting/) - I have gone through the FAQ (https://aniyomi.org/docs/faq/general) and troubleshooting guide (https://aniyomi.org/docs/guides/troubleshooting/)
- If this is an issue with an anime extension, that I should be opening an issue in https://github.com/aniyomiorg/aniyomi-extensions - If this is an issue with an anime extension, that I should be opening an issue in https://github.com/aniyomiorg/aniyomi-extensions
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue - I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue
- I will fill out the title and the information in this template - I will fill out the title and the information in this template

View file

@ -4,7 +4,7 @@ contact_links:
url: https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose url: https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose
about: Issues and requests for extensions and sources should be opened in the aniyomi-extensions repository instead about: Issues and requests for extensions and sources should be opened in the aniyomi-extensions repository instead
- name: 📦 Aniyomi extensions - name: 📦 Aniyomi extensions
url: https://aniyomi.org/extensions url: https://aniyomi.org/extensions/
about: Anime extensions and sources about: Anime extensions and sources
- name: 🧑‍💻 Aniyomi help discord - name: 🧑‍💻 Aniyomi help discord
url: https://discord.gg/F32UjdJZrR url: https://discord.gg/F32UjdJZrR

View file

@ -95,7 +95,7 @@ body:
required: true required: true
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose). - label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose).
required: true required: true
- label: I have gone through the [FAQ](https://aniyomi.org/help/faq/) and [troubleshooting guide](https://aniyomi.org/help/guides/troubleshooting/). - label: I have gone through the [FAQ](https://aniyomi.org/docs/faq/general) and [troubleshooting guide](https://aniyomi.org/docs/guides/troubleshooting/).
required: true required: true
- label: I have updated the app to version **[0.12.3.10](https://github.com/aniyomiorg/aniyomi/releases/latest)**. - label: I have updated the app to version **[0.12.3.10](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
required: true required: true

11
.github/renovate.json vendored
View file

@ -1,11 +0,0 @@
{
"extends": [
"config:base"
],
"schedule": ["every sunday"],
"ignoreDeps": [
"androidx.core:core-splashscreen",
"com.android.tools:r8",
"com.google.guava:guava"
]
}

17
.github/renovate.json5 vendored Normal file
View file

@ -0,0 +1,17 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
],
"schedule": ["every sunday"],
"packageRules": [
{
// Compiler plugins are tightly coupled to Kotlin version
"groupName": "Kotlin",
"matchPackagePrefixes": [
"androidx.compose.compiler",
"org.jetbrains.kotlin",
],
}
]
}

View file

@ -20,7 +20,7 @@ jobs:
steps: steps:
- name: Clone repo - name: Clone repo
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v1
@ -37,4 +37,4 @@ jobs:
- name: Build app and run unit tests - name: Build app and run unit tests
uses: gradle/gradle-command-action@v2 uses: gradle/gradle-command-action@v2
with: with:
arguments: lintKotlin assembleStandardRelease testStandardReleaseUnitTest arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest

View file

@ -22,7 +22,7 @@ jobs:
steps: steps:
- name: Clone repo - name: Clone repo
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v1
@ -46,7 +46,7 @@ jobs:
- name: Build app and run unit tests - name: Build app and run unit tests
uses: gradle/gradle-command-action@v2 uses: gradle/gradle-command-action@v2
with: with:
arguments: lintKotlin assembleStandardRelease testStandardReleaseUnitTest arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest
# Sign APK and create release for tags # Sign APK and create release for tags
@ -120,3 +120,14 @@ jobs:
prerelease: false prerelease: false
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
update-website:
needs: [ build ]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'aniyomiorg/aniyomi'
steps:
- name: Trigger Netlify build hook
run: curl -s -X POST -d {} "https://api.netlify.com/build_hooks/${TOKEN}"
env:
TOKEN: ${{ secrets.NETLIFY_HOOK_RELEASE }}

View file

@ -27,6 +27,13 @@ jobs:
"type": "body", "type": "body",
"regex": ".*\\* (Aniyomi version|Android version|Device): \\?.*", "regex": ".*\\* (Aniyomi version|Android version|Device): \\?.*",
"message": "Requested information in the template was not filled out." "message": "Requested information in the template was not filled out."
},
{
"type": "both",
"regex": ".*(?:fail(?:ed|ure|s)?|can\\s*(?:no|')?t|(?:not|un).*able|(?<!n[o']?t )blocked by|error) (?:to )?(?:get past|by ?pass|penetrate)?.*cloud ?fl?are.*",
"ignoreCase": true,
"labels": ["Cloudflare protected"],
"message": "Refer to the **Solving Cloudflare issues** section at https://aniyomi.org/docs/guides/troubleshooting/#cloudflare. If it doesn't work, migrate to other sources or wait until they lower their protection."
} }
] ]
auto-close-ignore-label: do-not-autoclose auto-close-ignore-label: do-not-autoclose

View file

@ -12,7 +12,7 @@ jobs:
lock: lock:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v4 - uses: dessant/lock-threads@v5
with: with:
github-token: ${{ github.token }} github-token: ${{ github.token }}
issue-inactive-days: '2' issue-inactive-days: '2'

3
.gitignore vendored
View file

@ -3,7 +3,8 @@
/acra.properties /acra.properties
/.idea/workspace.xml /.idea/workspace.xml
.DS_Store .DS_Store
.idea/ .idea/*
!.idea/icon.png
*iml *iml
*.iml *.iml

BIN
.idea/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -24,13 +24,17 @@ Before you start, please note that the ability to use following technologies is
- [Android Studio](https://developer.android.com/studio) - [Android Studio](https://developer.android.com/studio)
- Emulator or phone with developer options enabled to test changes. - Emulator or phone with developer options enabled to test changes.
## Linting
To auto-fix some linting errors, run the `ktlintFormat` Gradle task.
## Getting help ## Getting help
- Join [the Discord server](https://discord.gg/F32UjdJZrR) for online help and to ask questions while developing. - Join [the Discord server](https://discord.gg/F32UjdJZrR) for online help and to ask questions while developing.
# Translations # Translations
Translations are done externally via Weblate. See [our website](https://aniyomi.org/help/contribution/#translation) for more details. Translations are done externally via Weblate. See [our website](https://aniyomi.org/docs/contribute#translation) for more details.
# Forks # Forks

View file

@ -31,7 +31,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
<details><summary>Issues</summary> <details><summary>Issues</summary>
1. **Before reporting a new issue, take a look at the already opened [issues](https://github.com/aniyomiorg/aniyomi/issues).** 1. **Before reporting a new issue, take a look at the already opened [issues](https://aniyomi.org/changelogs/).**
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/841701076242530374?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/F32UjdJZrR) 2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/841701076242530374?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/F32UjdJZrR)
</details> </details>
@ -40,7 +40,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
* Include version (More → About → Version) * Include version (More → About → Version)
* If not latest, try updating, it may have already been solved * If not latest, try updating, it may have already been solved
* Preview version is equal to the number of commits as seen in the main page * Preview version is equal to the number of commits as seen on the main page
* Include steps to reproduce (if not obvious from description) * Include steps to reproduce (if not obvious from description)
* Include screenshot (if needed) * Include screenshot (if needed)
* If it could be device-dependent, try reproducing on another device (if possible) * If it could be device-dependent, try reproducing on another device (if possible)

3
app/.gitignore vendored
View file

@ -1,4 +1,3 @@
/build /build
*iml *iml
*.iml *.iml
custom.gradle

3
app/.idea/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
app/.idea/.name Normal file
View file

@ -0,0 +1 @@
MangaDownloader.kt

7
app/.idea/discord.xml Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
</component>
</project>

18
app/.idea/gradle.xml Normal file
View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

3
app/.idea/misc.xml Normal file
View file

@ -0,0 +1,3 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
</project>

6
app/.idea/vcs.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View file

@ -1,5 +1,4 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jmailen.gradle.kotlinter.tasks.LintTask
import java.io.FileInputStream import java.io.FileInputStream
import java.util.Properties import java.util.Properties
@ -21,8 +20,8 @@ android {
defaultConfig { defaultConfig {
applicationId = "xyz.jmir.tachiyomi.mi" applicationId = "xyz.jmir.tachiyomi.mi"
versionCode = 106 versionCode = 110
versionName = "0.14.6" versionName = "0.14.7"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@ -73,11 +72,11 @@ android {
initWith(getByName("release")) initWith(getByName("release"))
buildConfigField("boolean", "PREVIEW", "true") buildConfigField("boolean", "PREVIEW", "true")
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks.add("release")
val debugType = getByName("debug") val debugType = getByName("debug")
signingConfig = debugType.signingConfig
versionNameSuffix = debugType.versionNameSuffix versionNameSuffix = debugType.versionNameSuffix
applicationIdSuffix = debugType.applicationIdSuffix applicationIdSuffix = debugType.applicationIdSuffix
matchingFallbacks.add("release")
} }
create("benchmark") { create("benchmark") {
initWith(getByName("release")) initWith(getByName("release"))
@ -85,6 +84,7 @@ android {
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
matchingFallbacks.add("release") matchingFallbacks.add("release")
isDebuggable = false isDebuggable = false
isProfileable = true
versionNameSuffix = "-benchmark" versionNameSuffix = "-benchmark"
applicationIdSuffix = ".benchmark" applicationIdSuffix = ".benchmark"
} }
@ -110,15 +110,17 @@ android {
} }
packaging { packaging {
resources.excludes.addAll(listOf( resources.excludes.addAll(
"META-INF/DEPENDENCIES", listOf(
"LICENSE.txt", "META-INF/DEPENDENCIES",
"META-INF/LICENSE", "LICENSE.txt",
"META-INF/LICENSE.txt", "META-INF/LICENSE",
"META-INF/README.md", "META-INF/LICENSE.txt",
"META-INF/NOTICE", "META-INF/README.md",
"META-INF/*.kotlin_module", "META-INF/NOTICE",
)) "META-INF/*.kotlin_module",
),
)
} }
dependenciesInfo { dependenciesInfo {
@ -165,12 +167,13 @@ dependencies {
implementation(compose.material.icons) implementation(compose.material.icons)
implementation(compose.animation) implementation(compose.animation)
implementation(compose.animation.graphics) implementation(compose.animation.graphics)
implementation(compose.ui.tooling) debugImplementation(compose.ui.tooling)
implementation(compose.ui.tooling.preview)
implementation(compose.ui.util) implementation(compose.ui.util)
implementation(compose.accompanist.webview) implementation(compose.accompanist.webview)
implementation(compose.accompanist.permissions) implementation(compose.accompanist.permissions)
implementation(compose.accompanist.themeadapter)
implementation(compose.accompanist.systemuicontroller) implementation(compose.accompanist.systemuicontroller)
lintChecks(compose.lintchecks)
implementation(androidx.paging.runtime) implementation(androidx.paging.runtime)
implementation(androidx.paging.compose) implementation(androidx.paging.compose)
@ -178,6 +181,7 @@ dependencies {
implementation(libs.bundles.sqlite) implementation(libs.bundles.sqlite)
implementation(kotlinx.reflect) implementation(kotlinx.reflect)
implementation(kotlinx.immutables)
implementation(platform(kotlinx.coroutines.bom)) implementation(platform(kotlinx.coroutines.bom))
implementation(kotlinx.bundles.coroutines) implementation(kotlinx.bundles.coroutines)
@ -187,7 +191,6 @@ dependencies {
implementation(androidx.appcompat) implementation(androidx.appcompat)
implementation(androidx.biometricktx) implementation(androidx.biometricktx)
implementation(androidx.constraintlayout) implementation(androidx.constraintlayout)
implementation(androidx.coordinatorlayout)
implementation(androidx.corektx) implementation(androidx.corektx)
implementation(androidx.splashscreen) implementation(androidx.splashscreen)
implementation(androidx.recyclerview) implementation(androidx.recyclerview)
@ -198,10 +201,10 @@ dependencies {
implementation(androidx.bundles.lifecycle) implementation(androidx.bundles.lifecycle)
// Job scheduling // Job scheduling
implementation(androidx.bundles.workmanager) implementation(androidx.workmanager)
// RxJava // RxJava
implementation(libs.bundles.reactivex) implementation(libs.rxjava)
implementation(libs.flowreactivenetwork) implementation(libs.flowreactivenetwork)
// Networking // Networking
@ -227,6 +230,7 @@ dependencies {
implementation(libs.injekt.core) implementation(libs.injekt.core)
// Image loading // Image loading
implementation(platform(libs.coil.bom))
implementation(libs.bundles.coil) implementation(libs.bundles.coil)
implementation(libs.subsamplingscaleimageview) { implementation(libs.subsamplingscaleimageview) {
exclude(module = "image-decoder") exclude(module = "image-decoder")
@ -236,7 +240,6 @@ dependencies {
// UI libraries // UI libraries
implementation(libs.material) implementation(libs.material)
implementation(libs.flexible.adapter.core) implementation(libs.flexible.adapter.core)
implementation(libs.flexible.adapter.ui)
implementation(libs.photoview) implementation(libs.photoview)
implementation(libs.directionalviewpager) { implementation(libs.directionalviewpager) {
exclude(group = "androidx.viewpager", module = "viewpager") exclude(group = "androidx.viewpager", module = "viewpager")
@ -245,9 +248,8 @@ dependencies {
implementation(libs.bundles.richtext) implementation(libs.bundles.richtext)
implementation(libs.aboutLibraries.compose) implementation(libs.aboutLibraries.compose)
implementation(libs.bundles.voyager) implementation(libs.bundles.voyager)
implementation(libs.compose.cascade)
implementation(libs.compose.materialmotion) implementation(libs.compose.materialmotion)
implementation(libs.compose.simpleicons) implementation(libs.swipe)
// Logging // Logging
implementation(libs.logcat) implementation(libs.logcat)
@ -281,7 +283,9 @@ androidComponents {
beforeVariants { variantBuilder -> beforeVariants { variantBuilder ->
// Disables standardBenchmark // Disables standardBenchmark
if (variantBuilder.buildType == "benchmark") { if (variantBuilder.buildType == "benchmark") {
variantBuilder.enable = variantBuilder.productFlavors.containsAll(listOf("default" to "dev")) variantBuilder.enable = variantBuilder.productFlavors.containsAll(
listOf("default" to "dev"),
)
} }
} }
onVariants(selector().withFlavor("default" to "standard")) { onVariants(selector().withFlavor("default" to "standard")) {
@ -292,16 +296,10 @@ androidComponents {
} }
tasks { tasks {
withType<LintTask>().configureEach {
exclude { it.file.path.contains("generated[\\\\/]".toRegex()) }
}
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers) // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
withType<KotlinCompile> { withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf( kotlinOptions.freeCompilerArgs += listOf(
"-Xcontext-receivers", "-Xcontext-receivers",
"-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi", "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi", "-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
@ -310,6 +308,8 @@ tasks {
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi", "-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi", "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
"-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview", "-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi", "-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
@ -320,12 +320,12 @@ tasks {
kotlinOptions.freeCompilerArgs += listOf( kotlinOptions.freeCompilerArgs += listOf(
"-P", "-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
project.buildDir.absolutePath + "/compose_metrics" project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath,
) )
kotlinOptions.freeCompilerArgs += listOf( kotlinOptions.freeCompilerArgs += listOf(
"-P", "-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
project.buildDir.absolutePath + "/compose_metrics" project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath,
) )
} }
} }

View file

@ -14,7 +14,7 @@
} }
-keepclassmembers class * implements android.os.Parcelable { -keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR; public static final ** CREATOR;
} }
-keep class androidx.annotation.Keep -keep class androidx.annotation.Keep

View file

@ -11,10 +11,11 @@
-keep,allowoptimization class kotlin.** { public protected *; } -keep,allowoptimization class kotlin.** { public protected *; }
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; } -keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
-keep,allowoptimization class kotlinx.serialization.** { public protected *; } -keep,allowoptimization class kotlinx.serialization.** { public protected *; }
-keep,allowoptimization class kotlin.time.** { public protected *; }
-keep,allowoptimization class okhttp3.** { public protected *; } -keep,allowoptimization class okhttp3.** { public protected *; }
-keep,allowoptimization class okio.** { public protected *; } -keep,allowoptimization class okio.** { public protected *; }
-keep,allowoptimization class rx.** { public protected *; }
-keep,allowoptimization class org.jsoup.** { public protected *; } -keep,allowoptimization class org.jsoup.** { public protected *; }
-keep,allowoptimization class rx.** { public protected *; }
-keep,allowoptimization class app.cash.quickjs.** { public protected *; } -keep,allowoptimization class app.cash.quickjs.** { public protected *; }
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; } -keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
-keep,allowoptimization class is.xyz.mpv.** { public protected *; } -keep,allowoptimization class is.xyz.mpv.** { public protected *; }
@ -74,4 +75,4 @@
##---------------End: proguard configuration for kotlinx.serialization ---------- ##---------------End: proguard configuration for kotlinx.serialization ----------
# XmlUtil # XmlUtil
-keep public enum nl.adaptivity.xmlutil.EventType { *; } -keep public enum nl.adaptivity.xmlutil.EventType { *; }

View file

@ -1,5 +1,4 @@
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android"> <shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut <shortcut
android:enabled="true" android:enabled="true"
android:icon="@drawable/sc_collections_bookmark_48dp" android:icon="@drawable/sc_collections_bookmark_48dp"

View file

@ -29,21 +29,17 @@
<application <application
android:name=".App" android:name=".App"
android:allowBackup="false" android:allowBackup="false"
android:enableOnBackInvokedCallback="true"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true" android:largeHeap="true"
android:localeConfig="@xml/locales_config" android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.Tachiyomi"
android:supportsRtl="true" android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config"> android:theme="@style/Theme.Tachiyomi">
<!-- enable profiling by macrobenchmark -->
<profileable
android:shell="true"
tools:targetApi="q" />
<activity <activity
android:name=".ui.main.MainActivity" android:name=".ui.main.MainActivity"
@ -67,10 +63,10 @@
<activity <activity
android:name=".ui.main.DeepLinkAnimeActivity" android:name=".ui.deeplink.anime.DeepLinkAnimeActivity"
android:launchMode="singleTask" android:launchMode="singleTask"
android:theme="@android:style/Theme.NoDisplay" android:theme="@android:style/Theme.NoDisplay"
android:label="@string/action_global_anime_search" android:label="@string/action_search"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEARCH" /> <action android:name="android.intent.action.SEARCH" />
@ -94,10 +90,10 @@
</activity> </activity>
<activity <activity
android:name=".ui.main.DeepLinkMangaActivity" android:name=".ui.deeplink.manga.DeepLinkMangaActivity"
android:launchMode="singleTask" android:launchMode="singleTask"
android:theme="@android:style/Theme.NoDisplay" android:theme="@android:style/Theme.NoDisplay"
android:label="@string/action_global_manga_search" android:label="@string/action_search"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEARCH" /> <action android:name="android.intent.action.SEARCH" />
@ -172,8 +168,8 @@
android:exported="false" /> android:exported="false" />
<activity <activity
android:name=".ui.setting.track.AnilistLoginActivity" android:name=".ui.setting.track.TrackLoginActivity"
android:label="Anilist" android:label="@string/track_activity_name"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -181,69 +177,21 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data android:host="anilist-auth"/>
android:host="anilist-auth" <data android:host="bangumi-auth"/>
android:scheme="tachiyomi" /> <data android:host="myanimelist-auth"/>
<data android:host="shikimori-auth"/>
<data android:scheme="tachiyomi"/>
</intent-filter> </intent-filter>
</activity>
<activity
android:name=".ui.setting.track.MyAnimeListLoginActivity"
android:label="MyAnimeList"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data android:host="simkl-auth"/>
android:host="myanimelist-auth" <data android:scheme="aniyomi"/>
android:scheme="tachiyomi" />
</intent-filter>
</activity>
<activity
android:name=".ui.setting.track.ShikimoriLoginActivity"
android:label="Shikimori"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="shikimori-auth"
android:scheme="tachiyomi" />
</intent-filter>
</activity>
<activity
android:name=".ui.setting.track.BangumiLoginActivity"
android:label="Bangumi"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="bangumi-auth"
android:scheme="tachiyomi" />
</intent-filter>
</activity>
<activity
android:name=".ui.setting.track.SimklLoginActivity"
android:label="Simkl"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="simkl-auth"
android:scheme="aniyomi" />
</intent-filter> </intent-filter>
</activity> </activity>
@ -251,34 +199,6 @@
android:name=".data.notification.NotificationReceiver" android:name=".data.notification.NotificationReceiver"
android:exported="false" /> android:exported="false" />
<receiver
android:name="tachiyomi.presentation.widget.entries.manga.MangaUpdatesGridGlanceReceiver"
android:enabled="@bool/glance_appwidget_available"
android:exported="false"
android:label="@string/label_recent_updates">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/updates_grid_glance_widget_info" />
</receiver>
<receiver
android:name="tachiyomi.presentation.widget.entries.anime.AnimeUpdatesGridGlanceReceiver"
android:enabled="@bool/glance_appwidget_available"
android:exported="false"
android:label="@string/label_recent_anime_updates">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/updates_grid_glance_widget_info" />
</receiver>
<service <service
android:name=".data.download.manga.MangaDownloadService" android:name=".data.download.manga.MangaDownloadService"
android:exported="false" /> android:exported="false" />
@ -288,13 +208,11 @@
android:exported="false" /> android:exported="false" />
<service <service
android:name=".data.updater.AppUpdateService" android:name=".extension.manga.util.MangaExtensionInstallService"
android:exported="false" /> android:exported="false" />
<service android:name=".extension.manga.util.MangaExtensionInstallService" <service
android:exported="false" /> android:name=".extension.anime.util.AnimeExtensionInstallService"
<service android:name=".extension.anime.util.AnimeExtensionInstallService"
android:exported="false" /> android:exported="false" />
<service <service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService" android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"

File diff suppressed because it is too large Load diff

View file

@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Response import okhttp3.Response
import rx.Observable
import tachiyomi.core.preference.Preference import tachiyomi.core.preference.Preference
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -27,15 +26,6 @@ interface DataSaver {
} }
} }
fun HttpSource.fetchImage(page: Page, dataSaver: DataSaver): Observable<Response> {
val imageUrl = page.imageUrl ?: return fetchImage(page)
page.imageUrl = dataSaver.compress(imageUrl)
return fetchImage(page)
.doOnNext {
page.imageUrl = imageUrl
}
}
suspend fun HttpSource.getImage(page: Page, dataSaver: DataSaver): Response { suspend fun HttpSource.getImage(page: Page, dataSaver: DataSaver): Response {
val imageUrl = page.imageUrl ?: return getImage(page) val imageUrl = page.imageUrl ?: return getImage(page)
page.imageUrl = dataSaver.compress(imageUrl) page.imageUrl = dataSaver.compress(imageUrl)
@ -74,7 +64,13 @@ private class BandwidthHeroDataSaver(preferences: SourcePreferences) : DataSaver
override fun compress(imageUrl: String): String { override fun compress(imageUrl: String): String {
return if (dataSavedServer.isNotBlank() && !imageUrl.contains(dataSavedServer)) { return if (dataSavedServer.isNotBlank() && !imageUrl.contains(dataSavedServer)) {
when { when {
imageUrl.contains(".jpeg", true) || imageUrl.contains(".jpg", true) -> if (ignoreJpg) imageUrl else getUrl(imageUrl) imageUrl.contains(".jpeg", true) || imageUrl.contains(".jpg", true) -> if (ignoreJpg) {
imageUrl
} else {
getUrl(
imageUrl,
)
}
imageUrl.contains(".gif", true) -> if (ignoreGif) imageUrl else getUrl(imageUrl) imageUrl.contains(".gif", true) -> if (ignoreGif) imageUrl else getUrl(imageUrl)
else -> getUrl(imageUrl) else -> getUrl(imageUrl)
} }
@ -100,7 +96,13 @@ private class WsrvNlDataSaver(preferences: SourcePreferences) : DataSaver {
override fun compress(imageUrl: String): String { override fun compress(imageUrl: String): String {
return when { return when {
imageUrl.contains(".jpeg", true) || imageUrl.contains(".jpg", true) -> if (ignoreJpg) imageUrl else getUrl(imageUrl) imageUrl.contains(".jpeg", true) || imageUrl.contains(".jpg", true) -> if (ignoreJpg) {
imageUrl
} else {
getUrl(
imageUrl,
)
}
imageUrl.contains(".gif", true) -> if (ignoreGif) imageUrl else getUrl(imageUrl) imageUrl.contains(".gif", true) -> if (ignoreGif) imageUrl else getUrl(imageUrl)
else -> getUrl(imageUrl) else -> getUrl(imageUrl)
} }
@ -108,7 +110,11 @@ private class WsrvNlDataSaver(preferences: SourcePreferences) : DataSaver {
private fun getUrl(imageUrl: String): String { private fun getUrl(imageUrl: String): String {
// Network Request sent to wsrv // Network Request sent to wsrv
return "https://wsrv.nl/?url=$imageUrl" + if (imageUrl.contains(".webp", true) || imageUrl.contains(".gif", true)) { return "https://wsrv.nl/?url=$imageUrl" + if (imageUrl.contains(".webp", true) || imageUrl.contains(
".gif",
true,
)
) {
if (!format) { if (!format) {
// Preserve output image extension for animated images(.webp and .gif) // Preserve output image extension for animated images(.webp and .gif)
"&q=$quality&n=-1" "&q=$quality&n=-1"
@ -140,7 +146,13 @@ private class ReSmushItDataSaver(preferences: SourcePreferences) : DataSaver {
override fun compress(imageUrl: String): String { override fun compress(imageUrl: String): String {
return when { return when {
imageUrl.contains(".jpeg", true) || imageUrl.contains(".jpg", true) -> if (ignoreJpg) imageUrl else getUrl(imageUrl) imageUrl.contains(".jpeg", true) || imageUrl.contains(".jpg", true) -> if (ignoreJpg) {
imageUrl
} else {
getUrl(
imageUrl,
)
}
imageUrl.contains(".gif", true) -> if (ignoreGif) imageUrl else getUrl(imageUrl) imageUrl.contains(".gif", true) -> if (ignoreGif) imageUrl else getUrl(imageUrl)
else -> getUrl(imageUrl) else -> getUrl(imageUrl)
} }

View file

@ -31,7 +31,7 @@ class PreferenceMutableState<T>(
} }
override fun component2(): (T) -> Unit { override fun component2(): (T) -> Unit {
return { preference.set(it) } return preference::set
} }
} }

View file

@ -1,7 +1,6 @@
package eu.kanade.core.util package eu.kanade.core.util
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import java.util.concurrent.ConcurrentHashMap
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract import kotlin.contracts.contract
@ -20,15 +19,6 @@ fun <T : R, R : Any> List<T>.insertSeparators(
return newList return newList
} }
/**
* Returns a new map containing only the key entries of [transform] that are not null.
*/
inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): ConcurrentHashMap<R, V> {
val mutableMap = ConcurrentHashMap<R, V>()
forEach { element -> transform(element)?.let { mutableMap[it] = element.value } }
return mutableMap
}
fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) { fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
if (shouldAdd) { if (shouldAdd) {
add(value) add(value)

View file

@ -1,9 +1,11 @@
package eu.kanade.domain package eu.kanade.domain
import eu.kanade.domain.download.anime.interactor.DeleteAnimeDownload import eu.kanade.domain.download.anime.interactor.DeleteEpisodeDownload
import eu.kanade.domain.download.manga.interactor.DeleteChapterDownload import eu.kanade.domain.download.manga.interactor.DeleteChapterDownload
import eu.kanade.domain.entries.anime.interactor.SetAnimeViewerFlags import eu.kanade.domain.entries.anime.interactor.SetAnimeViewerFlags
import eu.kanade.domain.entries.anime.interactor.UpdateAnime import eu.kanade.domain.entries.anime.interactor.UpdateAnime
import eu.kanade.domain.entries.manga.interactor.GetExcludedScanlators
import eu.kanade.domain.entries.manga.interactor.SetExcludedScanlators
import eu.kanade.domain.entries.manga.interactor.SetMangaViewerFlags import eu.kanade.domain.entries.manga.interactor.SetMangaViewerFlags
import eu.kanade.domain.entries.manga.interactor.UpdateManga import eu.kanade.domain.entries.manga.interactor.UpdateManga
import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionLanguages import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionLanguages
@ -12,12 +14,11 @@ import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionsByType
import eu.kanade.domain.extension.manga.interactor.GetExtensionSources import eu.kanade.domain.extension.manga.interactor.GetExtensionSources
import eu.kanade.domain.extension.manga.interactor.GetMangaExtensionLanguages import eu.kanade.domain.extension.manga.interactor.GetMangaExtensionLanguages
import eu.kanade.domain.extension.manga.interactor.GetMangaExtensionsByType import eu.kanade.domain.extension.manga.interactor.GetMangaExtensionsByType
import eu.kanade.domain.items.chapter.interactor.GetAvailableScanlators
import eu.kanade.domain.items.chapter.interactor.SetReadStatus import eu.kanade.domain.items.chapter.interactor.SetReadStatus
import eu.kanade.domain.items.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.items.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.items.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
import eu.kanade.domain.items.episode.interactor.SetSeenStatus import eu.kanade.domain.items.episode.interactor.SetSeenStatus
import eu.kanade.domain.items.episode.interactor.SyncEpisodesWithSource import eu.kanade.domain.items.episode.interactor.SyncEpisodesWithSource
import eu.kanade.domain.items.episode.interactor.SyncEpisodesWithTrackServiceTwoWay
import eu.kanade.domain.source.anime.interactor.GetAnimeSourcesWithFavoriteCount import eu.kanade.domain.source.anime.interactor.GetAnimeSourcesWithFavoriteCount
import eu.kanade.domain.source.anime.interactor.GetEnabledAnimeSources import eu.kanade.domain.source.anime.interactor.GetEnabledAnimeSources
import eu.kanade.domain.source.anime.interactor.GetLanguagesWithAnimeSources import eu.kanade.domain.source.anime.interactor.GetLanguagesWithAnimeSources
@ -30,6 +31,14 @@ import eu.kanade.domain.source.manga.interactor.ToggleMangaSource
import eu.kanade.domain.source.manga.interactor.ToggleMangaSourcePin import eu.kanade.domain.source.manga.interactor.ToggleMangaSourcePin
import eu.kanade.domain.source.service.SetMigrateSorting import eu.kanade.domain.source.service.SetMigrateSorting
import eu.kanade.domain.source.service.ToggleLanguage import eu.kanade.domain.source.service.ToggleLanguage
import eu.kanade.domain.track.anime.interactor.AddAnimeTracks
import eu.kanade.domain.track.anime.interactor.RefreshAnimeTracks
import eu.kanade.domain.track.anime.interactor.SyncEpisodeProgressWithTrack
import eu.kanade.domain.track.anime.interactor.TrackEpisode
import eu.kanade.domain.track.manga.interactor.AddMangaTracks
import eu.kanade.domain.track.manga.interactor.RefreshMangaTracks
import eu.kanade.domain.track.manga.interactor.SyncChapterProgressWithTrack
import eu.kanade.domain.track.manga.interactor.TrackChapter
import tachiyomi.data.category.anime.AnimeCategoryRepositoryImpl import tachiyomi.data.category.anime.AnimeCategoryRepositoryImpl
import tachiyomi.data.category.manga.MangaCategoryRepositoryImpl import tachiyomi.data.category.manga.MangaCategoryRepositoryImpl
import tachiyomi.data.entries.anime.AnimeRepositoryImpl import tachiyomi.data.entries.anime.AnimeRepositoryImpl
@ -38,10 +47,11 @@ import tachiyomi.data.history.anime.AnimeHistoryRepositoryImpl
import tachiyomi.data.history.manga.MangaHistoryRepositoryImpl import tachiyomi.data.history.manga.MangaHistoryRepositoryImpl
import tachiyomi.data.items.chapter.ChapterRepositoryImpl import tachiyomi.data.items.chapter.ChapterRepositoryImpl
import tachiyomi.data.items.episode.EpisodeRepositoryImpl import tachiyomi.data.items.episode.EpisodeRepositoryImpl
import tachiyomi.data.source.anime.AnimeSourceDataRepositoryImpl import tachiyomi.data.release.ReleaseServiceImpl
import tachiyomi.data.source.anime.AnimeSourceRepositoryImpl import tachiyomi.data.source.anime.AnimeSourceRepositoryImpl
import tachiyomi.data.source.manga.MangaSourceDataRepositoryImpl import tachiyomi.data.source.anime.AnimeStubSourceRepositoryImpl
import tachiyomi.data.source.manga.MangaSourceRepositoryImpl import tachiyomi.data.source.manga.MangaSourceRepositoryImpl
import tachiyomi.data.source.manga.MangaStubSourceRepositoryImpl
import tachiyomi.data.track.anime.AnimeTrackRepositoryImpl import tachiyomi.data.track.anime.AnimeTrackRepositoryImpl
import tachiyomi.data.track.manga.MangaTrackRepositoryImpl import tachiyomi.data.track.manga.MangaTrackRepositoryImpl
import tachiyomi.data.updates.anime.AnimeUpdatesRepositoryImpl import tachiyomi.data.updates.anime.AnimeUpdatesRepositoryImpl
@ -55,7 +65,7 @@ import tachiyomi.domain.category.anime.interactor.RenameAnimeCategory
import tachiyomi.domain.category.anime.interactor.ReorderAnimeCategory import tachiyomi.domain.category.anime.interactor.ReorderAnimeCategory
import tachiyomi.domain.category.anime.interactor.ResetAnimeCategoryFlags import tachiyomi.domain.category.anime.interactor.ResetAnimeCategoryFlags
import tachiyomi.domain.category.anime.interactor.SetAnimeCategories import tachiyomi.domain.category.anime.interactor.SetAnimeCategories
import tachiyomi.domain.category.anime.interactor.SetDisplayModeForAnimeCategory import tachiyomi.domain.category.anime.interactor.SetAnimeDisplayMode
import tachiyomi.domain.category.anime.interactor.SetSortModeForAnimeCategory import tachiyomi.domain.category.anime.interactor.SetSortModeForAnimeCategory
import tachiyomi.domain.category.anime.interactor.UpdateAnimeCategory import tachiyomi.domain.category.anime.interactor.UpdateAnimeCategory
import tachiyomi.domain.category.anime.repository.AnimeCategoryRepository import tachiyomi.domain.category.anime.repository.AnimeCategoryRepository
@ -67,25 +77,29 @@ import tachiyomi.domain.category.manga.interactor.HideMangaCategory
import tachiyomi.domain.category.manga.interactor.RenameMangaCategory import tachiyomi.domain.category.manga.interactor.RenameMangaCategory
import tachiyomi.domain.category.manga.interactor.ReorderMangaCategory import tachiyomi.domain.category.manga.interactor.ReorderMangaCategory
import tachiyomi.domain.category.manga.interactor.ResetMangaCategoryFlags import tachiyomi.domain.category.manga.interactor.ResetMangaCategoryFlags
import tachiyomi.domain.category.manga.interactor.SetDisplayModeForMangaCategory
import tachiyomi.domain.category.manga.interactor.SetMangaCategories import tachiyomi.domain.category.manga.interactor.SetMangaCategories
import tachiyomi.domain.category.manga.interactor.SetMangaDisplayMode
import tachiyomi.domain.category.manga.interactor.SetSortModeForMangaCategory import tachiyomi.domain.category.manga.interactor.SetSortModeForMangaCategory
import tachiyomi.domain.category.manga.interactor.UpdateMangaCategory import tachiyomi.domain.category.manga.interactor.UpdateMangaCategory
import tachiyomi.domain.category.manga.repository.MangaCategoryRepository import tachiyomi.domain.category.manga.repository.MangaCategoryRepository
import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval
import tachiyomi.domain.entries.anime.interactor.GetAnime import tachiyomi.domain.entries.anime.interactor.GetAnime
import tachiyomi.domain.entries.anime.interactor.GetAnimeFavorites import tachiyomi.domain.entries.anime.interactor.GetAnimeFavorites
import tachiyomi.domain.entries.anime.interactor.GetAnimeWithEpisodes import tachiyomi.domain.entries.anime.interactor.GetAnimeWithEpisodes
import tachiyomi.domain.entries.anime.interactor.GetDuplicateLibraryAnime import tachiyomi.domain.entries.anime.interactor.GetDuplicateLibraryAnime
import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime
import tachiyomi.domain.entries.anime.interactor.GetMangaByUrlAndSourceId
import tachiyomi.domain.entries.anime.interactor.NetworkToLocalAnime import tachiyomi.domain.entries.anime.interactor.NetworkToLocalAnime
import tachiyomi.domain.entries.anime.interactor.ResetAnimeViewerFlags import tachiyomi.domain.entries.anime.interactor.ResetAnimeViewerFlags
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
import tachiyomi.domain.entries.anime.repository.AnimeRepository import tachiyomi.domain.entries.anime.repository.AnimeRepository
import tachiyomi.domain.entries.manga.interactor.GetAnimeByUrlAndSourceId
import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
import tachiyomi.domain.entries.manga.interactor.GetManga import tachiyomi.domain.entries.manga.interactor.GetManga
import tachiyomi.domain.entries.manga.interactor.GetMangaFavorites import tachiyomi.domain.entries.manga.interactor.GetMangaFavorites
import tachiyomi.domain.entries.manga.interactor.GetMangaWithChapters import tachiyomi.domain.entries.manga.interactor.GetMangaWithChapters
import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval
import tachiyomi.domain.entries.manga.interactor.NetworkToLocalManga import tachiyomi.domain.entries.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.entries.manga.interactor.ResetMangaViewerFlags import tachiyomi.domain.entries.manga.interactor.ResetMangaViewerFlags
import tachiyomi.domain.entries.manga.interactor.SetMangaChapterFlags import tachiyomi.domain.entries.manga.interactor.SetMangaChapterFlags
@ -102,25 +116,29 @@ import tachiyomi.domain.history.manga.interactor.RemoveMangaHistory
import tachiyomi.domain.history.manga.interactor.UpsertMangaHistory import tachiyomi.domain.history.manga.interactor.UpsertMangaHistory
import tachiyomi.domain.history.manga.repository.MangaHistoryRepository import tachiyomi.domain.history.manga.repository.MangaHistoryRepository
import tachiyomi.domain.items.chapter.interactor.GetChapter import tachiyomi.domain.items.chapter.interactor.GetChapter
import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId import tachiyomi.domain.items.chapter.interactor.GetChapterByUrlAndMangaId
import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.items.chapter.interactor.SetMangaDefaultChapterFlags import tachiyomi.domain.items.chapter.interactor.SetMangaDefaultChapterFlags
import tachiyomi.domain.items.chapter.interactor.ShouldUpdateDbChapter import tachiyomi.domain.items.chapter.interactor.ShouldUpdateDbChapter
import tachiyomi.domain.items.chapter.interactor.UpdateChapter import tachiyomi.domain.items.chapter.interactor.UpdateChapter
import tachiyomi.domain.items.chapter.repository.ChapterRepository import tachiyomi.domain.items.chapter.repository.ChapterRepository
import tachiyomi.domain.items.episode.interactor.GetEpisode import tachiyomi.domain.items.episode.interactor.GetEpisode
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId import tachiyomi.domain.items.episode.interactor.GetEpisodeByUrlAndAnimeId
import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId
import tachiyomi.domain.items.episode.interactor.SetAnimeDefaultEpisodeFlags import tachiyomi.domain.items.episode.interactor.SetAnimeDefaultEpisodeFlags
import tachiyomi.domain.items.episode.interactor.ShouldUpdateDbEpisode import tachiyomi.domain.items.episode.interactor.ShouldUpdateDbEpisode
import tachiyomi.domain.items.episode.interactor.UpdateEpisode import tachiyomi.domain.items.episode.interactor.UpdateEpisode
import tachiyomi.domain.items.episode.repository.EpisodeRepository import tachiyomi.domain.items.episode.repository.EpisodeRepository
import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.domain.release.service.ReleaseService
import tachiyomi.domain.source.anime.interactor.GetAnimeSourcesWithNonLibraryAnime import tachiyomi.domain.source.anime.interactor.GetAnimeSourcesWithNonLibraryAnime
import tachiyomi.domain.source.anime.interactor.GetRemoteAnime import tachiyomi.domain.source.anime.interactor.GetRemoteAnime
import tachiyomi.domain.source.anime.repository.AnimeSourceDataRepository
import tachiyomi.domain.source.anime.repository.AnimeSourceRepository import tachiyomi.domain.source.anime.repository.AnimeSourceRepository
import tachiyomi.domain.source.anime.repository.AnimeStubSourceRepository
import tachiyomi.domain.source.manga.interactor.GetMangaSourcesWithNonLibraryManga import tachiyomi.domain.source.manga.interactor.GetMangaSourcesWithNonLibraryManga
import tachiyomi.domain.source.manga.interactor.GetRemoteManga import tachiyomi.domain.source.manga.interactor.GetRemoteManga
import tachiyomi.domain.source.manga.repository.MangaSourceDataRepository
import tachiyomi.domain.source.manga.repository.MangaSourceRepository import tachiyomi.domain.source.manga.repository.MangaSourceRepository
import tachiyomi.domain.source.manga.repository.MangaStubSourceRepository
import tachiyomi.domain.track.anime.interactor.DeleteAnimeTrack import tachiyomi.domain.track.anime.interactor.DeleteAnimeTrack
import tachiyomi.domain.track.anime.interactor.GetAnimeTracks import tachiyomi.domain.track.anime.interactor.GetAnimeTracks
import tachiyomi.domain.track.anime.interactor.GetTracksPerAnime import tachiyomi.domain.track.anime.interactor.GetTracksPerAnime
@ -148,7 +166,7 @@ class DomainModule : InjektModule {
addFactory { GetAnimeCategories(get()) } addFactory { GetAnimeCategories(get()) }
addFactory { GetVisibleAnimeCategories(get()) } addFactory { GetVisibleAnimeCategories(get()) }
addFactory { ResetAnimeCategoryFlags(get(), get()) } addFactory { ResetAnimeCategoryFlags(get(), get()) }
addFactory { SetDisplayModeForAnimeCategory(get(), get()) } addFactory { SetAnimeDisplayMode(get()) }
addFactory { SetSortModeForAnimeCategory(get(), get()) } addFactory { SetSortModeForAnimeCategory(get(), get()) }
addFactory { CreateAnimeCategoryWithName(get(), get()) } addFactory { CreateAnimeCategoryWithName(get(), get()) }
addFactory { RenameAnimeCategory(get()) } addFactory { RenameAnimeCategory(get()) }
@ -161,7 +179,7 @@ class DomainModule : InjektModule {
addFactory { GetMangaCategories(get()) } addFactory { GetMangaCategories(get()) }
addFactory { GetVisibleMangaCategories(get()) } addFactory { GetVisibleMangaCategories(get()) }
addFactory { ResetMangaCategoryFlags(get(), get()) } addFactory { ResetMangaCategoryFlags(get(), get()) }
addFactory { SetDisplayModeForMangaCategory(get(), get()) } addFactory { SetMangaDisplayMode(get()) }
addFactory { SetSortModeForMangaCategory(get(), get()) } addFactory { SetSortModeForMangaCategory(get(), get()) }
addFactory { CreateMangaCategoryWithName(get(), get()) } addFactory { CreateMangaCategoryWithName(get(), get()) }
addFactory { RenameMangaCategory(get()) } addFactory { RenameMangaCategory(get()) }
@ -175,14 +193,16 @@ class DomainModule : InjektModule {
addFactory { GetAnimeFavorites(get()) } addFactory { GetAnimeFavorites(get()) }
addFactory { GetLibraryAnime(get()) } addFactory { GetLibraryAnime(get()) }
addFactory { GetAnimeWithEpisodes(get(), get()) } addFactory { GetAnimeWithEpisodes(get(), get()) }
addFactory { GetAnimeByUrlAndSourceId(get()) }
addFactory { GetAnime(get()) } addFactory { GetAnime(get()) }
addFactory { GetNextEpisodes(get(), get(), get()) } addFactory { GetNextEpisodes(get(), get(), get()) }
addFactory { ResetAnimeViewerFlags(get()) } addFactory { ResetAnimeViewerFlags(get()) }
addFactory { SetAnimeEpisodeFlags(get()) } addFactory { SetAnimeEpisodeFlags(get()) }
addFactory { AnimeFetchInterval(get()) }
addFactory { SetAnimeDefaultEpisodeFlags(get(), get(), get()) } addFactory { SetAnimeDefaultEpisodeFlags(get(), get(), get()) }
addFactory { SetAnimeViewerFlags(get()) } addFactory { SetAnimeViewerFlags(get()) }
addFactory { NetworkToLocalAnime(get()) } addFactory { NetworkToLocalAnime(get()) }
addFactory { UpdateAnime(get()) } addFactory { UpdateAnime(get(), get()) }
addFactory { SetAnimeCategories(get()) } addFactory { SetAnimeCategories(get()) }
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) } addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
@ -190,10 +210,12 @@ class DomainModule : InjektModule {
addFactory { GetMangaFavorites(get()) } addFactory { GetMangaFavorites(get()) }
addFactory { GetLibraryManga(get()) } addFactory { GetLibraryManga(get()) }
addFactory { GetMangaWithChapters(get(), get()) } addFactory { GetMangaWithChapters(get(), get()) }
addFactory { GetMangaByUrlAndSourceId(get()) }
addFactory { GetManga(get()) } addFactory { GetManga(get()) }
addFactory { GetNextChapters(get(), get(), get()) } addFactory { GetNextChapters(get(), get(), get()) }
addFactory { ResetMangaViewerFlags(get()) } addFactory { ResetMangaViewerFlags(get()) }
addFactory { SetMangaChapterFlags(get()) } addFactory { SetMangaChapterFlags(get()) }
addFactory { MangaFetchInterval(get()) }
addFactory { addFactory {
SetMangaDefaultChapterFlags( SetMangaDefaultChapterFlags(
get(), get(),
@ -203,45 +225,59 @@ class DomainModule : InjektModule {
} }
addFactory { SetMangaViewerFlags(get()) } addFactory { SetMangaViewerFlags(get()) }
addFactory { NetworkToLocalManga(get()) } addFactory { NetworkToLocalManga(get()) }
addFactory { UpdateManga(get()) } addFactory { UpdateManga(get(), get()) }
addFactory { SetMangaCategories(get()) } addFactory { SetMangaCategories(get()) }
addFactory { GetExcludedScanlators(get()) }
addFactory { SetExcludedScanlators(get()) }
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
addFactory { GetApplicationRelease(get(), get()) }
addSingletonFactory<AnimeTrackRepository> { AnimeTrackRepositoryImpl(get()) } addSingletonFactory<AnimeTrackRepository> { AnimeTrackRepositoryImpl(get()) }
addFactory { TrackEpisode(get(), get(), get(), get()) }
addFactory { AddAnimeTracks(get(), get(), get(), get()) }
addFactory { RefreshAnimeTracks(get(), get(), get(), get()) }
addFactory { DeleteAnimeTrack(get()) } addFactory { DeleteAnimeTrack(get()) }
addFactory { GetTracksPerAnime(get()) } addFactory { GetTracksPerAnime(get()) }
addFactory { GetAnimeTracks(get()) } addFactory { GetAnimeTracks(get()) }
addFactory { InsertAnimeTrack(get()) } addFactory { InsertAnimeTrack(get()) }
addFactory { SyncEpisodeProgressWithTrack(get(), get(), get()) }
addSingletonFactory<MangaTrackRepository> { MangaTrackRepositoryImpl(get()) } addSingletonFactory<MangaTrackRepository> { MangaTrackRepositoryImpl(get()) }
addFactory { TrackChapter(get(), get(), get(), get()) }
addFactory { AddMangaTracks(get(), get(), get(), get()) }
addFactory { RefreshMangaTracks(get(), get(), get(), get()) }
addFactory { DeleteMangaTrack(get()) } addFactory { DeleteMangaTrack(get()) }
addFactory { GetTracksPerManga(get()) } addFactory { GetTracksPerManga(get()) }
addFactory { GetMangaTracks(get()) } addFactory { GetMangaTracks(get()) }
addFactory { InsertMangaTrack(get()) } addFactory { InsertMangaTrack(get()) }
addFactory { SyncChapterProgressWithTrack(get(), get(), get()) }
addSingletonFactory<EpisodeRepository> { EpisodeRepositoryImpl(get()) } addSingletonFactory<EpisodeRepository> { EpisodeRepositoryImpl(get()) }
addFactory { GetEpisode(get()) } addFactory { GetEpisode(get()) }
addFactory { GetEpisodeByAnimeId(get()) } addFactory { GetEpisodesByAnimeId(get()) }
addFactory { GetEpisodeByUrlAndAnimeId(get()) }
addFactory { UpdateEpisode(get()) } addFactory { UpdateEpisode(get()) }
addFactory { SetSeenStatus(get(), get(), get(), get()) } addFactory { SetSeenStatus(get(), get(), get(), get()) }
addFactory { ShouldUpdateDbEpisode() } addFactory { ShouldUpdateDbEpisode() }
addFactory { SyncEpisodesWithSource(get(), get(), get(), get()) } addFactory { SyncEpisodesWithSource(get(), get(), get(), get(), get(), get(), get()) }
addFactory { SyncEpisodesWithTrackServiceTwoWay(get(), get()) }
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) } addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
addFactory { GetChapter(get()) } addFactory { GetChapter(get()) }
addFactory { GetChapterByMangaId(get()) } addFactory { GetChaptersByMangaId(get()) }
addFactory { GetChapterByUrlAndMangaId(get()) }
addFactory { UpdateChapter(get()) } addFactory { UpdateChapter(get()) }
addFactory { SetReadStatus(get(), get(), get(), get()) } addFactory { SetReadStatus(get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() } addFactory { ShouldUpdateDbChapter() }
addFactory { SyncChaptersWithSource(get(), get(), get(), get()) } addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) }
addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get()) } addFactory { GetAvailableScanlators(get()) }
addSingletonFactory<AnimeHistoryRepository> { AnimeHistoryRepositoryImpl(get()) } addSingletonFactory<AnimeHistoryRepository> { AnimeHistoryRepositoryImpl(get()) }
addFactory { GetAnimeHistory(get()) } addFactory { GetAnimeHistory(get()) }
addFactory { UpsertAnimeHistory(get()) } addFactory { UpsertAnimeHistory(get()) }
addFactory { RemoveAnimeHistory(get()) } addFactory { RemoveAnimeHistory(get()) }
addFactory { DeleteAnimeDownload(get(), get()) } addFactory { DeleteEpisodeDownload(get(), get()) }
addFactory { GetAnimeExtensionsByType(get(), get()) } addFactory { GetAnimeExtensionsByType(get(), get()) }
addFactory { GetAnimeExtensionSources(get()) } addFactory { GetAnimeExtensionSources(get()) }
@ -266,7 +302,7 @@ class DomainModule : InjektModule {
addFactory { GetMangaUpdates(get()) } addFactory { GetMangaUpdates(get()) }
addSingletonFactory<AnimeSourceRepository> { AnimeSourceRepositoryImpl(get(), get()) } addSingletonFactory<AnimeSourceRepository> { AnimeSourceRepositoryImpl(get(), get()) }
addSingletonFactory<AnimeSourceDataRepository> { AnimeSourceDataRepositoryImpl(get()) } addSingletonFactory<AnimeStubSourceRepository> { AnimeStubSourceRepositoryImpl(get()) }
addFactory { GetEnabledAnimeSources(get(), get()) } addFactory { GetEnabledAnimeSources(get(), get()) }
addFactory { GetLanguagesWithAnimeSources(get(), get()) } addFactory { GetLanguagesWithAnimeSources(get(), get()) }
addFactory { GetRemoteAnime(get()) } addFactory { GetRemoteAnime(get()) }
@ -276,7 +312,7 @@ class DomainModule : InjektModule {
addFactory { ToggleAnimeSourcePin(get()) } addFactory { ToggleAnimeSourcePin(get()) }
addSingletonFactory<MangaSourceRepository> { MangaSourceRepositoryImpl(get(), get()) } addSingletonFactory<MangaSourceRepository> { MangaSourceRepositoryImpl(get(), get()) }
addSingletonFactory<MangaSourceDataRepository> { MangaSourceDataRepositoryImpl(get()) } addSingletonFactory<MangaStubSourceRepository> { MangaStubSourceRepositoryImpl(get()) }
addFactory { GetEnabledMangaSources(get(), get()) } addFactory { GetEnabledMangaSources(get(), get()) }
addFactory { GetLanguagesWithMangaSources(get(), get()) } addFactory { GetLanguagesWithMangaSources(get(), get()) }
addFactory { GetRemoteManga(get()) } addFactory { GetRemoteManga(get()) }

View file

@ -3,9 +3,11 @@ package eu.kanade.domain.base
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.isPreviewBuildType import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isReleaseBuildType import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
class BasePreferences( class BasePreferences(
@ -13,21 +15,28 @@ class BasePreferences(
private val preferenceStore: PreferenceStore, private val preferenceStore: PreferenceStore,
) { ) {
fun confirmExit() = preferenceStore.getBoolean("pref_confirm_exit", false) fun downloadedOnly() = preferenceStore.getBoolean(
Preference.appStateKey("pref_downloaded_only"),
false,
)
fun downloadedOnly() = preferenceStore.getBoolean("pref_downloaded_only", false) fun incognitoMode() = preferenceStore.getBoolean(Preference.appStateKey("incognito_mode"), false)
fun incognitoMode() = preferenceStore.getBoolean("incognito_mode", false)
fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore) fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)
fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType) fun acraEnabled() = preferenceStore.getBoolean(
"acra.enable",
isPreviewBuildType || isReleaseBuildType,
)
fun deviceHasPip() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && context.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) fun deviceHasPip() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && context.packageManager.hasSystemFeature(
PackageManager.FEATURE_PICTURE_IN_PICTURE,
)
enum class ExtensionInstaller(val titleResId: Int) { enum class ExtensionInstaller(@StringRes val titleResId: Int) {
LEGACY(R.string.ext_installer_legacy), LEGACY(R.string.ext_installer_legacy),
PACKAGEINSTALLER(R.string.ext_installer_packageinstaller), PACKAGEINSTALLER(R.string.ext_installer_packageinstaller),
SHIZUKU(R.string.ext_installer_shizuku), SHIZUKU(R.string.ext_installer_shizuku),
PRIVATE(R.string.ext_installer_private),
} }
} }

View file

@ -18,7 +18,7 @@ class ExtensionInstallerPreference(
override fun key() = "extension_installer" override fun key() = "extension_installer"
val entries get() = ExtensionInstaller.values().run { val entries get() = ExtensionInstaller.entries.run {
if (context.hasMiuiPackageInstaller) { if (context.hasMiuiPackageInstaller) {
filter { it != ExtensionInstaller.PACKAGEINSTALLER } filter { it != ExtensionInstaller.PACKAGEINSTALLER }
} else { } else {

View file

@ -6,7 +6,7 @@ import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.items.episode.model.Episode import tachiyomi.domain.items.episode.model.Episode
import tachiyomi.domain.source.anime.service.AnimeSourceManager import tachiyomi.domain.source.anime.service.AnimeSourceManager
class DeleteAnimeDownload( class DeleteEpisodeDownload(
private val sourceManager: AnimeSourceManager, private val sourceManager: AnimeSourceManager,
private val downloadManager: AnimeDownloadManager, private val downloadManager: AnimeDownloadManager,
) { ) {

View file

@ -3,16 +3,19 @@ package eu.kanade.domain.entries.anime.interactor
import eu.kanade.domain.entries.anime.model.hasCustomCover import eu.kanade.domain.entries.anime.model.hasCustomCover
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.entries.anime.model.AnimeUpdate import tachiyomi.domain.entries.anime.model.AnimeUpdate
import tachiyomi.domain.entries.anime.repository.AnimeRepository import tachiyomi.domain.entries.anime.repository.AnimeRepository
import tachiyomi.source.local.entries.anime.isLocal import tachiyomi.source.local.entries.anime.isLocal
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.time.ZonedDateTime
import java.util.Date import java.util.Date
class UpdateAnime( class UpdateAnime(
private val animeRepository: AnimeRepository, private val animeRepository: AnimeRepository,
private val animeFetchInterval: AnimeFetchInterval,
) { ) {
suspend fun await(animeUpdate: AnimeUpdate): Boolean { suspend fun await(animeUpdate: AnimeUpdate): Boolean {
@ -73,12 +76,24 @@ class UpdateAnime(
) )
} }
suspend fun awaitUpdateFetchInterval(
anime: Anime,
dateTime: ZonedDateTime = ZonedDateTime.now(),
window: Pair<Long, Long> = animeFetchInterval.getWindow(dateTime),
): Boolean {
return animeFetchInterval.toAnimeUpdateOrNull(anime, dateTime, window)
?.let { animeRepository.updateAnime(it) }
?: false
}
suspend fun awaitUpdateLastUpdate(animeId: Long): Boolean { suspend fun awaitUpdateLastUpdate(animeId: Long): Boolean {
return animeRepository.updateAnime(AnimeUpdate(id = animeId, lastUpdate = Date().time)) return animeRepository.updateAnime(AnimeUpdate(id = animeId, lastUpdate = Date().time))
} }
suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean { suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean {
return animeRepository.updateAnime(AnimeUpdate(id = mangaId, coverLastModified = Date().time)) return animeRepository.updateAnime(
AnimeUpdate(id = mangaId, coverLastModified = Date().time),
)
} }
suspend fun awaitUpdateFavorite(animeId: Long, favorite: Boolean): Boolean { suspend fun awaitUpdateFavorite(animeId: Long, favorite: Boolean): Boolean {

View file

@ -3,24 +3,24 @@ package eu.kanade.domain.entries.anime.model
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
import tachiyomi.domain.entries.TriStateFilter import tachiyomi.core.preference.TriState
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
val Anime.downloadedFilter: TriStateFilter val Anime.downloadedFilter: TriState
get() { get() {
if (forceDownloaded()) return TriStateFilter.ENABLED_IS if (forceDownloaded()) return TriState.ENABLED_IS
return when (downloadedFilterRaw) { return when (downloadedFilterRaw) {
Anime.EPISODE_SHOW_DOWNLOADED -> TriStateFilter.ENABLED_IS Anime.EPISODE_SHOW_DOWNLOADED -> TriState.ENABLED_IS
Anime.EPISODE_SHOW_NOT_DOWNLOADED -> TriStateFilter.ENABLED_NOT Anime.EPISODE_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
else -> TriStateFilter.DISABLED else -> TriState.DISABLED
} }
} }
fun Anime.episodesFiltered(): Boolean { fun Anime.episodesFiltered(): Boolean {
return unseenFilter != TriStateFilter.DISABLED || return unseenFilter != TriState.DISABLED ||
downloadedFilter != TriStateFilter.DISABLED || downloadedFilter != TriState.DISABLED ||
bookmarkedFilter != TriStateFilter.DISABLED bookmarkedFilter != TriState.DISABLED
} }
fun Anime.forceDownloaded(): Boolean { fun Anime.forceDownloaded(): Boolean {
return favorite && Injekt.get<BasePreferences>().downloadedOnly().get() return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()

View file

@ -0,0 +1,24 @@
package eu.kanade.domain.entries.manga.interactor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import tachiyomi.data.handlers.manga.MangaDatabaseHandler
class GetExcludedScanlators(
private val handler: MangaDatabaseHandler,
) {
suspend fun await(mangaId: Long): Set<String> {
return handler.awaitList {
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
}
.toSet()
}
fun subscribe(mangaId: Long): Flow<Set<String>> {
return handler.subscribeToList {
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
}
.map { it.toSet() }
}
}

View file

@ -0,0 +1,22 @@
package eu.kanade.domain.entries.manga.interactor
import tachiyomi.data.handlers.manga.MangaDatabaseHandler
class SetExcludedScanlators(
private val handler: MangaDatabaseHandler,
) {
suspend fun await(mangaId: Long, excludedScanlators: Set<String>) {
handler.await(inTransaction = true) {
val currentExcluded = handler.awaitList {
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
}.toSet()
val toAdd = excludedScanlators.minus(currentExcluded)
for (scanlator in toAdd) {
excluded_scanlatorsQueries.insert(mangaId, scanlator)
}
val toRemove = currentExcluded.minus(excludedScanlators)
excluded_scanlatorsQueries.remove(mangaId, toRemove)
}
}
}

View file

@ -1,7 +1,7 @@
package eu.kanade.domain.entries.manga.interactor package eu.kanade.domain.entries.manga.interactor
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import tachiyomi.domain.entries.manga.model.MangaUpdate import tachiyomi.domain.entries.manga.model.MangaUpdate
import tachiyomi.domain.entries.manga.repository.MangaRepository import tachiyomi.domain.entries.manga.repository.MangaRepository
@ -9,22 +9,22 @@ class SetMangaViewerFlags(
private val mangaRepository: MangaRepository, private val mangaRepository: MangaRepository,
) { ) {
suspend fun awaitSetMangaReadingMode(id: Long, flag: Long) { suspend fun awaitSetReadingMode(id: Long, flag: Long) {
val manga = mangaRepository.getMangaById(id) val manga = mangaRepository.getMangaById(id)
mangaRepository.updateManga( mangaRepository.updateManga(
MangaUpdate( MangaUpdate(
id = id, id = id,
viewerFlags = manga.viewerFlags.setFlag(flag, ReadingModeType.MASK.toLong()), viewerFlags = manga.viewerFlags.setFlag(flag, ReadingMode.MASK.toLong()),
), ),
) )
} }
suspend fun awaitSetOrientationType(id: Long, flag: Long) { suspend fun awaitSetOrientation(id: Long, flag: Long) {
val manga = mangaRepository.getMangaById(id) val manga = mangaRepository.getMangaById(id)
mangaRepository.updateManga( mangaRepository.updateManga(
MangaUpdate( MangaUpdate(
id = id, id = id,
viewerFlags = manga.viewerFlags.setFlag(flag, OrientationType.MASK.toLong()), viewerFlags = manga.viewerFlags.setFlag(flag, ReaderOrientation.MASK.toLong()),
), ),
) )
} }

View file

@ -3,16 +3,19 @@ package eu.kanade.domain.entries.manga.interactor
import eu.kanade.domain.entries.manga.model.hasCustomCover import eu.kanade.domain.entries.manga.model.hasCustomCover
import eu.kanade.tachiyomi.data.cache.MangaCoverCache import eu.kanade.tachiyomi.data.cache.MangaCoverCache
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.entries.manga.model.MangaUpdate import tachiyomi.domain.entries.manga.model.MangaUpdate
import tachiyomi.domain.entries.manga.repository.MangaRepository import tachiyomi.domain.entries.manga.repository.MangaRepository
import tachiyomi.source.local.entries.manga.isLocal import tachiyomi.source.local.entries.manga.isLocal
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.time.ZonedDateTime
import java.util.Date import java.util.Date
class UpdateManga( class UpdateManga(
private val mangaRepository: MangaRepository, private val mangaRepository: MangaRepository,
private val mangaFetchInterval: MangaFetchInterval,
) { ) {
suspend fun await(mangaUpdate: MangaUpdate): Boolean { suspend fun await(mangaUpdate: MangaUpdate): Boolean {
@ -73,12 +76,24 @@ class UpdateManga(
) )
} }
suspend fun awaitUpdateFetchInterval(
manga: Manga,
dateTime: ZonedDateTime = ZonedDateTime.now(),
window: Pair<Long, Long> = mangaFetchInterval.getWindow(dateTime),
): Boolean {
return mangaFetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
?.let { mangaRepository.updateManga(it) }
?: false
}
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean { suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
return mangaRepository.updateManga(MangaUpdate(id = mangaId, lastUpdate = Date().time)) return mangaRepository.updateManga(MangaUpdate(id = mangaId, lastUpdate = Date().time))
} }
suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean { suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean {
return mangaRepository.updateManga(MangaUpdate(id = mangaId, coverLastModified = Date().time)) return mangaRepository.updateManga(
MangaUpdate(id = mangaId, coverLastModified = Date().time),
)
} }
suspend fun awaitUpdateFavorite(mangaId: Long, favorite: Boolean): Boolean { suspend fun awaitUpdateFavorite(mangaId: Long, favorite: Boolean): Boolean {

View file

@ -3,36 +3,36 @@ package eu.kanade.domain.entries.manga.model
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.cache.MangaCoverCache import eu.kanade.tachiyomi.data.cache.MangaCoverCache
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import tachiyomi.core.metadata.comicinfo.ComicInfo import tachiyomi.core.metadata.comicinfo.ComicInfo
import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus
import tachiyomi.domain.entries.TriStateFilter import tachiyomi.core.preference.TriState
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.model.Chapter import tachiyomi.domain.items.chapter.model.Chapter
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
// TODO: move these into the domain model // TODO: move these into the domain model
val Manga.readingModeType: Long val Manga.readingMode: Long
get() = viewerFlags and ReadingModeType.MASK.toLong() get() = viewerFlags and ReadingMode.MASK.toLong()
val Manga.orientationType: Long val Manga.readerOrientation: Long
get() = viewerFlags and OrientationType.MASK.toLong() get() = viewerFlags and ReaderOrientation.MASK.toLong()
val Manga.downloadedFilter: TriStateFilter val Manga.downloadedFilter: TriState
get() { get() {
if (forceDownloaded()) return TriStateFilter.ENABLED_IS if (forceDownloaded()) return TriState.ENABLED_IS
return when (downloadedFilterRaw) { return when (downloadedFilterRaw) {
Manga.CHAPTER_SHOW_DOWNLOADED -> TriStateFilter.ENABLED_IS Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriStateFilter.ENABLED_NOT Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
else -> TriStateFilter.DISABLED else -> TriState.DISABLED
} }
} }
fun Manga.chaptersFiltered(): Boolean { fun Manga.chaptersFiltered(): Boolean {
return unreadFilter != TriStateFilter.DISABLED || return unreadFilter != TriState.DISABLED ||
downloadedFilter != TriStateFilter.DISABLED || downloadedFilter != TriState.DISABLED ||
bookmarkedFilter != TriStateFilter.DISABLED bookmarkedFilter != TriState.DISABLED
} }
fun Manga.forceDownloaded(): Boolean { fun Manga.forceDownloaded(): Boolean {
return favorite && Injekt.get<BasePreferences>().downloadedOnly().get() return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()
@ -95,9 +95,16 @@ fun Manga.hasCustomCover(coverCache: MangaCoverCache = Injekt.get()): Boolean {
/** /**
* Creates a ComicInfo instance based on the manga and chapter metadata. * Creates a ComicInfo instance based on the manga and chapter metadata.
*/ */
fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String) = ComicInfo( fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String, categories: List<String>?) = ComicInfo(
title = ComicInfo.Title(chapter.name), title = ComicInfo.Title(chapter.name),
series = ComicInfo.Series(manga.title), series = ComicInfo.Series(manga.title),
number = chapter.chapterNumber.takeIf { it >= 0 }?.let {
if ((it.rem(1) == 0.0)) {
ComicInfo.Number(it.toInt().toString())
} else {
ComicInfo.Number(it.toString())
}
},
web = ComicInfo.Web(chapterUrl), web = ComicInfo.Web(chapterUrl),
summary = manga.description?.let { ComicInfo.Summary(it) }, summary = manga.description?.let { ComicInfo.Summary(it) },
writer = manga.author?.let { ComicInfo.Writer(it) }, writer = manga.author?.let { ComicInfo.Writer(it) },
@ -107,6 +114,7 @@ fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String) = ComicInfo
publishingStatus = ComicInfo.PublishingStatusTachiyomi( publishingStatus = ComicInfo.PublishingStatusTachiyomi(
ComicInfoPublishingStatus.toComicInfoValue(manga.status), ComicInfoPublishingStatus.toComicInfoValue(manga.status),
), ),
categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) },
inker = null, inker = null,
colorist = null, colorist = null,
letterer = null, letterer = null,

View file

@ -0,0 +1,24 @@
package eu.kanade.domain.items.chapter.interactor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import tachiyomi.domain.items.chapter.repository.ChapterRepository
class GetAvailableScanlators(
private val repository: ChapterRepository,
) {
private fun List<String>.cleanupAvailableScanlators(): Set<String> {
return mapNotNull { it.ifBlank { null } }.toSet()
}
suspend fun await(mangaId: Long): Set<String> {
return repository.getScanlatorsByMangaId(mangaId)
.cleanupAvailableScanlators()
}
fun subscribe(mangaId: Long): Flow<Set<String>> {
return repository.getScanlatorsByMangaIdAsFlow(mangaId)
.map { it.cleanupAvailableScanlators() }
}
}

View file

@ -72,9 +72,9 @@ class SetReadStatus(
suspend fun await(manga: Manga, read: Boolean) = suspend fun await(manga: Manga, read: Boolean) =
await(manga.id, read) await(manga.id, read)
sealed class Result { sealed interface Result {
object Success : Result() data object Success : Result
object NoChapters : Result() data object NoChapters : Result
data class InternalError(val error: Throwable) : Result() data class InternalError(val error: Throwable) : Result
} }
} }

View file

@ -1,5 +1,6 @@
package eu.kanade.domain.items.chapter.interactor package eu.kanade.domain.items.chapter.interactor
import eu.kanade.domain.entries.manga.interactor.GetExcludedScanlators
import eu.kanade.domain.entries.manga.interactor.UpdateManga import eu.kanade.domain.entries.manga.interactor.UpdateManga
import eu.kanade.domain.entries.manga.model.toSManga import eu.kanade.domain.entries.manga.model.toSManga
import eu.kanade.domain.items.chapter.model.copyFromSChapter import eu.kanade.domain.items.chapter.model.copyFromSChapter
@ -11,7 +12,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import tachiyomi.data.items.chapter.ChapterSanitizer import tachiyomi.data.items.chapter.ChapterSanitizer
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.items.chapter.interactor.ShouldUpdateDbChapter import tachiyomi.domain.items.chapter.interactor.ShouldUpdateDbChapter
import tachiyomi.domain.items.chapter.interactor.UpdateChapter import tachiyomi.domain.items.chapter.interactor.UpdateChapter
import tachiyomi.domain.items.chapter.model.Chapter import tachiyomi.domain.items.chapter.model.Chapter
@ -20,20 +21,20 @@ import tachiyomi.domain.items.chapter.model.toChapterUpdate
import tachiyomi.domain.items.chapter.repository.ChapterRepository import tachiyomi.domain.items.chapter.repository.ChapterRepository
import tachiyomi.domain.items.chapter.service.ChapterRecognition import tachiyomi.domain.items.chapter.service.ChapterRecognition
import tachiyomi.source.local.entries.manga.isLocal import tachiyomi.source.local.entries.manga.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.Long.max import java.lang.Long.max
import java.time.ZonedDateTime
import java.util.Date import java.util.Date
import java.util.TreeSet import java.util.TreeSet
class SyncChaptersWithSource( class SyncChaptersWithSource(
private val downloadManager: MangaDownloadManager = Injekt.get(), private val downloadManager: MangaDownloadManager,
private val downloadProvider: MangaDownloadProvider = Injekt.get(), private val downloadProvider: MangaDownloadProvider,
private val chapterRepository: ChapterRepository = Injekt.get(), private val chapterRepository: ChapterRepository,
private val shouldUpdateDbChapter: ShouldUpdateDbChapter = Injekt.get(), private val shouldUpdateDbChapter: ShouldUpdateDbChapter,
private val updateManga: UpdateManga = Injekt.get(), private val updateManga: UpdateManga,
private val updateChapter: UpdateChapter = Injekt.get(), private val updateChapter: UpdateChapter,
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(), private val getChaptersByMangaId: GetChaptersByMangaId,
private val getExcludedScanlators: GetExcludedScanlators,
) { ) {
/** /**
@ -48,11 +49,15 @@ class SyncChaptersWithSource(
rawSourceChapters: List<SChapter>, rawSourceChapters: List<SChapter>,
manga: Manga, manga: Manga,
source: MangaSource, source: MangaSource,
manualFetch: Boolean = false,
fetchWindow: Pair<Long, Long> = Pair(0, 0),
): List<Chapter> { ): List<Chapter> {
if (rawSourceChapters.isEmpty() && !source.isLocal()) { if (rawSourceChapters.isEmpty() && !source.isLocal()) {
throw NoChaptersException() throw NoChaptersException()
} }
val now = ZonedDateTime.now()
val sourceChapters = rawSourceChapters val sourceChapters = rawSourceChapters
.distinctBy { it.url } .distinctBy { it.url }
.mapIndexed { i, sChapter -> .mapIndexed { i, sChapter ->
@ -63,7 +68,7 @@ class SyncChaptersWithSource(
} }
// Chapters from db. // Chapters from db.
val dbChapters = getChapterByMangaId.await(manga.id) val dbChapters = getChaptersByMangaId.await(manga.id)
// Chapters from the source not in db. // Chapters from the source not in db.
val toAdd = mutableListOf<Chapter>() val toAdd = mutableListOf<Chapter>()
@ -96,7 +101,11 @@ class SyncChaptersWithSource(
} }
// Recognize chapter number for the chapter. // Recognize chapter number for the chapter.
val chapterNumber = ChapterRecognition.parseChapterNumber(manga.title, chapter.name, chapter.chapterNumber) val chapterNumber = ChapterRecognition.parseChapterNumber(
manga.title,
chapter.name,
chapter.chapterNumber,
)
chapter = chapter.copy(chapterNumber = chapterNumber) chapter = chapter.copy(chapterNumber = chapterNumber)
val dbChapter = dbChapters.find { it.url == chapter.url } val dbChapter = dbChapters.find { it.url == chapter.url }
@ -112,8 +121,16 @@ class SyncChaptersWithSource(
toAdd.add(toAddChapter) toAdd.add(toAddChapter)
} else { } else {
if (shouldUpdateDbChapter.await(dbChapter, chapter)) { if (shouldUpdateDbChapter.await(dbChapter, chapter)) {
val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(dbChapter, chapter) && val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(
downloadManager.isChapterDownloaded(dbChapter.name, dbChapter.scanlator, manga.title, manga.source) dbChapter,
chapter,
) &&
downloadManager.isChapterDownloaded(
dbChapter.name,
dbChapter.scanlator,
manga.title,
manga.source,
)
if (shouldRenameChapter) { if (shouldRenameChapter) {
downloadManager.renameChapter(source, manga, dbChapter, chapter) downloadManager.renameChapter(source, manga, dbChapter, chapter)
@ -134,14 +151,21 @@ class SyncChaptersWithSource(
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions. // Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) { if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
updateManga.awaitUpdateFetchInterval(
manga,
now,
fetchWindow,
)
}
return emptyList() return emptyList()
} }
val reAdded = mutableListOf<Chapter>() val reAdded = mutableListOf<Chapter>()
val deletedChapterNumbers = TreeSet<Float>() val deletedChapterNumbers = TreeSet<Double>()
val deletedReadChapterNumbers = TreeSet<Float>() val deletedReadChapterNumbers = TreeSet<Double>()
val deletedBookmarkedChapterNumbers = TreeSet<Float>() val deletedBookmarkedChapterNumbers = TreeSet<Double>()
toDelete.forEach { chapter -> toDelete.forEach { chapter ->
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber) if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
@ -188,6 +212,7 @@ class SyncChaptersWithSource(
val chapterUpdates = toChange.map { it.toChapterUpdate() } val chapterUpdates = toChange.map { it.toChapterUpdate() }
updateChapter.awaitAll(chapterUpdates) updateChapter.awaitAll(chapterUpdates)
} }
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
// Set this manga as updated since chapters were changed // Set this manga as updated since chapters were changed
// Note that last_update actually represents last time the chapter list changed at all // Note that last_update actually represents last time the chapter list changed at all
@ -195,6 +220,10 @@ class SyncChaptersWithSource(
val reAddedUrls = reAdded.map { it.url }.toHashSet() val reAddedUrls = reAdded.map { it.url }.toHashSet()
return updatedToAdd.filterNot { it.url in reAddedUrls } val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet()
return updatedToAdd.filterNot {
it.url in reAddedUrls || it.scanlator in excludedScanlators
}
} }
} }

View file

@ -1,6 +1,5 @@
package eu.kanade.domain.items.chapter.model package eu.kanade.domain.items.chapter.model
import data.Chapters
import eu.kanade.tachiyomi.data.database.models.manga.ChapterImpl import eu.kanade.tachiyomi.data.database.models.manga.ChapterImpl
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import tachiyomi.domain.items.chapter.model.Chapter import tachiyomi.domain.items.chapter.model.Chapter
@ -12,7 +11,7 @@ fun Chapter.toSChapter(): SChapter {
it.url = url it.url = url
it.name = name it.name = name
it.date_upload = dateUpload it.date_upload = dateUpload
it.chapter_number = chapterNumber it.chapter_number = chapterNumber.toFloat()
it.scanlator = scanlator it.scanlator = scanlator
} }
} }
@ -22,18 +21,8 @@ fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter {
name = sChapter.name, name = sChapter.name,
url = sChapter.url, url = sChapter.url,
dateUpload = sChapter.date_upload, dateUpload = sChapter.date_upload,
chapterNumber = sChapter.chapter_number, chapterNumber = sChapter.chapter_number.toDouble(),
scanlator = sChapter.scanlator?.ifBlank { null }, scanlator = sChapter.scanlator?.ifBlank { null }?.trim(),
)
}
fun Chapter.copyFrom(other: Chapters): Chapter {
return copy(
name = other.name,
url = other.url,
dateUpload = other.date_upload,
chapterNumber = other.chapter_number,
scanlator = other.scanlator?.ifBlank { null },
) )
} }
@ -48,6 +37,6 @@ fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also {
it.last_page_read = lastPageRead.toInt() it.last_page_read = lastPageRead.toInt()
it.date_fetch = dateFetch it.date_fetch = dateFetch
it.date_upload = dateUpload it.date_upload = dateUpload
it.chapter_number = chapterNumber it.chapter_number = chapterNumber.toFloat()
it.source_order = sourceOrder.toInt() it.source_order = sourceOrder.toInt()
} }

View file

@ -2,7 +2,7 @@ package eu.kanade.domain.items.chapter.model
import eu.kanade.domain.entries.manga.model.downloadedFilter import eu.kanade.domain.entries.manga.model.downloadedFilter
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager
import eu.kanade.tachiyomi.ui.entries.manga.ChapterItem import eu.kanade.tachiyomi.ui.entries.manga.ChapterList
import tachiyomi.domain.entries.applyFilter import tachiyomi.domain.entries.applyFilter
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.model.Chapter import tachiyomi.domain.items.chapter.model.Chapter
@ -23,7 +23,12 @@ fun List<Chapter>.applyFilters(manga: Manga, downloadManager: MangaDownloadManag
.filter { chapter -> applyFilter(bookmarkedFilter) { chapter.bookmark } } .filter { chapter -> applyFilter(bookmarkedFilter) { chapter.bookmark } }
.filter { chapter -> .filter { chapter ->
applyFilter(downloadedFilter) { applyFilter(downloadedFilter) {
val downloaded = downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source) val downloaded = downloadManager.isChapterDownloaded(
chapter.name,
chapter.scanlator,
manga.title,
manga.source,
)
downloaded || isLocalManga downloaded || isLocalManga
} }
} }
@ -34,7 +39,7 @@ fun List<Chapter>.applyFilters(manga: Manga, downloadManager: MangaDownloadManag
* Applies the view filters to the list of chapters obtained from the database. * Applies the view filters to the list of chapters obtained from the database.
* @return an observable of the list of chapters filtered and sorted. * @return an observable of the list of chapters filtered and sorted.
*/ */
fun List<ChapterItem>.applyFilters(manga: Manga): Sequence<ChapterItem> { fun List<ChapterList.Item>.applyFilters(manga: Manga): Sequence<ChapterList.Item> {
val isLocalManga = manga.isLocal() val isLocalManga = manga.isLocal()
val unreadFilter = manga.unreadFilter val unreadFilter = manga.unreadFilter
val downloadedFilter = manga.downloadedFilter val downloadedFilter = manga.downloadedFilter

View file

@ -1,6 +1,6 @@
package eu.kanade.domain.items.episode.interactor package eu.kanade.domain.items.episode.interactor
import eu.kanade.domain.download.anime.interactor.DeleteAnimeDownload import eu.kanade.domain.download.anime.interactor.DeleteEpisodeDownload
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.util.lang.withNonCancellableContext import tachiyomi.core.util.lang.withNonCancellableContext
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
@ -13,7 +13,7 @@ import tachiyomi.domain.items.episode.repository.EpisodeRepository
class SetSeenStatus( class SetSeenStatus(
private val downloadPreferences: DownloadPreferences, private val downloadPreferences: DownloadPreferences,
private val deleteDownload: DeleteAnimeDownload, private val deleteDownload: DeleteEpisodeDownload,
private val animeRepository: AnimeRepository, private val animeRepository: AnimeRepository,
private val episodeRepository: EpisodeRepository, private val episodeRepository: EpisodeRepository,
) { ) {
@ -72,9 +72,9 @@ class SetSeenStatus(
suspend fun await(anime: Anime, seen: Boolean) = suspend fun await(anime: Anime, seen: Boolean) =
await(anime.id, seen) await(anime.id, seen)
sealed class Result { sealed interface Result {
object Success : Result() data object Success : Result
object NoEpisodes : Result() data object NoEpisodes : Result
data class InternalError(val error: Throwable) : Result() data class InternalError(val error: Throwable) : Result
} }
} }

View file

@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadProvider import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadProvider
import tachiyomi.data.items.episode.EpisodeSanitizer import tachiyomi.data.items.episode.EpisodeSanitizer
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId
import tachiyomi.domain.items.episode.interactor.ShouldUpdateDbEpisode import tachiyomi.domain.items.episode.interactor.ShouldUpdateDbEpisode
import tachiyomi.domain.items.episode.interactor.UpdateEpisode import tachiyomi.domain.items.episode.interactor.UpdateEpisode
import tachiyomi.domain.items.episode.model.Episode import tachiyomi.domain.items.episode.model.Episode
@ -20,20 +20,19 @@ import tachiyomi.domain.items.episode.model.toEpisodeUpdate
import tachiyomi.domain.items.episode.repository.EpisodeRepository import tachiyomi.domain.items.episode.repository.EpisodeRepository
import tachiyomi.domain.items.episode.service.EpisodeRecognition import tachiyomi.domain.items.episode.service.EpisodeRecognition
import tachiyomi.source.local.entries.anime.isLocal import tachiyomi.source.local.entries.anime.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.Long.max import java.lang.Long.max
import java.time.ZonedDateTime
import java.util.Date import java.util.Date
import java.util.TreeSet import java.util.TreeSet
class SyncEpisodesWithSource( class SyncEpisodesWithSource(
private val downloadManager: AnimeDownloadManager = Injekt.get(), private val downloadManager: AnimeDownloadManager,
private val downloadProvider: AnimeDownloadProvider = Injekt.get(), private val downloadProvider: AnimeDownloadProvider,
private val episodeRepository: EpisodeRepository = Injekt.get(), private val episodeRepository: EpisodeRepository,
private val shouldUpdateDbEpisode: ShouldUpdateDbEpisode = Injekt.get(), private val shouldUpdateDbEpisode: ShouldUpdateDbEpisode,
private val updateAnime: UpdateAnime = Injekt.get(), private val updateAnime: UpdateAnime,
private val updateEpisode: UpdateEpisode = Injekt.get(), private val updateEpisode: UpdateEpisode,
private val getEpisodeByAnimeId: GetEpisodeByAnimeId = Injekt.get(), private val getEpisodesByAnimeId: GetEpisodesByAnimeId,
) { ) {
/** /**
@ -48,11 +47,15 @@ class SyncEpisodesWithSource(
rawSourceEpisodes: List<SEpisode>, rawSourceEpisodes: List<SEpisode>,
anime: Anime, anime: Anime,
source: AnimeSource, source: AnimeSource,
manualFetch: Boolean = false,
fetchWindow: Pair<Long, Long> = Pair(0, 0),
): List<Episode> { ): List<Episode> {
if (rawSourceEpisodes.isEmpty() && !source.isLocal()) { if (rawSourceEpisodes.isEmpty() && !source.isLocal()) {
throw NoEpisodesException() throw NoEpisodesException()
} }
val now = ZonedDateTime.now()
val sourceEpisodes = rawSourceEpisodes val sourceEpisodes = rawSourceEpisodes
.distinctBy { it.url } .distinctBy { it.url }
.mapIndexed { i, sEpisode -> .mapIndexed { i, sEpisode ->
@ -63,7 +66,7 @@ class SyncEpisodesWithSource(
} }
// Episodes from db. // Episodes from db.
val dbEpisodes = getEpisodeByAnimeId.await(anime.id) val dbEpisodes = getEpisodesByAnimeId.await(anime.id)
// Episodes from the source not in db. // Episodes from the source not in db.
val toAdd = mutableListOf<Episode>() val toAdd = mutableListOf<Episode>()
@ -96,7 +99,11 @@ class SyncEpisodesWithSource(
} }
// Recognize episode number for the episode. // Recognize episode number for the episode.
val episodeNumber = EpisodeRecognition.parseEpisodeNumber(anime.title, episode.name, episode.episodeNumber) val episodeNumber = EpisodeRecognition.parseEpisodeNumber(
anime.title,
episode.name,
episode.episodeNumber,
)
episode = episode.copy(episodeNumber = episodeNumber) episode = episode.copy(episodeNumber = episodeNumber)
val dbEpisode = dbEpisodes.find { it.url == episode.url } val dbEpisode = dbEpisodes.find { it.url == episode.url }
@ -112,8 +119,16 @@ class SyncEpisodesWithSource(
toAdd.add(toAddEpisode) toAdd.add(toAddEpisode)
} else { } else {
if (shouldUpdateDbEpisode.await(dbEpisode, episode)) { if (shouldUpdateDbEpisode.await(dbEpisode, episode)) {
val shouldRenameEpisode = downloadProvider.isEpisodeDirNameChanged(dbEpisode, episode) && val shouldRenameEpisode = downloadProvider.isEpisodeDirNameChanged(
downloadManager.isEpisodeDownloaded(dbEpisode.name, dbEpisode.scanlator, anime.title, anime.source) dbEpisode,
episode,
) &&
downloadManager.isEpisodeDownloaded(
dbEpisode.name,
dbEpisode.scanlator,
anime.title,
anime.source,
)
if (shouldRenameEpisode) { if (shouldRenameEpisode) {
downloadManager.renameEpisode(source, anime, dbEpisode, episode) downloadManager.renameEpisode(source, anime, dbEpisode, episode)
@ -125,7 +140,9 @@ class SyncEpisodesWithSource(
sourceOrder = episode.sourceOrder, sourceOrder = episode.sourceOrder,
) )
if (episode.dateUpload != 0L) { if (episode.dateUpload != 0L) {
toChangeEpisode = toChangeEpisode.copy(dateUpload = sourceEpisode.dateUpload) toChangeEpisode = toChangeEpisode.copy(
dateUpload = sourceEpisode.dateUpload,
)
} }
toChange.add(toChangeEpisode) toChange.add(toChangeEpisode)
} }
@ -134,14 +151,21 @@ class SyncEpisodesWithSource(
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions. // Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) { if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
if (manualFetch || anime.fetchInterval == 0 || anime.nextUpdate < fetchWindow.first) {
updateAnime.awaitUpdateFetchInterval(
anime,
now,
fetchWindow,
)
}
return emptyList() return emptyList()
} }
val reAdded = mutableListOf<Episode>() val reAdded = mutableListOf<Episode>()
val deletedEpisodeNumbers = TreeSet<Float>() val deletedEpisodeNumbers = TreeSet<Double>()
val deletedSeenEpisodeNumbers = TreeSet<Float>() val deletedSeenEpisodeNumbers = TreeSet<Double>()
val deletedBookmarkedEpisodeNumbers = TreeSet<Float>() val deletedBookmarkedEpisodeNumbers = TreeSet<Double>()
toDelete.forEach { episode -> toDelete.forEach { episode ->
if (episode.seen) deletedSeenEpisodeNumbers.add(episode.episodeNumber) if (episode.seen) deletedSeenEpisodeNumbers.add(episode.episodeNumber)
@ -188,6 +212,7 @@ class SyncEpisodesWithSource(
val episodeUpdates = toChange.map { it.toEpisodeUpdate() } val episodeUpdates = toChange.map { it.toEpisodeUpdate() }
updateEpisode.awaitAll(episodeUpdates) updateEpisode.awaitAll(episodeUpdates)
} }
updateAnime.awaitUpdateFetchInterval(anime, now, fetchWindow)
// Set this anime as updated since episodes were changed // Set this anime as updated since episodes were changed
// Note that last_update actually represents last time the episode list changed at all // Note that last_update actually represents last time the episode list changed at all

View file

@ -1,6 +1,5 @@
package eu.kanade.domain.items.episode.model package eu.kanade.domain.items.episode.model
import dataanime.Episodes
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.data.database.models.anime.EpisodeImpl import eu.kanade.tachiyomi.data.database.models.anime.EpisodeImpl
import tachiyomi.domain.items.episode.model.Episode import tachiyomi.domain.items.episode.model.Episode
@ -12,7 +11,7 @@ fun Episode.toSEpisode(): SEpisode {
it.url = url it.url = url
it.name = name it.name = name
it.date_upload = dateUpload it.date_upload = dateUpload
it.episode_number = episodeNumber it.episode_number = episodeNumber.toFloat()
it.scanlator = scanlator it.scanlator = scanlator
} }
} }
@ -22,21 +21,11 @@ fun Episode.copyFromSEpisode(sEpisode: SEpisode): Episode {
name = sEpisode.name, name = sEpisode.name,
url = sEpisode.url, url = sEpisode.url,
dateUpload = sEpisode.date_upload, dateUpload = sEpisode.date_upload,
episodeNumber = sEpisode.episode_number, episodeNumber = sEpisode.episode_number.toDouble(),
scanlator = sEpisode.scanlator?.ifBlank { null }, scanlator = sEpisode.scanlator?.ifBlank { null },
) )
} }
fun Episode.copyFrom(other: Episodes): Episode {
return copy(
name = other.name,
url = other.url,
dateUpload = other.date_upload,
episodeNumber = other.episode_number,
scanlator = other.scanlator?.ifBlank { null },
)
}
fun Episode.toDbEpisode(): DbEpisode = EpisodeImpl().also { fun Episode.toDbEpisode(): DbEpisode = EpisodeImpl().also {
it.id = id it.id = id
it.anime_id = animeId it.anime_id = animeId
@ -49,6 +38,6 @@ fun Episode.toDbEpisode(): DbEpisode = EpisodeImpl().also {
it.total_seconds = totalSeconds it.total_seconds = totalSeconds
it.date_fetch = dateFetch it.date_fetch = dateFetch
it.date_upload = dateUpload it.date_upload = dateUpload
it.episode_number = episodeNumber it.episode_number = episodeNumber.toFloat()
it.source_order = sourceOrder.toInt() it.source_order = sourceOrder.toInt()
} }

View file

@ -2,7 +2,7 @@ package eu.kanade.domain.items.episode.model
import eu.kanade.domain.entries.anime.model.downloadedFilter import eu.kanade.domain.entries.anime.model.downloadedFilter
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager
import eu.kanade.tachiyomi.ui.entries.anime.EpisodeItem import eu.kanade.tachiyomi.ui.entries.anime.EpisodeList
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.entries.applyFilter import tachiyomi.domain.entries.applyFilter
import tachiyomi.domain.items.episode.model.Episode import tachiyomi.domain.items.episode.model.Episode
@ -23,7 +23,12 @@ fun List<Episode>.applyFilters(anime: Anime, downloadManager: AnimeDownloadManag
.filter { episode -> applyFilter(bookmarkedFilter) { episode.bookmark } } .filter { episode -> applyFilter(bookmarkedFilter) { episode.bookmark } }
.filter { episode -> .filter { episode ->
applyFilter(downloadedFilter) { applyFilter(downloadedFilter) {
val downloaded = downloadManager.isEpisodeDownloaded(episode.name, episode.scanlator, anime.title, anime.source) val downloaded = downloadManager.isEpisodeDownloaded(
episode.name,
episode.scanlator,
anime.title,
anime.source,
)
downloaded || isLocalAnime downloaded || isLocalAnime
} }
} }
@ -34,7 +39,7 @@ fun List<Episode>.applyFilters(anime: Anime, downloadManager: AnimeDownloadManag
* Applies the view filters to the list of episodes obtained from the database. * Applies the view filters to the list of episodes obtained from the database.
* @return an observable of the list of episodes filtered and sorted. * @return an observable of the list of episodes filtered and sorted.
*/ */
fun List<EpisodeItem>.applyFilters(anime: Anime): Sequence<EpisodeItem> { fun List<EpisodeList.Item>.applyFilters(anime: Anime): Sequence<EpisodeList.Item> {
val isLocalAnime = anime.isLocal() val isLocalAnime = anime.isLocal()
val unseenFilter = anime.unseenFilter val unseenFilter = anime.unseenFilter
val downloadedFilter = anime.downloadedFilter val downloadedFilter = anime.downloadedFilter

View file

@ -4,12 +4,11 @@ import eu.kanade.domain.source.service.SetMigrateSorting
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import tachiyomi.core.util.lang.compareToWithCollator
import tachiyomi.domain.source.anime.model.AnimeSource import tachiyomi.domain.source.anime.model.AnimeSource
import tachiyomi.domain.source.anime.repository.AnimeSourceRepository import tachiyomi.domain.source.anime.repository.AnimeSourceRepository
import tachiyomi.source.local.entries.anime.LocalAnimeSource import tachiyomi.source.local.entries.anime.LocalAnimeSource
import java.text.Collator
import java.util.Collections import java.util.Collections
import java.util.Locale
class GetAnimeSourcesWithFavoriteCount( class GetAnimeSourcesWithFavoriteCount(
private val repository: AnimeSourceRepository, private val repository: AnimeSourceRepository,
@ -32,17 +31,13 @@ class GetAnimeSourcesWithFavoriteCount(
direction: SetMigrateSorting.Direction, direction: SetMigrateSorting.Direction,
sorting: SetMigrateSorting.Mode, sorting: SetMigrateSorting.Mode,
): java.util.Comparator<Pair<AnimeSource, Long>> { ): java.util.Comparator<Pair<AnimeSource, Long>> {
val locale = Locale.getDefault()
val collator = Collator.getInstance(locale).apply {
strength = Collator.PRIMARY
}
val sortFn: (Pair<AnimeSource, Long>, Pair<AnimeSource, Long>) -> Int = { a, b -> val sortFn: (Pair<AnimeSource, Long>, Pair<AnimeSource, Long>) -> Int = { a, b ->
when (sorting) { when (sorting) {
SetMigrateSorting.Mode.ALPHABETICAL -> { SetMigrateSorting.Mode.ALPHABETICAL -> {
when { when {
a.first.isStub && b.first.isStub.not() -> -1 a.first.isStub && b.first.isStub.not() -> -1
b.first.isStub && a.first.isStub.not() -> 1 b.first.isStub && a.first.isStub.not() -> 1
else -> collator.compare(a.first.name.lowercase(locale), b.first.name.lowercase(locale)) else -> a.first.name.lowercase().compareToWithCollator(b.first.name.lowercase())
} }
} }
SetMigrateSorting.Mode.TOTAL -> { SetMigrateSorting.Mode.TOTAL -> {

View file

@ -6,13 +6,14 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import tachiyomi.domain.source.anime.model.AnimeSource import tachiyomi.domain.source.anime.model.AnimeSource
import tachiyomi.domain.source.anime.repository.AnimeSourceRepository import tachiyomi.domain.source.anime.repository.AnimeSourceRepository
import java.util.SortedMap
class GetLanguagesWithAnimeSources( class GetLanguagesWithAnimeSources(
private val repository: AnimeSourceRepository, private val repository: AnimeSourceRepository,
private val preferences: SourcePreferences, private val preferences: SourcePreferences,
) { ) {
fun subscribe(): Flow<Map<String, List<AnimeSource>>> { fun subscribe(): Flow<SortedMap<String, List<AnimeSource>>> {
return combine( return combine(
preferences.enabledLanguages().changes(), preferences.enabledLanguages().changes(),
preferences.disabledAnimeSources().changes(), preferences.disabledAnimeSources().changes(),
@ -23,7 +24,8 @@ class GetLanguagesWithAnimeSources(
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name }, .thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
) )
sortedSources.groupBy { it.lang } sortedSources
.groupBy { it.lang }
.toSortedMap( .toSortedMap(
compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator), compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
) )

View file

@ -21,7 +21,13 @@ class ToggleAnimeSource(
fun await(sourceIds: List<Long>, enable: Boolean) { fun await(sourceIds: List<Long>, enable: Boolean) {
val transformedSourceIds = sourceIds.map { it.toString() } val transformedSourceIds = sourceIds.map { it.toString() }
preferences.disabledAnimeSources().getAndSet { disabled -> preferences.disabledAnimeSources().getAndSet { disabled ->
if (enable) disabled.minus(transformedSourceIds) else disabled.plus(transformedSourceIds) if (enable) {
disabled.minus(transformedSourceIds)
} else {
disabled.plus(
transformedSourceIds,
)
}
} }
} }

View file

@ -6,13 +6,14 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import tachiyomi.domain.source.manga.model.Source import tachiyomi.domain.source.manga.model.Source
import tachiyomi.domain.source.manga.repository.MangaSourceRepository import tachiyomi.domain.source.manga.repository.MangaSourceRepository
import java.util.SortedMap
class GetLanguagesWithMangaSources( class GetLanguagesWithMangaSources(
private val repository: MangaSourceRepository, private val repository: MangaSourceRepository,
private val preferences: SourcePreferences, private val preferences: SourcePreferences,
) { ) {
fun subscribe(): Flow<Map<String, List<Source>>> { fun subscribe(): Flow<SortedMap<String, List<Source>>> {
return combine( return combine(
preferences.enabledLanguages().changes(), preferences.enabledLanguages().changes(),
preferences.disabledMangaSources().changes(), preferences.disabledMangaSources().changes(),
@ -23,7 +24,8 @@ class GetLanguagesWithMangaSources(
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name }, .thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
) )
sortedSources.groupBy { it.lang } sortedSources
.groupBy { it.lang }
.toSortedMap( .toSortedMap(
compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator), compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
) )

View file

@ -4,12 +4,11 @@ import eu.kanade.domain.source.service.SetMigrateSorting
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import tachiyomi.core.util.lang.compareToWithCollator
import tachiyomi.domain.source.manga.model.Source import tachiyomi.domain.source.manga.model.Source
import tachiyomi.domain.source.manga.repository.MangaSourceRepository import tachiyomi.domain.source.manga.repository.MangaSourceRepository
import tachiyomi.source.local.entries.manga.LocalMangaSource import tachiyomi.source.local.entries.manga.LocalMangaSource
import java.text.Collator
import java.util.Collections import java.util.Collections
import java.util.Locale
class GetMangaSourcesWithFavoriteCount( class GetMangaSourcesWithFavoriteCount(
private val repository: MangaSourceRepository, private val repository: MangaSourceRepository,
@ -32,17 +31,13 @@ class GetMangaSourcesWithFavoriteCount(
direction: SetMigrateSorting.Direction, direction: SetMigrateSorting.Direction,
sorting: SetMigrateSorting.Mode, sorting: SetMigrateSorting.Mode,
): java.util.Comparator<Pair<Source, Long>> { ): java.util.Comparator<Pair<Source, Long>> {
val locale = Locale.getDefault()
val collator = Collator.getInstance(locale).apply {
strength = Collator.PRIMARY
}
val sortFn: (Pair<Source, Long>, Pair<Source, Long>) -> Int = { a, b -> val sortFn: (Pair<Source, Long>, Pair<Source, Long>) -> Int = { a, b ->
when (sorting) { when (sorting) {
SetMigrateSorting.Mode.ALPHABETICAL -> { SetMigrateSorting.Mode.ALPHABETICAL -> {
when { when {
a.first.isStub && b.first.isStub.not() -> -1 a.first.isStub && b.first.isStub.not() -> -1
b.first.isStub && a.first.isStub.not() -> 1 b.first.isStub && a.first.isStub.not() -> 1
else -> collator.compare(a.first.name.lowercase(locale), b.first.name.lowercase(locale)) else -> a.first.name.lowercase().compareToWithCollator(b.first.name.lowercase())
} }
} }
SetMigrateSorting.Mode.TOTAL -> { SetMigrateSorting.Mode.TOTAL -> {

View file

@ -21,7 +21,13 @@ class ToggleMangaSource(
fun await(sourceIds: List<Long>, enable: Boolean) { fun await(sourceIds: List<Long>, enable: Boolean) {
val transformedSourceIds = sourceIds.map { it.toString() } val transformedSourceIds = sourceIds.map { it.toString() }
preferences.disabledMangaSources().getAndSet { disabled -> preferences.disabledMangaSources().getAndSet { disabled ->
if (enable) disabled.minus(transformedSourceIds) else disabled.plus(transformedSourceIds) if (enable) {
disabled.minus(transformedSourceIds)
} else {
disabled.plus(
transformedSourceIds,
)
}
} }
} }

View file

@ -1,6 +1,7 @@
package eu.kanade.domain.source.service package eu.kanade.domain.source.service
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.preference.getEnum import tachiyomi.core.preference.getEnum
import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibraryDisplayMode
@ -11,17 +12,31 @@ class SourcePreferences(
// Common options // Common options
fun sourceDisplayMode() = preferenceStore.getObject("pref_display_mode_catalogue", LibraryDisplayMode.default, LibraryDisplayMode.Serializer::serialize, LibraryDisplayMode.Serializer::deserialize) fun sourceDisplayMode() = preferenceStore.getObject(
"pref_display_mode_catalogue",
LibraryDisplayMode.default,
LibraryDisplayMode.Serializer::serialize,
LibraryDisplayMode.Serializer::deserialize,
)
fun enabledLanguages() = preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages()) fun enabledLanguages() = preferenceStore.getStringSet(
"source_languages",
LocaleHelper.getDefaultEnabledLanguages(),
)
fun showNsfwSource() = preferenceStore.getBoolean("show_nsfw_source", true) fun showNsfwSource() = preferenceStore.getBoolean("show_nsfw_source", true)
fun migrationSortingMode() = preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL) fun migrationSortingMode() = preferenceStore.getEnum(
"pref_migration_sorting",
SetMigrateSorting.Mode.ALPHABETICAL,
)
fun migrationSortingDirection() = preferenceStore.getEnum("pref_migration_direction", SetMigrateSorting.Direction.ASCENDING) fun migrationSortingDirection() = preferenceStore.getEnum(
"pref_migration_direction",
SetMigrateSorting.Direction.ASCENDING,
)
fun trustedSignatures() = preferenceStore.getStringSet("trusted_signatures", emptySet()) fun trustedSignatures() = preferenceStore.getStringSet(Preference.appStateKey("trusted_signatures"), emptySet())
// Mixture Sources // Mixture Sources
@ -31,18 +46,27 @@ class SourcePreferences(
fun pinnedAnimeSources() = preferenceStore.getStringSet("pinned_anime_catalogues", emptySet()) fun pinnedAnimeSources() = preferenceStore.getStringSet("pinned_anime_catalogues", emptySet())
fun pinnedMangaSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet()) fun pinnedMangaSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
fun lastUsedAnimeSource() = preferenceStore.getLong("last_anime_catalogue_source", -1) fun lastUsedAnimeSource() = preferenceStore.getLong(
fun lastUsedMangaSource() = preferenceStore.getLong("last_catalogue_source", -1) Preference.appStateKey("last_anime_catalogue_source"),
-1,
)
fun lastUsedMangaSource() = preferenceStore.getLong(
Preference.appStateKey("last_catalogue_source"),
-1,
)
fun animeExtensionUpdatesCount() = preferenceStore.getInt("animeext_updates_count", 0) fun animeExtensionUpdatesCount() = preferenceStore.getInt("animeext_updates_count", 0)
fun mangaExtensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0) fun mangaExtensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
fun searchPinnedAnimeSourcesOnly() = preferenceStore.getBoolean("search_pinned_anime_sources_only", false) fun hideInAnimeLibraryItems() = preferenceStore.getBoolean(
fun searchPinnedMangaSourcesOnly() = preferenceStore.getBoolean("search_pinned_sources_only", false) "browse_hide_in_anime_library_items",
false,
)
fun hideInAnimeLibraryItems() = preferenceStore.getBoolean("browse_hide_in_anime_library_items", false) fun hideInMangaLibraryItems() = preferenceStore.getBoolean(
"browse_hide_in_library_items",
fun hideInMangaLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false) false,
)
// SY --> // SY -->
@ -62,7 +86,10 @@ class SourcePreferences(
fun dataSaverImageQuality() = preferenceStore.getInt("data_saver_image_quality", 80) fun dataSaverImageQuality() = preferenceStore.getInt("data_saver_image_quality", 80)
fun dataSaverImageFormatJpeg() = preferenceStore.getBoolean("data_saver_image_format_jpeg", false) fun dataSaverImageFormatJpeg() = preferenceStore.getBoolean(
"data_saver_image_format_jpeg",
false,
)
fun dataSaverServer() = preferenceStore.getString("data_saver_server", "") fun dataSaverServer() = preferenceStore.getString("data_saver_server", "")

View file

@ -0,0 +1,108 @@
package eu.kanade.domain.track.anime.interactor
import eu.kanade.domain.track.anime.model.toDbTrack
import eu.kanade.domain.track.anime.model.toDomainTrack
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack
import eu.kanade.tachiyomi.data.track.AnimeTracker
import eu.kanade.tachiyomi.data.track.EnhancedAnimeTracker
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
import logcat.LogPriority
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withNonCancellableContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.history.anime.interactor.GetAnimeHistory
import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId
import tachiyomi.domain.track.anime.interactor.GetAnimeTracks
import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.ZoneOffset
class AddAnimeTracks(
private val getTracks: GetAnimeTracks,
private val insertTrack: InsertAnimeTrack,
private val syncChapterProgressWithTrack: SyncEpisodeProgressWithTrack,
private val getEpisodesByAnimeId: GetEpisodesByAnimeId,
) {
// TODO: update all trackers based on common data
suspend fun bind(tracker: AnimeTracker, item: AnimeTrack, animeId: Long) = withNonCancellableContext {
withIOContext {
val allChapters = getEpisodesByAnimeId.await(animeId)
val hasSeenEpisodes = allChapters.any { it.seen }
tracker.bind(item, hasSeenEpisodes)
var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext
insertTrack.await(track)
// TODO: merge into [SyncChapterProgressWithTrack]?
// Update chapter progress if newer chapters marked read locally
if (hasSeenEpisodes) {
val latestLocalReadChapterNumber = allChapters
.sortedBy { it.episodeNumber }
.takeWhile { it.seen }
.lastOrNull()
?.episodeNumber ?: -1.0
if (latestLocalReadChapterNumber > track.lastEpisodeSeen) {
track = track.copy(
lastEpisodeSeen = latestLocalReadChapterNumber,
)
tracker.setRemoteLastEpisodeSeen(track.toDbTrack(), latestLocalReadChapterNumber.toInt())
}
if (track.startDate <= 0) {
val firstReadChapterDate = Injekt.get<GetAnimeHistory>().await(animeId)
.sortedBy { it.seenAt }
.firstOrNull()
?.seenAt
firstReadChapterDate?.let {
val startDate = firstReadChapterDate.time.convertEpochMillisZone(
ZoneOffset.systemDefault(),
ZoneOffset.UTC,
)
track = track.copy(
startDate = startDate,
)
tracker.setRemoteStartDate(track.toDbTrack(), startDate)
}
}
}
syncChapterProgressWithTrack.await(animeId, track, tracker)
}
}
suspend fun bindEnhancedTrackers(anime: Anime, source: AnimeSource) = withNonCancellableContext {
withIOContext {
getTracks.await(anime.id)
.filterIsInstance<EnhancedAnimeTracker>()
.filter { it.accept(source) }
.forEach { service ->
try {
service.match(anime)?.let { track ->
track.anime_id = anime.id
(service as Tracker).animeService.bind(track)
insertTrack.await(track.toDomainTrack()!!)
syncChapterProgressWithTrack.await(
anime.id,
track.toDomainTrack()!!,
service.animeService,
)
}
} catch (e: Exception) {
logcat(
LogPriority.WARN,
e,
) { "Could not match anime: ${anime.title} with service $service" }
}
}
}
}
}

View file

@ -0,0 +1,46 @@
package eu.kanade.domain.track.anime.interactor
import eu.kanade.domain.track.anime.model.toDbTrack
import eu.kanade.domain.track.anime.model.toDomainTrack
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.TrackerManager
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.supervisorScope
import tachiyomi.domain.track.anime.interactor.GetAnimeTracks
import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
class RefreshAnimeTracks(
private val getTracks: GetAnimeTracks,
private val trackerManager: TrackerManager,
private val insertTrack: InsertAnimeTrack,
private val syncEpisodeProgressWithTrack: SyncEpisodeProgressWithTrack,
) {
/**
* Fetches updated tracking data from all logged in trackers.
*
* @return Failed updates.
*/
suspend fun await(animeId: Long): List<Pair<Tracker?, Throwable>> {
return supervisorScope {
return@supervisorScope getTracks.await(animeId)
.map { it to trackerManager.get(it.syncId) }
.filter { (_, service) -> service?.isLoggedIn == true }
.map { (track, service) ->
async {
return@async try {
val updatedTrack = service!!.animeService.refresh(track.toDbTrack())
insertTrack.await(updatedTrack.toDomainTrack()!!)
syncEpisodeProgressWithTrack.await(animeId, track, service.animeService)
null
} catch (e: Throwable) {
service to e
}
}
}
.awaitAll()
.filterNotNull()
}
}
}

View file

@ -1,28 +1,35 @@
package eu.kanade.domain.items.episode.interactor package eu.kanade.domain.track.anime.interactor
import eu.kanade.domain.track.anime.model.toDbTrack import eu.kanade.domain.track.anime.model.toDbTrack
import eu.kanade.tachiyomi.data.track.AnimeTrackService import eu.kanade.tachiyomi.data.track.AnimeTracker
import eu.kanade.tachiyomi.data.track.EnhancedAnimeTracker
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId
import tachiyomi.domain.items.episode.interactor.UpdateEpisode import tachiyomi.domain.items.episode.interactor.UpdateEpisode
import tachiyomi.domain.items.episode.model.Episode
import tachiyomi.domain.items.episode.model.toEpisodeUpdate import tachiyomi.domain.items.episode.model.toEpisodeUpdate
import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
import tachiyomi.domain.track.anime.model.AnimeTrack import tachiyomi.domain.track.anime.model.AnimeTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class SyncEpisodesWithTrackServiceTwoWay( class SyncEpisodeProgressWithTrack(
private val updateEpisode: UpdateEpisode = Injekt.get(), private val updateEpisode: UpdateEpisode,
private val insertTrack: InsertAnimeTrack = Injekt.get(), private val insertTrack: InsertAnimeTrack,
private val getEpisodesByAnimeId: GetEpisodesByAnimeId,
) { ) {
suspend fun await( suspend fun await(
episodes: List<Episode>, animeId: Long,
remoteTrack: AnimeTrack, remoteTrack: AnimeTrack,
service: AnimeTrackService, service: AnimeTracker,
) { ) {
val sortedEpisodes = episodes.sortedBy { it.episodeNumber } if (service !is EnhancedAnimeTracker) {
return
}
val sortedEpisodes = getEpisodesByAnimeId.await(animeId)
.sortedBy { it.episodeNumber }
.filter { it.isRecognizedNumber }
val episodeUpdates = sortedEpisodes val episodeUpdates = sortedEpisodes
.filter { episode -> episode.episodeNumber <= remoteTrack.lastEpisodeSeen && !episode.seen } .filter { episode -> episode.episodeNumber <= remoteTrack.lastEpisodeSeen && !episode.seen }
.map { it.copy(seen = true).toEpisodeUpdate() } .map { it.copy(seen = true).toEpisodeUpdate() }

View file

@ -0,0 +1,59 @@
package eu.kanade.domain.track.anime.interactor
import android.content.Context
import eu.kanade.domain.track.anime.model.toDbTrack
import eu.kanade.domain.track.anime.model.toDomainTrack
import eu.kanade.domain.track.anime.service.DelayedAnimeTrackingUpdateJob
import eu.kanade.domain.track.anime.store.DelayedAnimeTrackingStore
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.util.system.isOnline
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import logcat.LogPriority
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.track.anime.interactor.GetAnimeTracks
import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
class TrackEpisode(
private val getTracks: GetAnimeTracks,
private val trackerManager: TrackerManager,
private val insertTrack: InsertAnimeTrack,
private val delayedTrackingStore: DelayedAnimeTrackingStore,
) {
suspend fun await(context: Context, animeId: Long, episodeNumber: Double) = coroutineScope {
launchNonCancellable {
val tracks = getTracks.await(animeId)
if (tracks.isEmpty()) return@launchNonCancellable
tracks.mapNotNull { track ->
val service = trackerManager.get(track.syncId)
if (service == null || !service.isLoggedIn || episodeNumber <= track.lastEpisodeSeen) {
return@mapNotNull null
}
async {
runCatching {
if (context.isOnline()) {
val updatedTrack = service.animeService.refresh(track.toDbTrack())
.toDomainTrack(idRequired = true)!!
.copy(lastEpisodeSeen = episodeNumber)
service.animeService.update(updatedTrack.toDbTrack(), true)
insertTrack.await(updatedTrack)
delayedTrackingStore.removeAnimeItem(track.id)
} else {
delayedTrackingStore.addAnime(track.id, episodeNumber)
DelayedAnimeTrackingUpdateJob.setupTask(context)
}
}
}
}
.awaitAll()
.mapNotNull { it.exceptionOrNull() }
.forEach { logcat(LogPriority.INFO, it) }
}
}
}

View file

@ -13,7 +13,9 @@ fun AnimeTrack.copyPersonalFrom(other: AnimeTrack): AnimeTrack {
) )
} }
fun AnimeTrack.toDbTrack(): DbAnimeTrack = eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack.create(syncId).also { fun AnimeTrack.toDbTrack(): DbAnimeTrack = eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack.create(
syncId,
).also {
it.id = id it.id = id
it.anime_id = animeId it.anime_id = animeId
it.media_id = remoteId it.media_id = remoteId
@ -22,7 +24,7 @@ fun AnimeTrack.toDbTrack(): DbAnimeTrack = eu.kanade.tachiyomi.data.database.mod
it.last_episode_seen = lastEpisodeSeen.toFloat() it.last_episode_seen = lastEpisodeSeen.toFloat()
it.total_episodes = totalEpisodes.toInt() it.total_episodes = totalEpisodes.toInt()
it.status = status.toInt() it.status = status.toInt()
it.score = score it.score = score.toFloat()
it.tracking_url = remoteUrl it.tracking_url = remoteUrl
it.started_watching_date = startDate it.started_watching_date = startDate
it.finished_watching_date = finishDate it.finished_watching_date = finishDate
@ -40,7 +42,7 @@ fun DbAnimeTrack.toDomainTrack(idRequired: Boolean = true): AnimeTrack? {
lastEpisodeSeen = last_episode_seen.toDouble(), lastEpisodeSeen = last_episode_seen.toDouble(),
totalEpisodes = total_episodes.toLong(), totalEpisodes = total_episodes.toLong(),
status = status.toLong(), status = status.toLong(),
score = score, score = score.toDouble(),
remoteUrl = tracking_url, remoteUrl = tracking_url,
startDate = started_watching_date, startDate = started_watching_date,
finishDate = finished_watching_date, finishDate = finished_watching_date,

View file

@ -8,30 +8,32 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import eu.kanade.domain.track.anime.model.toDbTrack import eu.kanade.domain.track.anime.interactor.TrackEpisode
import eu.kanade.domain.track.anime.store.DelayedAnimeTrackingStore import eu.kanade.domain.track.anime.store.DelayedAnimeTrackingStore
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.util.system.workManager import eu.kanade.tachiyomi.util.system.workManager
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.track.anime.interactor.GetAnimeTracks import tachiyomi.domain.track.anime.interactor.GetAnimeTracks
import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit import kotlin.time.Duration.Companion.minutes
import kotlin.time.toJavaDuration
class DelayedAnimeTrackingUpdateJob(context: Context, workerParams: WorkerParameters) : class DelayedAnimeTrackingUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) { CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val getTracks = Injekt.get<GetAnimeTracks>() if (runAttemptCount > 3) {
val insertTrack = Injekt.get<InsertAnimeTrack>() return Result.failure()
}
val getTracks = Injekt.get<GetAnimeTracks>()
val trackEpisode = Injekt.get<TrackEpisode>()
val trackManager = Injekt.get<TrackManager>()
val delayedTrackingStore = Injekt.get<DelayedAnimeTrackingStore>() val delayedTrackingStore = Injekt.get<DelayedAnimeTrackingStore>()
val results = withIOContext { withIOContext {
delayedTrackingStore.getAnimeItems() delayedTrackingStore.getAnimeItems()
.mapNotNull { .mapNotNull {
val track = getTracks.awaitOne(it.trackId) val track = getTracks.awaitOne(it.trackId)
@ -40,24 +42,16 @@ class DelayedAnimeTrackingUpdateJob(context: Context, workerParams: WorkerParame
} }
track?.copy(lastEpisodeSeen = it.lastEpisodeSeen.toDouble()) track?.copy(lastEpisodeSeen = it.lastEpisodeSeen.toDouble())
} }
.mapNotNull { animeTrack -> .forEach { animeTrack ->
try { logcat(LogPriority.DEBUG) {
val service = trackManager.getService(animeTrack.syncId) "Updating delayed track item: ${animeTrack.animeId}" +
if (service != null && service.isLogged) { ", last chapter read: ${animeTrack.lastEpisodeSeen}"
logcat(LogPriority.DEBUG) { "Updating delayed track item: ${animeTrack.id}, last episode seen: ${animeTrack.lastEpisodeSeen}" }
service.animeService.update(animeTrack.toDbTrack(), true)
insertTrack.await(animeTrack)
}
delayedTrackingStore.removeAnimeItem(animeTrack.id)
null
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
false
} }
trackEpisode.await(context, animeTrack.animeId, animeTrack.lastEpisodeSeen)
} }
} }
return if (results.isNotEmpty()) Result.failure() else Result.success() return if (delayedTrackingStore.getAnimeItems().isEmpty()) Result.success() else Result.retry()
} }
companion object { companion object {
@ -70,7 +64,7 @@ class DelayedAnimeTrackingUpdateJob(context: Context, workerParams: WorkerParame
val request = OneTimeWorkRequestBuilder<DelayedAnimeTrackingUpdateJob>() val request = OneTimeWorkRequestBuilder<DelayedAnimeTrackingUpdateJob>()
.setConstraints(constraints) .setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 20, TimeUnit.SECONDS) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5.minutes.toJavaDuration())
.addTag(TAG) .addTag(TAG)
.build() .build()

View file

@ -4,7 +4,6 @@ import android.content.Context
import androidx.core.content.edit import androidx.core.content.edit
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.track.anime.model.AnimeTrack
class DelayedAnimeTrackingStore(context: Context) { class DelayedAnimeTrackingStore(context: Context) {
@ -13,13 +12,12 @@ class DelayedAnimeTrackingStore(context: Context) {
*/ */
private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE) private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
fun addAnimeItem(track: AnimeTrack) { fun addAnime(trackId: Long, lastEpisodeSeen: Double) {
val trackId = track.id.toString() val previousLastChapterRead = preferences.getFloat(trackId.toString(), 0f)
val lastEpisodeSeen = preferences.getFloat(trackId, 0f) if (lastEpisodeSeen > previousLastChapterRead) {
if (track.lastEpisodeSeen > lastEpisodeSeen) { logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last episode seen: $lastEpisodeSeen" }
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last episode seen: ${track.lastEpisodeSeen}" }
preferences.edit { preferences.edit {
putFloat(trackId, track.lastEpisodeSeen.toFloat()) putFloat(trackId.toString(), lastEpisodeSeen.toFloat())
} }
} }
} }

View file

@ -0,0 +1,108 @@
package eu.kanade.domain.track.manga.interactor
import eu.kanade.domain.track.manga.model.toDbTrack
import eu.kanade.domain.track.manga.model.toDomainTrack
import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack
import eu.kanade.tachiyomi.data.track.EnhancedMangaTracker
import eu.kanade.tachiyomi.data.track.MangaTracker
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.source.MangaSource
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
import logcat.LogPriority
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withNonCancellableContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.history.manga.interactor.GetMangaHistory
import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.track.manga.interactor.GetMangaTracks
import tachiyomi.domain.track.manga.interactor.InsertMangaTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.ZoneOffset
class AddMangaTracks(
private val getTracks: GetMangaTracks,
private val insertTrack: InsertMangaTrack,
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
private val getChaptersByMangaId: GetChaptersByMangaId,
) {
// TODO: update all trackers based on common data
suspend fun bind(tracker: MangaTracker, item: MangaTrack, mangaId: Long) = withNonCancellableContext {
withIOContext {
val allChapters = getChaptersByMangaId.await(mangaId)
val hasReadChapters = allChapters.any { it.read }
tracker.bind(item, hasReadChapters)
var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext
insertTrack.await(track)
// TODO: merge into [SyncChapterProgressWithTrack]?
// Update chapter progress if newer chapters marked read locally
if (hasReadChapters) {
val latestLocalReadChapterNumber = allChapters
.sortedBy { it.chapterNumber }
.takeWhile { it.read }
.lastOrNull()
?.chapterNumber ?: -1.0
if (latestLocalReadChapterNumber > track.lastChapterRead) {
track = track.copy(
lastChapterRead = latestLocalReadChapterNumber,
)
tracker.setRemoteLastChapterRead(track.toDbTrack(), latestLocalReadChapterNumber.toInt())
}
if (track.startDate <= 0) {
val firstReadChapterDate = Injekt.get<GetMangaHistory>().await(mangaId)
.sortedBy { it.readAt }
.firstOrNull()
?.readAt
firstReadChapterDate?.let {
val startDate = firstReadChapterDate.time.convertEpochMillisZone(
ZoneOffset.systemDefault(),
ZoneOffset.UTC,
)
track = track.copy(
startDate = startDate,
)
tracker.setRemoteStartDate(track.toDbTrack(), startDate)
}
}
}
syncChapterProgressWithTrack.await(mangaId, track, tracker)
}
}
suspend fun bindEnhancedTrackers(manga: Manga, source: MangaSource) = withNonCancellableContext {
withIOContext {
getTracks.await(manga.id)
.filterIsInstance<EnhancedMangaTracker>()
.filter { it.accept(source) }
.forEach { service ->
try {
service.match(manga)?.let { track ->
track.manga_id = manga.id
(service as Tracker).mangaService.bind(track)
insertTrack.await(track.toDomainTrack()!!)
syncChapterProgressWithTrack.await(
manga.id,
track.toDomainTrack()!!,
service.mangaService,
)
}
} catch (e: Exception) {
logcat(
LogPriority.WARN,
e,
) { "Could not match manga: ${manga.title} with service $service" }
}
}
}
}
}

View file

@ -0,0 +1,46 @@
package eu.kanade.domain.track.manga.interactor
import eu.kanade.domain.track.manga.model.toDbTrack
import eu.kanade.domain.track.manga.model.toDomainTrack
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.TrackerManager
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.supervisorScope
import tachiyomi.domain.track.manga.interactor.GetMangaTracks
import tachiyomi.domain.track.manga.interactor.InsertMangaTrack
class RefreshMangaTracks(
private val getTracks: GetMangaTracks,
private val trackerManager: TrackerManager,
private val insertTrack: InsertMangaTrack,
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
) {
/**
* Fetches updated tracking data from all logged in trackers.
*
* @return Failed updates.
*/
suspend fun await(mangaId: Long): List<Pair<Tracker?, Throwable>> {
return supervisorScope {
return@supervisorScope getTracks.await(mangaId)
.map { it to trackerManager.get(it.syncId) }
.filter { (_, service) -> service?.isLoggedIn == true }
.map { (track, service) ->
async {
return@async try {
val updatedTrack = service!!.mangaService.refresh(track.toDbTrack())
insertTrack.await(updatedTrack.toDomainTrack()!!)
syncChapterProgressWithTrack.await(mangaId, track, service.mangaService)
null
} catch (e: Throwable) {
service to e
}
}
}
.awaitAll()
.filterNotNull()
}
}
}

View file

@ -1,28 +1,35 @@
package eu.kanade.domain.items.chapter.interactor package eu.kanade.domain.track.manga.interactor
import eu.kanade.domain.track.manga.model.toDbTrack import eu.kanade.domain.track.manga.model.toDbTrack
import eu.kanade.tachiyomi.data.track.MangaTrackService import eu.kanade.tachiyomi.data.track.EnhancedMangaTracker
import eu.kanade.tachiyomi.data.track.MangaTracker
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.items.chapter.interactor.UpdateChapter import tachiyomi.domain.items.chapter.interactor.UpdateChapter
import tachiyomi.domain.items.chapter.model.Chapter
import tachiyomi.domain.items.chapter.model.toChapterUpdate import tachiyomi.domain.items.chapter.model.toChapterUpdate
import tachiyomi.domain.track.manga.interactor.InsertMangaTrack import tachiyomi.domain.track.manga.interactor.InsertMangaTrack
import tachiyomi.domain.track.manga.model.MangaTrack import tachiyomi.domain.track.manga.model.MangaTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class SyncChaptersWithTrackServiceTwoWay( class SyncChapterProgressWithTrack(
private val updateChapter: UpdateChapter = Injekt.get(), private val updateChapter: UpdateChapter,
private val insertTrack: InsertMangaTrack = Injekt.get(), private val insertTrack: InsertMangaTrack,
private val getChaptersByMangaId: GetChaptersByMangaId,
) { ) {
suspend fun await( suspend fun await(
chapters: List<Chapter>, mangaId: Long,
remoteTrack: MangaTrack, remoteTrack: MangaTrack,
service: MangaTrackService, tracker: MangaTracker,
) { ) {
val sortedChapters = chapters.sortedBy { it.chapterNumber } if (tracker !is EnhancedMangaTracker) {
return
}
val sortedChapters = getChaptersByMangaId.await(mangaId)
.sortedBy { it.chapterNumber }
.filter { it.isRecognizedNumber }
val chapterUpdates = sortedChapters val chapterUpdates = sortedChapters
.filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read } .filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read }
.map { it.copy(read = true).toChapterUpdate() } .map { it.copy(read = true).toChapterUpdate() }
@ -32,7 +39,7 @@ class SyncChaptersWithTrackServiceTwoWay(
val updatedTrack = remoteTrack.copy(lastChapterRead = localLastRead.toDouble()) val updatedTrack = remoteTrack.copy(lastChapterRead = localLastRead.toDouble())
try { try {
service.update(updatedTrack.toDbTrack()) tracker.update(updatedTrack.toDbTrack())
updateChapter.awaitAll(chapterUpdates) updateChapter.awaitAll(chapterUpdates)
insertTrack.await(updatedTrack) insertTrack.await(updatedTrack)
} catch (e: Throwable) { } catch (e: Throwable) {

View file

@ -0,0 +1,60 @@
package eu.kanade.domain.track.manga.interactor
import android.content.Context
import eu.kanade.domain.track.manga.model.toDbTrack
import eu.kanade.domain.track.manga.model.toDomainTrack
import eu.kanade.domain.track.manga.service.DelayedMangaTrackingUpdateJob
import eu.kanade.domain.track.manga.store.DelayedMangaTrackingStore
import eu.kanade.tachiyomi.data.track.TrackerManager
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import logcat.LogPriority
import tachiyomi.core.util.lang.withNonCancellableContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.track.manga.interactor.GetMangaTracks
import tachiyomi.domain.track.manga.interactor.InsertMangaTrack
class TrackChapter(
private val getTracks: GetMangaTracks,
private val trackerManager: TrackerManager,
private val insertTrack: InsertMangaTrack,
private val delayedTrackingStore: DelayedMangaTrackingStore,
) {
suspend fun await(context: Context, mangaId: Long, chapterNumber: Double) {
withNonCancellableContext {
val tracks = getTracks.await(mangaId)
if (tracks.isEmpty()) return@withNonCancellableContext
tracks.mapNotNull { track ->
val service = trackerManager.get(track.syncId)
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
return@mapNotNull null
}
}
async {
runCatching {
try {
val updatedTrack = service.mangaService.refresh(track.toDbTrack())
.toDomainTrack(idRequired = true)!!
.copy(lastChapterRead = chapterNumber)
service.mangaService.update(updatedTrack.toDbTrack(), true)
insertTrack.await(updatedTrack)
delayedTrackingStore.removeMangaItem(track.id)
} catch (e: Exception) {
delayedTrackingStore.addManga(track.id, chapterNumber)
DelayedMangaTrackingUpdateJob.setupTask(context)
throw e
}
}
}
}
.awaitAll()
.mapNotNull { it.exceptionOrNull() }
.forEach { logcat(LogPriority.INFO, it) }
}
}
}

View file

@ -13,7 +13,9 @@ fun MangaTrack.copyPersonalFrom(other: MangaTrack): MangaTrack {
) )
} }
fun MangaTrack.toDbTrack(): DbMangaTrack = eu.kanade.tachiyomi.data.database.models.manga.MangaTrack.create(syncId).also { fun MangaTrack.toDbTrack(): DbMangaTrack = eu.kanade.tachiyomi.data.database.models.manga.MangaTrack.create(
syncId,
).also {
it.id = id it.id = id
it.manga_id = mangaId it.manga_id = mangaId
it.media_id = remoteId it.media_id = remoteId
@ -22,7 +24,7 @@ fun MangaTrack.toDbTrack(): DbMangaTrack = eu.kanade.tachiyomi.data.database.mod
it.last_chapter_read = lastChapterRead.toFloat() it.last_chapter_read = lastChapterRead.toFloat()
it.total_chapters = totalChapters.toInt() it.total_chapters = totalChapters.toInt()
it.status = status.toInt() it.status = status.toInt()
it.score = score it.score = score.toFloat()
it.tracking_url = remoteUrl it.tracking_url = remoteUrl
it.started_reading_date = startDate it.started_reading_date = startDate
it.finished_reading_date = finishDate it.finished_reading_date = finishDate
@ -40,7 +42,7 @@ fun DbMangaTrack.toDomainTrack(idRequired: Boolean = true): MangaTrack? {
lastChapterRead = last_chapter_read.toDouble(), lastChapterRead = last_chapter_read.toDouble(),
totalChapters = total_chapters.toLong(), totalChapters = total_chapters.toLong(),
status = status.toLong(), status = status.toLong(),
score = score, score = score.toDouble(),
remoteUrl = tracking_url, remoteUrl = tracking_url,
startDate = started_reading_date, startDate = started_reading_date,
finishDate = finished_reading_date, finishDate = finished_reading_date,

View file

@ -8,30 +8,32 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import eu.kanade.domain.track.manga.model.toDbTrack import eu.kanade.domain.track.manga.interactor.TrackChapter
import eu.kanade.domain.track.manga.store.DelayedMangaTrackingStore import eu.kanade.domain.track.manga.store.DelayedMangaTrackingStore
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.util.system.workManager import eu.kanade.tachiyomi.util.system.workManager
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.track.manga.interactor.GetMangaTracks import tachiyomi.domain.track.manga.interactor.GetMangaTracks
import tachiyomi.domain.track.manga.interactor.InsertMangaTrack
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit import kotlin.time.Duration.Companion.minutes
import kotlin.time.toJavaDuration
class DelayedMangaTrackingUpdateJob(context: Context, workerParams: WorkerParameters) : class DelayedMangaTrackingUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) { CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val getTracks = Injekt.get<GetMangaTracks>() if (runAttemptCount > 3) {
val insertTrack = Injekt.get<InsertMangaTrack>() return Result.failure()
}
val getTracks = Injekt.get<GetMangaTracks>()
val trackChapter = Injekt.get<TrackChapter>()
val trackManager = Injekt.get<TrackManager>()
val delayedTrackingStore = Injekt.get<DelayedMangaTrackingStore>() val delayedTrackingStore = Injekt.get<DelayedMangaTrackingStore>()
val results = withIOContext { withIOContext {
delayedTrackingStore.getMangaItems() delayedTrackingStore.getMangaItems()
.mapNotNull { .mapNotNull {
val track = getTracks.awaitOne(it.trackId) val track = getTracks.awaitOne(it.trackId)
@ -40,24 +42,15 @@ class DelayedMangaTrackingUpdateJob(context: Context, workerParams: WorkerParame
} }
track?.copy(lastChapterRead = it.lastChapterRead.toDouble()) track?.copy(lastChapterRead = it.lastChapterRead.toDouble())
} }
.mapNotNull { track -> .forEach { track ->
try { logcat(LogPriority.DEBUG) {
val service = trackManager.getService(track.syncId) "Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}"
if (service != null && service.isLogged) {
logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.id}, last chapter read: ${track.lastChapterRead}" }
service.mangaService.update(track.toDbTrack(), true)
insertTrack.await(track)
}
delayedTrackingStore.removeMangaItem(track.id)
null
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
false
} }
trackChapter.await(context, track.mangaId, track.lastChapterRead)
} }
} }
return if (results.isNotEmpty()) Result.failure() else Result.success() return if (delayedTrackingStore.getMangaItems().isEmpty()) Result.success() else Result.retry()
} }
companion object { companion object {
@ -70,7 +63,7 @@ class DelayedMangaTrackingUpdateJob(context: Context, workerParams: WorkerParame
val request = OneTimeWorkRequestBuilder<DelayedMangaTrackingUpdateJob>() val request = OneTimeWorkRequestBuilder<DelayedMangaTrackingUpdateJob>()
.setConstraints(constraints) .setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 20, TimeUnit.SECONDS) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5.minutes.toJavaDuration())
.addTag(TAG) .addTag(TAG)
.build() .build()

View file

@ -4,7 +4,6 @@ import android.content.Context
import androidx.core.content.edit import androidx.core.content.edit
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.track.manga.model.MangaTrack
class DelayedMangaTrackingStore(context: Context) { class DelayedMangaTrackingStore(context: Context) {
@ -13,13 +12,12 @@ class DelayedMangaTrackingStore(context: Context) {
*/ */
private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE) private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
fun addMangaItem(track: MangaTrack) { fun addManga(trackId: Long, lastChapterRead: Double) {
val trackId = track.id.toString() val previousLastChapterRead = preferences.getFloat(trackId.toString(), 0f)
val lastChapterRead = preferences.getFloat(trackId, 0f) if (lastChapterRead > previousLastChapterRead) {
if (track.lastChapterRead > lastChapterRead) { logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last chapter read: $lastChapterRead" }
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last chapter read: ${track.lastChapterRead}" }
preferences.edit { preferences.edit {
putFloat(trackId, track.lastChapterRead.toFloat()) putFloat(trackId.toString(), lastChapterRead.toFloat())
} }
} }
} }

View file

@ -1,6 +1,6 @@
package eu.kanade.domain.track.service package eu.kanade.domain.track.service
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.anilist.Anilist
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
@ -8,16 +8,16 @@ class TrackPreferences(
private val preferenceStore: PreferenceStore, private val preferenceStore: PreferenceStore,
) { ) {
fun trackUsername(sync: TrackService) = preferenceStore.getString(trackUsername(sync.id), "") fun trackUsername(sync: Tracker) = preferenceStore.getString(trackUsername(sync.id), "")
fun trackPassword(sync: TrackService) = preferenceStore.getString(trackPassword(sync.id), "") fun trackPassword(sync: Tracker) = preferenceStore.getString(trackPassword(sync.id), "")
fun setTrackCredentials(sync: TrackService, username: String, password: String) { fun setCredentials(sync: Tracker, username: String, password: String) {
trackUsername(sync).set(username) trackUsername(sync).set(username)
trackPassword(sync).set(password) trackPassword(sync).set(password)
} }
fun trackToken(sync: TrackService) = preferenceStore.getString(trackToken(sync.id), "") fun trackToken(sync: Tracker) = preferenceStore.getString(trackToken(sync.id), "")
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10) fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
@ -25,7 +25,10 @@ class TrackPreferences(
fun trackOnAddingToLibrary() = preferenceStore.getBoolean("track_on_adding_to_library", true) fun trackOnAddingToLibrary() = preferenceStore.getBoolean("track_on_adding_to_library", true)
fun showNextEpisodeAiringTime() = preferenceStore.getBoolean("show_next_episode_airing_time", true) fun showNextEpisodeAiringTime() = preferenceStore.getBoolean(
"show_next_episode_airing_time",
true,
)
companion object { companion object {
fun trackUsername(syncId: Long) = "pref_mangasync_username_$syncId" fun trackUsername(syncId: Long) = "pref_mangasync_username_$syncId"

View file

@ -28,7 +28,7 @@ class UiPreferences(
fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false) fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
fun relativeTime() = preferenceStore.getInt("relative_time", 7) fun relativeTime() = preferenceStore.getBoolean("relative_time_v2", true)
fun dateFormat() = preferenceStore.getString("app_date_format", "") fun dateFormat() = preferenceStore.getString("app_date_format", "")

View file

@ -5,21 +5,21 @@ import eu.kanade.tachiyomi.R
enum class AppTheme(val titleResId: Int?) { enum class AppTheme(val titleResId: Int?) {
DEFAULT(R.string.label_default), DEFAULT(R.string.label_default),
MONET(R.string.theme_monet), MONET(R.string.theme_monet),
CLOUDFLARE(R.string.theme_cloudflare),
COTTONCANDY(R.string.theme_cottoncandy), COTTONCANDY(R.string.theme_cottoncandy),
DOOM(R.string.theme_doom),
GREEN_APPLE(R.string.theme_greenapple), GREEN_APPLE(R.string.theme_greenapple),
LAVENDER(R.string.theme_lavender), LAVENDER(R.string.theme_lavender),
MATRIX(R.string.theme_matrix),
MIDNIGHT_DUSK(R.string.theme_midnightdusk), MIDNIGHT_DUSK(R.string.theme_midnightdusk),
MOCHA(R.string.theme_mocha), MOCHA(R.string.theme_mocha),
SAPPHIRE(R.string.theme_sapphire),
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri), STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
TAKO(R.string.theme_tako), TAKO(R.string.theme_tako),
TEALTURQUOISE(R.string.theme_tealturquoise), TEALTURQUOISE(R.string.theme_tealturquoise),
TIDAL_WAVE(R.string.theme_tidalwave), TIDAL_WAVE(R.string.theme_tidalwave),
YINYANG(R.string.theme_yinyang), YINYANG(R.string.theme_yinyang),
YOTSUBA(R.string.theme_yotsuba), YOTSUBA(R.string.theme_yotsuba),
CLOUDFLARE(R.string.theme_cloudflare),
SAPPHIRE(R.string.theme_sapphire),
DOOM(R.string.theme_doom),
MATRIX(R.string.theme_matrix),
// Deprecated // Deprecated
DARK_BLUE(null), DARK_BLUE(null),

View file

@ -25,7 +25,10 @@ fun BaseBrowseItem(
onClick = onClickItem, onClick = onClickItem,
onLongClick = onLongClickItem, onLongClick = onLongClickItem,
) )
.padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small), .padding(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
icon() icon()

View file

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
import androidx.compose.material.icons.outlined.ArrowForward import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Error import androidx.compose.material.icons.outlined.Error
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
@ -54,25 +55,13 @@ fun GlobalSearchResultItem(
Text(text = subtitle) Text(text = subtitle)
} }
IconButton(onClick = onClick) { IconButton(onClick = onClick) {
Icon(imageVector = Icons.Outlined.ArrowForward, contentDescription = null) Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
} }
} }
content() content()
} }
} }
@Composable
fun GlobalSearchEmptyResultItem() {
Text(
text = stringResource(R.string.no_results_found),
modifier = Modifier
.padding(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
),
)
}
@Composable @Composable
fun GlobalSearchLoadingResultItem() { fun GlobalSearchLoadingResultItem() {
Box( Box(

View file

@ -1,40 +0,0 @@
package eu.kanade.presentation.browse
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import eu.kanade.presentation.components.SearchToolbar
@Composable
fun GlobalSearchToolbar(
searchQuery: String?,
progress: Int,
total: Int,
navigateUp: () -> Unit,
onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
) {
Box {
SearchToolbar(
searchQuery = searchQuery,
onChangeSearchQuery = onChangeSearchQuery,
onSearch = onSearch,
onClickCloseSearch = navigateUp,
navigateUp = navigateUp,
scrollBehavior = scrollBehavior,
)
if (progress in 1 until total) {
LinearProgressIndicator(
progress = progress / total.toFloat(),
modifier = Modifier
.align(Alignment.BottomStart)
.fillMaxWidth(),
)
}
}
}

View file

@ -10,24 +10,25 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.History import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -52,11 +53,10 @@ import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.AnimeExtensionDetailsState import eu.kanade.tachiyomi.ui.browse.anime.extension.details.AnimeExtensionDetailsScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.DIVIDER_ALPHA
import tachiyomi.presentation.core.components.material.Divider
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
@ -64,7 +64,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
@Composable @Composable
fun AnimeExtensionDetailsScreen( fun AnimeExtensionDetailsScreen(
navigateUp: () -> Unit, navigateUp: () -> Unit,
state: AnimeExtensionDetailsState, state: AnimeExtensionDetailsScreenModel.State,
onClickSourcePreferences: (sourceId: Long) -> Unit, onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickWhatsNew: () -> Unit, onClickWhatsNew: () -> Unit,
onClickReadme: () -> Unit, onClickReadme: () -> Unit,
@ -81,40 +81,42 @@ fun AnimeExtensionDetailsScreen(
navigateUp = navigateUp, navigateUp = navigateUp,
actions = { actions = {
AppBarActions( AppBarActions(
actions = buildList { actions = persistentListOf<AppBar.AppBarAction>().builder()
if (state.extension?.isUnofficial == false) { .apply {
add( if (state.extension?.isUnofficial == false) {
AppBar.Action( add(
title = stringResource(R.string.whats_new), AppBar.Action(
icon = Icons.Outlined.History, title = stringResource(R.string.whats_new),
onClick = onClickWhatsNew, icon = Icons.Outlined.History,
), onClick = onClickWhatsNew,
) ),
add( )
AppBar.Action( add(
title = stringResource(R.string.action_faq_and_guides), AppBar.Action(
icon = Icons.Outlined.HelpOutline, title = stringResource(R.string.action_faq_and_guides),
onClick = onClickReadme, icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onClickReadme,
),
)
}
addAll(
listOf(
AppBar.OverflowAction(
title = stringResource(R.string.action_enable_all),
onClick = onClickEnableAll,
),
AppBar.OverflowAction(
title = stringResource(R.string.action_disable_all),
onClick = onClickDisableAll,
),
AppBar.OverflowAction(
title = stringResource(R.string.pref_clear_cookies),
onClick = onClickClearCookies,
),
), ),
) )
} }
addAll( .build(),
listOf(
AppBar.OverflowAction(
title = stringResource(R.string.action_enable_all),
onClick = onClickEnableAll,
),
AppBar.OverflowAction(
title = stringResource(R.string.action_disable_all),
onClick = onClickDisableAll,
),
AppBar.OverflowAction(
title = stringResource(R.string.pref_clear_cookies),
onClick = onClickClearCookies,
),
),
)
},
) )
}, },
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
@ -175,7 +177,8 @@ private fun AnimeExtensionDetails(
data = Uri.fromParts("package", extension.pkgName, null) data = Uri.fromParts("package", extension.pkgName, null)
context.startActivity(this) context.startActivity(this)
} }
}, Unit
}.takeIf { extension.isShared },
onClickAgeRating = { onClickAgeRating = {
showNsfwWarning = true showNsfwWarning = true
}, },
@ -187,7 +190,6 @@ private fun AnimeExtensionDetails(
key = { it.source.id }, key = { it.source.id },
) { source -> ) { source ->
SourceSwitchPreference( SourceSwitchPreference(
modifier = Modifier.animateItemPlacement(),
source = source, source = source,
onClickSourcePreferences = onClickSourcePreferences, onClickSourcePreferences = onClickSourcePreferences,
onClickSource = onClickSource, onClickSource = onClickSource,
@ -208,7 +210,7 @@ private fun DetailsHeader(
extension: AnimeExtension, extension: AnimeExtension,
onClickAgeRating: () -> Unit, onClickAgeRating: () -> Unit,
onClickUninstall: () -> Unit, onClickUninstall: () -> Unit,
onClickAppInfo: () -> Unit, onClickAppInfo: (() -> Unit)?,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -237,7 +239,9 @@ private fun DetailsHeader(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
val strippedPkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.animeextension.") val strippedPkgName = extension.pkgName.substringAfter(
"eu.kanade.tachiyomi.animeextension.",
)
Text( Text(
text = strippedPkgName, text = strippedPkgName,
@ -292,6 +296,7 @@ private fun DetailsHeader(
top = MaterialTheme.padding.small, top = MaterialTheme.padding.small,
bottom = MaterialTheme.padding.medium, bottom = MaterialTheme.padding.medium,
), ),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
OutlinedButton( OutlinedButton(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
@ -300,29 +305,29 @@ private fun DetailsHeader(
Text(stringResource(R.string.ext_uninstall)) Text(stringResource(R.string.ext_uninstall))
} }
Spacer(Modifier.width(16.dp)) if (onClickAppInfo != null) {
Button(
Button( modifier = Modifier.weight(1f),
modifier = Modifier.weight(1f), onClick = onClickAppInfo,
onClick = onClickAppInfo, ) {
) { Text(
Text( text = stringResource(R.string.ext_app_info),
text = stringResource(R.string.ext_app_info), color = MaterialTheme.colorScheme.onPrimary,
color = MaterialTheme.colorScheme.onPrimary, )
) }
} }
} }
Divider() HorizontalDivider()
} }
} }
@Composable @Composable
private fun InfoText( private fun InfoText(
modifier: Modifier,
primaryText: String, primaryText: String,
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
secondaryText: String, secondaryText: String,
modifier: Modifier = Modifier,
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
) { ) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
@ -355,20 +360,17 @@ private fun InfoText(
@Composable @Composable
private fun InfoDivider() { private fun InfoDivider() {
Divider( VerticalDivider(
modifier = Modifier modifier = Modifier.height(20.dp),
.height(20.dp)
.width(1.dp),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = DIVIDER_ALPHA),
) )
} }
@Composable @Composable
private fun SourceSwitchPreference( private fun SourceSwitchPreference(
modifier: Modifier = Modifier,
source: AnimeExtensionSourceItem, source: AnimeExtensionSourceItem,
onClickSourcePreferences: (sourceId: Long) -> Unit, onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickSource: (sourceId: Long) -> Unit, onClickSource: (sourceId: Long) -> Unit,
modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current

View file

@ -2,6 +2,7 @@ package eu.kanade.presentation.browse.anime
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -12,7 +13,6 @@ import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.anime.extension.AnimeExtensionFilterState import eu.kanade.tachiyomi.ui.browse.anime.extension.AnimeExtensionFilterState
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
@ -53,12 +53,11 @@ private fun AnimeExtensionFilterContent(
onClickLang: (String) -> Unit, onClickLang: (String) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
FastScrollLazyColumn( LazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
) { ) {
items(state.languages) { language -> items(state.languages) { language ->
SwitchPreferenceWidget( SwitchPreferenceWidget(
modifier = Modifier.animateItemPlacement(),
title = LocaleHelper.getSourceDisplayName(language, context), title = LocaleHelper.getSourceDisplayName(language, context),
checked = language in state.enabledLanguages, checked = language in state.enabledLanguages,
onCheckedChanged = { onClickLang(language) }, onCheckedChanged = { onClickLang(language) },

View file

@ -43,7 +43,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.InstallStep import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.ui.browse.anime.extension.AnimeExtensionUiModel import eu.kanade.tachiyomi.ui.browse.anime.extension.AnimeExtensionUiModel
import eu.kanade.tachiyomi.ui.browse.anime.extension.AnimeExtensionsState import eu.kanade.tachiyomi.ui.browse.anime.extension.AnimeExtensionsScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.PullRefresh import tachiyomi.presentation.core.components.material.PullRefresh
@ -56,7 +56,7 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable @Composable
fun AnimeExtensionScreen( fun AnimeExtensionScreen(
state: AnimeExtensionsState, state: AnimeExtensionsScreenModel.State,
contentPadding: PaddingValues, contentPadding: PaddingValues,
searchQuery: String?, searchQuery: String?,
onLongClickItem: (AnimeExtension) -> Unit, onLongClickItem: (AnimeExtension) -> Unit,
@ -72,10 +72,10 @@ fun AnimeExtensionScreen(
PullRefresh( PullRefresh(
refreshing = state.isRefreshing, refreshing = state.isRefreshing,
onRefresh = onRefresh, onRefresh = onRefresh,
enabled = !state.isLoading, enabled = { !state.isLoading },
) { ) {
when { when {
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
state.isEmpty -> { state.isEmpty -> {
val msg = if (!searchQuery.isNullOrEmpty()) { val msg = if (!searchQuery.isNullOrEmpty()) {
R.string.no_results_found R.string.no_results_found
@ -107,7 +107,7 @@ fun AnimeExtensionScreen(
@Composable @Composable
private fun AnimeExtensionContent( private fun AnimeExtensionContent(
state: AnimeExtensionsState, state: AnimeExtensionsScreenModel.State,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onLongClickItem: (AnimeExtension) -> Unit, onLongClickItem: (AnimeExtension) -> Unit,
onClickItemCancel: (AnimeExtension) -> Unit, onClickItemCancel: (AnimeExtension) -> Unit,
@ -147,14 +147,13 @@ private fun AnimeExtensionContent(
} }
ExtensionHeader( ExtensionHeader(
textRes = header.textRes, textRes = header.textRes,
modifier = Modifier.animateItemPlacement(),
action = action, action = action,
) )
} }
is AnimeExtensionUiModel.Header.Text -> { is AnimeExtensionUiModel.Header.Text -> {
ExtensionHeader( ExtensionHeader(
text = header.text, text = header.text,
modifier = Modifier.animateItemPlacement(),
) )
} }
} }
@ -166,7 +165,7 @@ private fun AnimeExtensionContent(
key = { "extension-${it.hashCode()}" }, key = { "extension-${it.hashCode()}" },
) { item -> ) { item ->
AnimeExtensionItem( AnimeExtensionItem(
modifier = Modifier.animateItemPlacement(),
item = item, item = item,
onClickItem = { onClickItem = {
when (it) { when (it) {
@ -216,12 +215,12 @@ private fun AnimeExtensionContent(
@Composable @Composable
private fun AnimeExtensionItem( private fun AnimeExtensionItem(
modifier: Modifier = Modifier,
item: AnimeExtensionUiModel.Item, item: AnimeExtensionUiModel.Item,
onClickItem: (AnimeExtension) -> Unit, onClickItem: (AnimeExtension) -> Unit,
onLongClickItem: (AnimeExtension) -> Unit, onLongClickItem: (AnimeExtension) -> Unit,
onClickItemCancel: (AnimeExtension) -> Unit, onClickItemCancel: (AnimeExtension) -> Unit,
onClickItemAction: (AnimeExtension) -> Unit, onClickItemAction: (AnimeExtension) -> Unit,
modifier: Modifier = Modifier,
) { ) {
val (extension, installStep) = item val (extension, installStep) = item
BaseBrowseItem( BaseBrowseItem(
@ -246,7 +245,10 @@ private fun AnimeExtensionItem(
) )
} }
val padding by animateDpAsState(targetValue = if (idle) 0.dp else 8.dp) val padding by animateDpAsState(
targetValue = if (idle) 0.dp else 8.dp,
label = "iconPadding",
)
AnimeExtensionIcon( AnimeExtensionIcon(
extension = extension, extension = extension,
modifier = Modifier modifier = Modifier
@ -295,7 +297,10 @@ private fun AnimeExtensionItemContent(
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) { ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
if (extension is AnimeExtension.Installed && extension.lang.isNotEmpty()) { if (extension is AnimeExtension.Installed && extension.lang.isNotEmpty()) {
Text( Text(
text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current), text = LocaleHelper.getSourceDisplayName(
extension.lang,
LocalContext.current,
),
) )
} }

View file

@ -12,7 +12,7 @@ import eu.kanade.presentation.browse.anime.components.BaseAnimeSourceItem
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.anime.source.AnimeSourcesFilterState import eu.kanade.tachiyomi.ui.browse.anime.source.AnimeSourcesFilterScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.domain.source.anime.model.AnimeSource import tachiyomi.domain.source.anime.model.AnimeSource
import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.FastScrollLazyColumn
@ -22,7 +22,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
@Composable @Composable
fun AnimeSourcesFilterScreen( fun AnimeSourcesFilterScreen(
navigateUp: () -> Unit, navigateUp: () -> Unit,
state: AnimeSourcesFilterState.Success, state: AnimeSourcesFilterScreenModel.State.Success,
onClickLanguage: (String) -> Unit, onClickLanguage: (String) -> Unit,
onClickSource: (AnimeSource) -> Unit, onClickSource: (AnimeSource) -> Unit,
) { ) {
@ -54,7 +54,7 @@ fun AnimeSourcesFilterScreen(
@Composable @Composable
private fun AnimeSourcesFilterContent( private fun AnimeSourcesFilterContent(
contentPadding: PaddingValues, contentPadding: PaddingValues,
state: AnimeSourcesFilterState.Success, state: AnimeSourcesFilterScreenModel.State.Success,
onClickLanguage: (String) -> Unit, onClickLanguage: (String) -> Unit,
onClickSource: (AnimeSource) -> Unit, onClickSource: (AnimeSource) -> Unit,
) { ) {
@ -64,28 +64,29 @@ private fun AnimeSourcesFilterContent(
state.items.forEach { (language, sources) -> state.items.forEach { (language, sources) ->
val enabled = language in state.enabledLanguages val enabled = language in state.enabledLanguages
item( item(
key = language.hashCode(), key = language,
contentType = "source-filter-header", contentType = "source-filter-header",
) { ) {
AnimeSourcesFilterHeader( AnimeSourcesFilterHeader(
modifier = Modifier.animateItemPlacement(),
language = language, language = language,
enabled = enabled, enabled = enabled,
onClickItem = onClickLanguage, onClickItem = onClickLanguage,
) )
} }
if (!enabled) return@forEach if (enabled) {
items( items(
items = sources, items = sources,
key = { "source-filter-${it.key()}" }, key = { "source-filter-${it.key()}" },
contentType = { "source-filter-item" }, contentType = { "source-filter-item" },
) { source -> ) { source ->
AnimeSourcesFilterItem( AnimeSourcesFilterItem(
modifier = Modifier.animateItemPlacement(),
source = source, source = source,
isEnabled = "${source.id}" !in state.disabledSources, isEnabled = "${source.id}" !in state.disabledSources,
onClickItem = onClickSource, onClickItem = onClickSource,
) )
}
} }
} }
} }
@ -93,10 +94,10 @@ private fun AnimeSourcesFilterContent(
@Composable @Composable
fun AnimeSourcesFilterHeader( fun AnimeSourcesFilterHeader(
modifier: Modifier,
language: String, language: String,
enabled: Boolean, enabled: Boolean,
onClickItem: (String) -> Unit, onClickItem: (String) -> Unit,
modifier: Modifier = Modifier,
) { ) {
SwitchPreferenceWidget( SwitchPreferenceWidget(
modifier = modifier, modifier = modifier,
@ -108,10 +109,10 @@ fun AnimeSourcesFilterHeader(
@Composable @Composable
private fun AnimeSourcesFilterItem( private fun AnimeSourcesFilterItem(
modifier: Modifier,
source: AnimeSource, source: AnimeSource,
isEnabled: Boolean, isEnabled: Boolean,
onClickItem: (AnimeSource) -> Unit, onClickItem: (AnimeSource) -> Unit,
modifier: Modifier = Modifier,
) { ) {
BaseAnimeSourceItem( BaseAnimeSourceItem(
modifier = modifier, modifier = modifier,

View file

@ -23,7 +23,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.browse.anime.components.BaseAnimeSourceItem import eu.kanade.presentation.browse.anime.components.BaseAnimeSourceItem
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.anime.source.AnimeSourcesState import eu.kanade.tachiyomi.ui.browse.anime.source.AnimeSourcesScreenModel
import eu.kanade.tachiyomi.ui.browse.anime.source.browse.BrowseAnimeSourceScreenModel.Listing import eu.kanade.tachiyomi.ui.browse.anime.source.browse.BrowseAnimeSourceScreenModel.Listing
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.domain.source.anime.model.AnimeSource import tachiyomi.domain.source.anime.model.AnimeSource
@ -40,14 +40,14 @@ import tachiyomi.source.local.entries.anime.LocalAnimeSource
@Composable @Composable
fun AnimeSourcesScreen( fun AnimeSourcesScreen(
state: AnimeSourcesState, state: AnimeSourcesScreenModel.State,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onClickItem: (AnimeSource, Listing) -> Unit, onClickItem: (AnimeSource, Listing) -> Unit,
onClickPin: (AnimeSource) -> Unit, onClickPin: (AnimeSource) -> Unit,
onLongClickItem: (AnimeSource) -> Unit, onLongClickItem: (AnimeSource) -> Unit,
) { ) {
when { when {
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
state.isEmpty -> EmptyScreen( state.isEmpty -> EmptyScreen(
textResource = R.string.source_empty_screen, textResource = R.string.source_empty_screen,
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
@ -74,12 +74,12 @@ fun AnimeSourcesScreen(
when (model) { when (model) {
is AnimeSourceUiModel.Header -> { is AnimeSourceUiModel.Header -> {
AnimeSourceHeader( AnimeSourceHeader(
modifier = Modifier.animateItemPlacement(),
language = model.language, language = model.language,
) )
} }
is AnimeSourceUiModel.Item -> AnimeSourceItem( is AnimeSourceUiModel.Item -> AnimeSourceItem(
modifier = Modifier.animateItemPlacement(),
source = model.source, source = model.source,
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = onLongClickItem, onLongClickItem = onLongClickItem,
@ -94,25 +94,28 @@ fun AnimeSourcesScreen(
@Composable @Composable
private fun AnimeSourceHeader( private fun AnimeSourceHeader(
modifier: Modifier = Modifier,
language: String, language: String,
modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current
Text( Text(
text = LocaleHelper.getSourceDisplayName(language, context), text = LocaleHelper.getSourceDisplayName(language, context),
modifier = modifier modifier = modifier
.padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small), .padding(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
),
style = MaterialTheme.typography.header, style = MaterialTheme.typography.header,
) )
} }
@Composable @Composable
private fun AnimeSourceItem( private fun AnimeSourceItem(
modifier: Modifier = Modifier,
source: AnimeSource, source: AnimeSource,
onClickItem: (AnimeSource, Listing) -> Unit, onClickItem: (AnimeSource, Listing) -> Unit,
onLongClickItem: (AnimeSource) -> Unit, onLongClickItem: (AnimeSource) -> Unit,
onClickPin: (AnimeSource) -> Unit, onClickPin: (AnimeSource) -> Unit,
modifier: Modifier = Modifier,
) { ) {
BaseAnimeSourceItem( BaseAnimeSourceItem(
modifier = modifier, modifier = modifier,
@ -144,7 +147,13 @@ private fun AnimeSourcePinButton(
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin
val tint = if (isPinned) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground.copy(alpha = SecondaryItemAlpha) val tint = if (isPinned) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onBackground.copy(
alpha = SecondaryItemAlpha,
)
}
val description = if (isPinned) R.string.action_unpin else R.string.action_pin val description = if (isPinned) R.string.action_unpin else R.string.action_pin
IconButton(onClick = onClick) { IconButton(onClick = onClick) {
Icon( Icon(
@ -192,7 +201,7 @@ fun AnimeSourceOptionsDialog(
) )
} }
sealed class AnimeSourceUiModel { sealed interface AnimeSourceUiModel {
data class Item(val source: AnimeSource) : AnimeSourceUiModel() data class Item(val source: AnimeSource) : AnimeSourceUiModel
data class Header(val language: String) : AnimeSourceUiModel() data class Header(val language: String) : AnimeSourceUiModel
} }

View file

@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Public import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Refresh
@ -24,6 +25,7 @@ import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.util.formattedMessage import eu.kanade.presentation.util.formattedMessage
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.AnimeSource import eu.kanade.tachiyomi.animesource.AnimeSource
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibraryDisplayMode
@ -61,12 +63,12 @@ fun BrowseAnimeSourceContent(
if (animeList.itemCount > 0 && errorState != null && errorState is LoadState.Error) { if (animeList.itemCount > 0 && errorState != null && errorState is LoadState.Error) {
val result = snackbarHostState.showSnackbar( val result = snackbarHostState.showSnackbar(
message = getErrorMessage(errorState), message = getErrorMessage(errorState),
actionLabel = context.getString(R.string.action_webview_refresh), actionLabel = context.getString(R.string.action_retry),
duration = SnackbarDuration.Indefinite, duration = SnackbarDuration.Indefinite,
) )
when (result) { when (result) {
SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss() SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss()
SnackbarResult.ActionPerformed -> animeList.refresh() SnackbarResult.ActionPerformed -> animeList.retry()
} }
} }
} }
@ -76,15 +78,15 @@ fun BrowseAnimeSourceContent(
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
message = getErrorMessage(errorState), message = getErrorMessage(errorState),
actions = if (source is LocalAnimeSource) { actions = if (source is LocalAnimeSource) {
listOf( persistentListOf(
EmptyScreenAction( EmptyScreenAction(
stringResId = R.string.local_source_help_guide, stringResId = R.string.local_source_help_guide,
icon = Icons.Outlined.HelpOutline, icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onLocalAnimeSourceHelpClick, onClick = onLocalAnimeSourceHelpClick,
), ),
) )
} else { } else {
listOf( persistentListOf(
EmptyScreenAction( EmptyScreenAction(
stringResId = R.string.action_retry, stringResId = R.string.action_retry,
icon = Icons.Outlined.Refresh, icon = Icons.Outlined.Refresh,
@ -97,7 +99,7 @@ fun BrowseAnimeSourceContent(
), ),
EmptyScreenAction( EmptyScreenAction(
stringResId = R.string.label_help, stringResId = R.string.label_help,
icon = Icons.Outlined.HelpOutline, icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onHelpClick, onClick = onHelpClick,
), ),
) )
@ -145,7 +147,7 @@ fun BrowseAnimeSourceContent(
} }
@Composable @Composable
fun MissingSourceScreen( internal fun MissingSourceScreen(
source: StubAnimeSource, source: StubAnimeSource,
navigateUp: () -> Unit, navigateUp: () -> Unit,
) { ) {

View file

@ -1,34 +1,30 @@
package eu.kanade.presentation.browse.anime package eu.kanade.presentation.browse.anime
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.browse.GlobalSearchErrorResultItem import eu.kanade.presentation.browse.GlobalSearchErrorResultItem
import eu.kanade.presentation.browse.GlobalSearchLoadingResultItem import eu.kanade.presentation.browse.GlobalSearchLoadingResultItem
import eu.kanade.presentation.browse.GlobalSearchResultItem import eu.kanade.presentation.browse.GlobalSearchResultItem
import eu.kanade.presentation.browse.GlobalSearchToolbar
import eu.kanade.presentation.browse.anime.components.GlobalAnimeSearchCardRow import eu.kanade.presentation.browse.anime.components.GlobalAnimeSearchCardRow
import eu.kanade.tachiyomi.R import eu.kanade.presentation.browse.anime.components.GlobalAnimeSearchToolbar
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.AnimeSearchItemResult import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.AnimeSearchItemResult
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.GlobalAnimeSearchState import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.AnimeSearchScreenModel
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.AnimeSourceFilter
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.presentation.core.components.LazyColumn
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
@Composable @Composable
fun GlobalAnimeSearchScreen( fun GlobalAnimeSearchScreen(
state: GlobalAnimeSearchState, state: AnimeSearchScreenModel.State,
navigateUp: () -> Unit, navigateUp: () -> Unit,
onChangeSearchQuery: (String?) -> Unit, onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit, onSearch: (String) -> Unit,
onChangeSearchFilter: (AnimeSourceFilter) -> Unit,
onToggleResults: () -> Unit,
getAnime: @Composable (Anime) -> State<Anime>, getAnime: @Composable (Anime) -> State<Anime>,
onClickSource: (AnimeCatalogueSource) -> Unit, onClickSource: (AnimeCatalogueSource) -> Unit,
onClickItem: (Anime) -> Unit, onClickItem: (Anime) -> Unit,
@ -36,19 +32,23 @@ fun GlobalAnimeSearchScreen(
) { ) {
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
GlobalSearchToolbar( GlobalAnimeSearchToolbar(
searchQuery = state.searchQuery, searchQuery = state.searchQuery,
progress = state.progress, progress = state.progress,
total = state.total, total = state.total,
navigateUp = navigateUp, navigateUp = navigateUp,
onChangeSearchQuery = onChangeSearchQuery, onChangeSearchQuery = onChangeSearchQuery,
onSearch = onSearch, onSearch = onSearch,
sourceFilter = state.sourceFilter,
onChangeSearchFilter = onChangeSearchFilter,
onlyShowHasResults = state.onlyShowHasResults,
onToggleResults = onToggleResults,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) )
}, },
) { paddingValues -> ) { paddingValues ->
GlobalAnimeSearchContent( GlobalSearchContent(
items = state.items, items = state.filteredItems,
contentPadding = paddingValues, contentPadding = paddingValues,
getAnime = getAnime, getAnime = getAnime,
onClickSource = onClickSource, onClickSource = onClickSource,
@ -59,13 +59,14 @@ fun GlobalAnimeSearchScreen(
} }
@Composable @Composable
private fun GlobalAnimeSearchContent( internal fun GlobalSearchContent(
items: Map<AnimeCatalogueSource, AnimeSearchItemResult>, items: Map<AnimeCatalogueSource, AnimeSearchItemResult>,
contentPadding: PaddingValues, contentPadding: PaddingValues,
getAnime: @Composable (Anime) -> State<Anime>, getAnime: @Composable (Anime) -> State<Anime>,
onClickSource: (AnimeCatalogueSource) -> Unit, onClickSource: (AnimeCatalogueSource) -> Unit,
onClickItem: (Anime) -> Unit, onClickItem: (Anime) -> Unit,
onLongClickItem: (Anime) -> Unit, onLongClickItem: (Anime) -> Unit,
fromSourceId: Long? = null,
) { ) {
LazyColumn( LazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
@ -73,7 +74,8 @@ private fun GlobalAnimeSearchContent(
items.forEach { (source, result) -> items.forEach { (source, result) ->
item(key = source.id) { item(key = source.id) {
GlobalSearchResultItem( GlobalSearchResultItem(
title = source.name, title = fromSourceId
?.let { "${source.name}".takeIf { source.id == fromSourceId } } ?: source.name,
subtitle = LocaleHelper.getDisplayName(source.lang), subtitle = LocaleHelper.getDisplayName(source.lang),
onClick = { onClickSource(source) }, onClick = { onClickSource(source) },
) { ) {
@ -82,18 +84,6 @@ private fun GlobalAnimeSearchContent(
GlobalSearchLoadingResultItem() GlobalSearchLoadingResultItem()
} }
is AnimeSearchItemResult.Success -> { is AnimeSearchItemResult.Success -> {
if (result.isEmpty) {
Text(
text = stringResource(R.string.no_results_found),
modifier = Modifier
.padding(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
),
)
return@GlobalSearchResultItem
}
GlobalAnimeSearchCardRow( GlobalAnimeSearchCardRow(
titles = result.result, titles = result.result,
getAnime = getAnime, getAnime = getAnime,

View file

@ -8,7 +8,7 @@ import androidx.compose.ui.Modifier
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.entries.anime.components.BaseAnimeListItem import eu.kanade.presentation.entries.anime.components.BaseAnimeListItem
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.anime.migration.anime.MigrateAnimeState import eu.kanade.tachiyomi.ui.browse.anime.migration.anime.MigrateAnimeScreenModel
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
@ -18,7 +18,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
fun MigrateAnimeScreen( fun MigrateAnimeScreen(
navigateUp: () -> Unit, navigateUp: () -> Unit,
title: String?, title: String?,
state: MigrateAnimeState, state: MigrateAnimeScreenModel.State,
onClickItem: (Anime) -> Unit, onClickItem: (Anime) -> Unit,
onClickCover: (Anime) -> Unit, onClickCover: (Anime) -> Unit,
) { ) {
@ -51,7 +51,7 @@ fun MigrateAnimeScreen(
@Composable @Composable
private fun MigrateAnimeContent( private fun MigrateAnimeContent(
contentPadding: PaddingValues, contentPadding: PaddingValues,
state: MigrateAnimeState, state: MigrateAnimeScreenModel.State,
onClickItem: (Anime) -> Unit, onClickItem: (Anime) -> Unit,
onClickCover: (Anime) -> Unit, onClickCover: (Anime) -> Unit,
) { ) {
@ -70,10 +70,10 @@ private fun MigrateAnimeContent(
@Composable @Composable
private fun MigrateAnimeItem( private fun MigrateAnimeItem(
modifier: Modifier = Modifier,
anime: Anime, anime: Anime,
onClickItem: (Anime) -> Unit, onClickItem: (Anime) -> Unit,
onClickCover: (Anime) -> Unit, onClickCover: (Anime) -> Unit,
modifier: Modifier = Modifier,
) { ) {
BaseAnimeListItem( BaseAnimeListItem(
modifier = modifier, modifier = modifier,

View file

@ -1,49 +1,48 @@
package eu.kanade.presentation.browse.anime package eu.kanade.presentation.browse.anime
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import eu.kanade.presentation.browse.GlobalSearchEmptyResultItem import eu.kanade.presentation.browse.anime.components.GlobalAnimeSearchToolbar
import eu.kanade.presentation.browse.GlobalSearchErrorResultItem
import eu.kanade.presentation.browse.GlobalSearchLoadingResultItem
import eu.kanade.presentation.browse.GlobalSearchResultItem
import eu.kanade.presentation.browse.GlobalSearchToolbar
import eu.kanade.presentation.browse.anime.components.GlobalAnimeSearchCardRow
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.ui.browse.anime.migration.search.MigrateAnimeSearchState import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.AnimeSearchScreenModel
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.AnimeSearchItemResult import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.AnimeSourceFilter
import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.presentation.core.components.LazyColumn
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
@Composable @Composable
fun MigrateAnimeSearchScreen( fun MigrateAnimeSearchScreen(
state: AnimeSearchScreenModel.State,
fromSourceId: Long?,
navigateUp: () -> Unit, navigateUp: () -> Unit,
state: MigrateAnimeSearchState,
getAnime: @Composable (Anime) -> State<Anime>,
onChangeSearchQuery: (String?) -> Unit, onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit, onSearch: (String) -> Unit,
onChangeSearchFilter: (AnimeSourceFilter) -> Unit,
onToggleResults: () -> Unit,
getAnime: @Composable (Anime) -> State<Anime>,
onClickSource: (AnimeCatalogueSource) -> Unit, onClickSource: (AnimeCatalogueSource) -> Unit,
onClickItem: (Anime) -> Unit, onClickItem: (Anime) -> Unit,
onLongClickItem: (Anime) -> Unit, onLongClickItem: (Anime) -> Unit,
) { ) {
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
GlobalSearchToolbar( GlobalAnimeSearchToolbar(
searchQuery = state.searchQuery, searchQuery = state.searchQuery,
progress = state.progress, progress = state.progress,
total = state.total, total = state.total,
navigateUp = navigateUp, navigateUp = navigateUp,
onChangeSearchQuery = onChangeSearchQuery, onChangeSearchQuery = onChangeSearchQuery,
onSearch = onSearch, onSearch = onSearch,
sourceFilter = state.sourceFilter,
onChangeSearchFilter = onChangeSearchFilter,
onlyShowHasResults = state.onlyShowHasResults,
onToggleResults = onToggleResults,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) )
}, },
) { paddingValues -> ) { paddingValues ->
MigrateAnimeSearchContent( GlobalSearchContent(
sourceId = state.anime?.source ?: -1, fromSourceId = fromSourceId,
items = state.items, items = state.filteredItems,
contentPadding = paddingValues, contentPadding = paddingValues,
getAnime = getAnime, getAnime = getAnime,
onClickSource = onClickSource, onClickSource = onClickSource,
@ -52,50 +51,3 @@ fun MigrateAnimeSearchScreen(
) )
} }
} }
@Composable
fun MigrateAnimeSearchContent(
sourceId: Long,
items: Map<AnimeCatalogueSource, AnimeSearchItemResult>,
contentPadding: PaddingValues,
getAnime: @Composable (Anime) -> State<Anime>,
onClickSource: (AnimeCatalogueSource) -> Unit,
onClickItem: (Anime) -> Unit,
onLongClickItem: (Anime) -> Unit,
) {
LazyColumn(
contentPadding = contentPadding,
) {
items.forEach { (source, result) ->
item(key = source.id) {
GlobalSearchResultItem(
title = if (source.id == sourceId) "${source.name}" else source.name,
subtitle = LocaleHelper.getDisplayName(source.lang),
onClick = { onClickSource(source) },
) {
when (result) {
AnimeSearchItemResult.Loading -> {
GlobalSearchLoadingResultItem()
}
is AnimeSearchItemResult.Success -> {
if (result.isEmpty) {
GlobalSearchEmptyResultItem()
return@GlobalSearchResultItem
}
GlobalAnimeSearchCardRow(
titles = result.result,
getAnime = getAnime,
onClick = onClickItem,
onLongClick = onLongClickItem,
)
}
is AnimeSearchItemResult.Error -> {
GlobalSearchErrorResultItem(message = result.throwable.message)
}
}
}
}
}
}
}

View file

@ -26,7 +26,7 @@ import eu.kanade.domain.source.service.SetMigrateSorting
import eu.kanade.presentation.browse.anime.components.AnimeSourceIcon import eu.kanade.presentation.browse.anime.components.AnimeSourceIcon
import eu.kanade.presentation.browse.anime.components.BaseAnimeSourceItem import eu.kanade.presentation.browse.anime.components.BaseAnimeSourceItem
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.anime.migration.sources.MigrateAnimeSourceState import eu.kanade.tachiyomi.ui.browse.anime.migration.sources.MigrateAnimeSourceScreenModel
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.domain.source.anime.model.AnimeSource import tachiyomi.domain.source.anime.model.AnimeSource
import tachiyomi.presentation.core.components.Badge import tachiyomi.presentation.core.components.Badge
@ -43,7 +43,7 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable @Composable
fun MigrateAnimeSourceScreen( fun MigrateAnimeSourceScreen(
state: MigrateAnimeSourceState, state: MigrateAnimeSourceScreenModel.State,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onClickItem: (AnimeSource) -> Unit, onClickItem: (AnimeSource) -> Unit,
onToggleSortingDirection: () -> Unit, onToggleSortingDirection: () -> Unit,
@ -51,7 +51,7 @@ fun MigrateAnimeSourceScreen(
) { ) {
val context = LocalContext.current val context = LocalContext.current
when { when {
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
state.isEmpty -> EmptyScreen( state.isEmpty -> EmptyScreen(
textResource = R.string.information_empty_library, textResource = R.string.information_empty_library,
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
@ -102,14 +102,26 @@ private fun MigrateAnimeSourceList(
IconButton(onClick = onToggleSortingMode) { IconButton(onClick = onToggleSortingMode) {
when (sortingMode) { when (sortingMode) {
SetMigrateSorting.Mode.ALPHABETICAL -> Icon(Icons.Outlined.SortByAlpha, contentDescription = stringResource(R.string.action_sort_alpha)) SetMigrateSorting.Mode.ALPHABETICAL -> Icon(
SetMigrateSorting.Mode.TOTAL -> Icon(Icons.Outlined.Numbers, contentDescription = stringResource(R.string.action_sort_count)) Icons.Outlined.SortByAlpha,
contentDescription = stringResource(R.string.action_sort_alpha),
)
SetMigrateSorting.Mode.TOTAL -> Icon(
Icons.Outlined.Numbers,
contentDescription = stringResource(R.string.action_sort_count),
)
} }
} }
IconButton(onClick = onToggleSortingDirection) { IconButton(onClick = onToggleSortingDirection) {
when (sortingDirection) { when (sortingDirection) {
SetMigrateSorting.Direction.ASCENDING -> Icon(Icons.Outlined.ArrowUpward, contentDescription = stringResource(R.string.action_asc)) SetMigrateSorting.Direction.ASCENDING -> Icon(
SetMigrateSorting.Direction.DESCENDING -> Icon(Icons.Outlined.ArrowDownward, contentDescription = stringResource(R.string.action_desc)) Icons.Outlined.ArrowUpward,
contentDescription = stringResource(R.string.action_asc),
)
SetMigrateSorting.Direction.DESCENDING -> Icon(
Icons.Outlined.ArrowDownward,
contentDescription = stringResource(R.string.action_desc),
)
} }
} }
} }
@ -120,7 +132,7 @@ private fun MigrateAnimeSourceList(
key = { (source, _) -> "migrate-${source.id}" }, key = { (source, _) -> "migrate-${source.id}" },
) { (source, count) -> ) { (source, count) ->
MigrateAnimeSourceItem( MigrateAnimeSourceItem(
modifier = Modifier.animateItemPlacement(),
source = source, source = source,
count = count, count = count,
onClickItem = { onClickItem(source) }, onClickItem = { onClickItem(source) },
@ -132,11 +144,11 @@ private fun MigrateAnimeSourceList(
@Composable @Composable
private fun MigrateAnimeSourceItem( private fun MigrateAnimeSourceItem(
modifier: Modifier = Modifier,
source: AnimeSource, source: AnimeSource,
count: Long, count: Long,
onClickItem: () -> Unit, onClickItem: () -> Unit,
onLongClickItem: () -> Unit, onLongClickItem: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
BaseAnimeSourceItem( BaseAnimeSourceItem(
modifier = modifier, modifier = modifier,

View file

@ -17,8 +17,8 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable @Composable
fun BaseAnimeSourceItem( fun BaseAnimeSourceItem(
modifier: Modifier = Modifier,
source: AnimeSource, source: AnimeSource,
modifier: Modifier = Modifier,
showLanguageInContent: Boolean = true, showLanguageInContent: Boolean = true,
onClickItem: () -> Unit = {}, onClickItem: () -> Unit = {},
onLongClickItem: () -> Unit = {}, onLongClickItem: () -> Unit = {},
@ -26,7 +26,9 @@ fun BaseAnimeSourceItem(
action: @Composable RowScope.(AnimeSource) -> Unit = {}, action: @Composable RowScope.(AnimeSource) -> Unit = {},
content: @Composable RowScope.(AnimeSource, String?) -> Unit = defaultContent, content: @Composable RowScope.(AnimeSource, String?) -> Unit = defaultContent,
) { ) {
val sourceLangString = LocaleHelper.getSourceDisplayName(source.lang, LocalContext.current).takeIf { showLanguageInContent } val sourceLangString = LocaleHelper.getSourceDisplayName(source.lang, LocalContext.current).takeIf {
showLanguageInContent
}
BaseBrowseItem( BaseBrowseItem(
modifier = modifier, modifier = modifier,
onClickItem = onClickItem, onClickItem = onClickItem,

View file

@ -1,6 +1,5 @@
package eu.kanade.presentation.browse.anime.components package eu.kanade.presentation.browse.anime.components
import android.content.pm.PackageManager
import android.util.DisplayMetrics import android.util.DisplayMetrics
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -31,6 +30,7 @@ import eu.kanade.domain.source.anime.model.icon
import eu.kanade.presentation.util.rememberResourceBitmapPainter import eu.kanade.presentation.util.rememberResourceBitmapPainter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.source.anime.model.AnimeSource import tachiyomi.domain.source.anime.model.AnimeSource
import tachiyomi.source.local.entries.anime.LocalAnimeSource import tachiyomi.source.local.entries.anime.LocalAnimeSource
@ -127,7 +127,10 @@ private fun AnimeExtension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) { return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) {
withIOContext { withIOContext {
value = try { value = try {
val appInfo = context.packageManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA) val appInfo = AnimeExtensionLoader.getAnimeExtensionPackageInfoFromPkgName(
context,
pkgName,
)!!.applicationInfo
val appResources = context.packageManager.getResourcesForApplication(appInfo) val appResources = context.packageManager.getResourcesForApplication(appInfo)
Result.Success( Result.Success(
appResources.getDrawableForDensity(appInfo.icon, density, null)!! appResources.getDrawableForDensity(appInfo.icon, density, null)!!
@ -142,7 +145,7 @@ private fun AnimeExtension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT
} }
sealed class Result<out T> { sealed class Result<out T> {
object Loading : Result<Nothing>() data object Loading : Result<Nothing>()
object Error : Result<Nothing>() data object Error : Result<Nothing>()
data class Success<out T>(val value: T) : Result<T>() data class Success<out T>(val value: T) : Result<T>()
} }

View file

@ -40,7 +40,7 @@ fun BrowseAnimeSourceComfortableGrid(
} }
} }
items(animeList.itemCount) { index -> items(count = animeList.itemCount) { index ->
val anime by animeList[index]?.collectAsState() ?: return@items val anime by animeList[index]?.collectAsState() ?: return@items
BrowseAnimeSourceComfortableGridItem( BrowseAnimeSourceComfortableGridItem(
anime = anime, anime = anime,

View file

@ -40,7 +40,7 @@ fun BrowseAnimeSourceCompactGrid(
} }
} }
items(animeList.itemCount) { index -> items(count = animeList.itemCount) { index ->
val anime by animeList[index]?.collectAsState() ?: return@items val anime by animeList[index]?.collectAsState() ?: return@items
BrowseAnimeSourceCompactGridItem( BrowseAnimeSourceCompactGridItem(
anime = anime, anime = anime,

View file

@ -1,13 +1,13 @@
package eu.kanade.presentation.browse.anime.components package eu.kanade.presentation.browse.anime.components
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.items
import eu.kanade.presentation.browse.InLibraryBadge import eu.kanade.presentation.browse.InLibraryBadge
import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem
import eu.kanade.presentation.library.CommonEntryItemDefaults import eu.kanade.presentation.library.CommonEntryItemDefaults
@ -15,7 +15,6 @@ import eu.kanade.presentation.library.EntryListItem
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.entries.anime.model.AnimeCover import tachiyomi.domain.entries.anime.model.AnimeCover
import tachiyomi.presentation.core.components.LazyColumn
import tachiyomi.presentation.core.util.plus import tachiyomi.presentation.core.util.plus
@Composable @Composable
@ -34,9 +33,8 @@ fun BrowseAnimeSourceList(
} }
} }
items(animeList) { animeflow -> items(count = animeList.itemCount) { index ->
animeflow ?: return@items val anime by animeList[index]?.collectAsState() ?: return@items
val anime by animeflow.collectAsState()
BrowseAnimeSourceListItem( BrowseAnimeSourceListItem(
anime = anime, anime = anime,
onClick = { onAnimeClick(anime) }, onClick = { onAnimeClick(anime) },

View file

@ -1,6 +1,7 @@
package eu.kanade.presentation.browse.anime.components package eu.kanade.presentation.browse.anime.components
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ViewList
import androidx.compose.material.icons.filled.ViewList import androidx.compose.material.icons.filled.ViewList
import androidx.compose.material.icons.filled.ViewModule import androidx.compose.material.icons.filled.ViewModule
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -20,6 +21,7 @@ import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.AnimeSource import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.source.local.entries.anime.LocalAnimeSource import tachiyomi.source.local.entries.anime.LocalAnimeSource
@ -53,28 +55,44 @@ fun BrowseAnimeSourceToolbar(
onClickCloseSearch = navigateUp, onClickCloseSearch = navigateUp,
actions = { actions = {
AppBarActions( AppBarActions(
actions = listOfNotNull( actions = persistentListOf<AppBar.AppBarAction>().builder()
AppBar.Action( .apply {
title = stringResource(R.string.action_display_mode), add(
icon = if (displayMode == LibraryDisplayMode.List) Icons.Filled.ViewList else Icons.Filled.ViewModule, AppBar.Action(
onClick = { selectingDisplayMode = true }, title = stringResource(R.string.action_display_mode),
), icon = if (displayMode == LibraryDisplayMode.List) {
if (isLocalSource) { Icons.AutoMirrored.Filled.ViewList
AppBar.OverflowAction( } else {
title = stringResource(R.string.label_help), Icons.Filled.ViewModule
onClick = onHelpClick, },
onClick = { selectingDisplayMode = true },
),
) )
} else { if (isLocalSource) {
AppBar.OverflowAction( add(
title = stringResource(R.string.action_open_in_web_view), AppBar.OverflowAction(
onClick = onWebViewClick, title = stringResource(R.string.label_help),
) onClick = onHelpClick,
}, ),
AppBar.OverflowAction( )
title = stringResource(R.string.action_settings), } else {
onClick = onSettingsClick, add(
).takeIf { isConfigurableSource }, AppBar.OverflowAction(
), title = stringResource(R.string.action_open_in_web_view),
onClick = onWebViewClick,
),
)
}
if (isConfigurableSource) {
add(
AppBar.OverflowAction(
title = stringResource(R.string.action_settings),
onClick = onSettingsClick,
),
)
}
}
.build(),
) )
DropdownMenu( DropdownMenu(

View file

@ -1,15 +1,26 @@
package eu.kanade.presentation.browse.anime.components package eu.kanade.presentation.browse.anime.components
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import eu.kanade.presentation.browse.GlobalSearchCard import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.browse.InLibraryBadge
import eu.kanade.presentation.library.CommonEntryItemDefaults
import eu.kanade.presentation.library.EntryComfortableGridItem
import eu.kanade.tachiyomi.R
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.entries.anime.model.AnimeCover
import tachiyomi.domain.entries.anime.model.asAnimeCover import tachiyomi.domain.entries.anime.model.asAnimeCover
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
@ -20,13 +31,18 @@ fun GlobalAnimeSearchCardRow(
onClick: (Anime) -> Unit, onClick: (Anime) -> Unit,
onLongClick: (Anime) -> Unit, onLongClick: (Anime) -> Unit,
) { ) {
if (titles.isEmpty()) {
EmptyResultItem()
return
}
LazyRow( LazyRow(
contentPadding = PaddingValues(MaterialTheme.padding.small), contentPadding = PaddingValues(MaterialTheme.padding.small),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
) { ) {
items(titles) { items(titles) {
val title by getAnime(it) val title by getAnime(it)
GlobalSearchCard( AnimeItem(
title = title.title, title = title.title,
cover = title.asAnimeCover(), cover = title.asAnimeCover(),
isFavorite = title.favorite, isFavorite = title.favorite,
@ -36,3 +52,38 @@ fun GlobalAnimeSearchCardRow(
} }
} }
} }
@Composable
private fun AnimeItem(
title: String,
cover: AnimeCover,
isFavorite: Boolean,
onClick: () -> Unit,
onLongClick: () -> Unit,
) {
Box(modifier = Modifier.width(96.dp)) {
EntryComfortableGridItem(
title = title,
titleMaxLines = 3,
coverData = cover,
coverBadgeStart = {
InLibraryBadge(enabled = isFavorite)
},
coverAlpha = if (isFavorite) CommonEntryItemDefaults.BrowseFavoriteCoverAlpha else 1f,
onClick = onClick,
onLongClick = onLongClick,
)
}
}
@Composable
private fun EmptyResultItem() {
Text(
text = stringResource(R.string.no_results_found),
modifier = Modifier
.padding(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
),
)
}

View file

@ -0,0 +1,128 @@
package eu.kanade.presentation.browse.anime.components
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.DoneAll
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.PushPin
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.AnimeSourceFilter
import tachiyomi.presentation.core.components.material.padding
@Composable
fun GlobalAnimeSearchToolbar(
searchQuery: String?,
progress: Int,
total: Int,
navigateUp: () -> Unit,
onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit,
sourceFilter: AnimeSourceFilter,
onChangeSearchFilter: (AnimeSourceFilter) -> Unit,
onlyShowHasResults: Boolean,
onToggleResults: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
) {
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
Box {
SearchToolbar(
searchQuery = searchQuery,
onChangeSearchQuery = onChangeSearchQuery,
onSearch = onSearch,
onClickCloseSearch = navigateUp,
navigateUp = navigateUp,
scrollBehavior = scrollBehavior,
)
if (progress in 1..<total) {
LinearProgressIndicator(
progress = { progress / total.toFloat() },
modifier = Modifier
.align(Alignment.BottomStart)
.fillMaxWidth(),
)
}
}
Row(
modifier = Modifier
.horizontalScroll(rememberScrollState())
.padding(horizontal = MaterialTheme.padding.small),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
// TODO: make this UX better; it only applies when triggering a new search
FilterChip(
selected = sourceFilter == AnimeSourceFilter.PinnedOnly,
onClick = { onChangeSearchFilter(AnimeSourceFilter.PinnedOnly) },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.PushPin,
contentDescription = null,
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(id = R.string.pinned_sources))
},
)
FilterChip(
selected = sourceFilter == AnimeSourceFilter.All,
onClick = { onChangeSearchFilter(AnimeSourceFilter.All) },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.DoneAll,
contentDescription = null,
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(id = R.string.all))
},
)
VerticalDivider()
FilterChip(
selected = onlyShowHasResults,
onClick = { onToggleResults() },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.FilterList,
contentDescription = null,
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(id = R.string.has_results))
},
)
}
HorizontalDivider()
}
}

View file

@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Public import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Refresh
@ -24,6 +25,7 @@ import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.util.formattedMessage import eu.kanade.presentation.util.formattedMessage
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.MangaSource import eu.kanade.tachiyomi.source.MangaSource
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibraryDisplayMode
@ -61,12 +63,12 @@ fun BrowseSourceContent(
if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) { if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) {
val result = snackbarHostState.showSnackbar( val result = snackbarHostState.showSnackbar(
message = getErrorMessage(errorState), message = getErrorMessage(errorState),
actionLabel = context.getString(R.string.action_webview_refresh), actionLabel = context.getString(R.string.action_retry),
duration = SnackbarDuration.Indefinite, duration = SnackbarDuration.Indefinite,
) )
when (result) { when (result) {
SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss() SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss()
SnackbarResult.ActionPerformed -> mangaList.refresh() SnackbarResult.ActionPerformed -> mangaList.retry()
} }
} }
} }
@ -76,15 +78,15 @@ fun BrowseSourceContent(
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
message = getErrorMessage(errorState), message = getErrorMessage(errorState),
actions = if (source is LocalMangaSource) { actions = if (source is LocalMangaSource) {
listOf( persistentListOf(
EmptyScreenAction( EmptyScreenAction(
stringResId = R.string.local_source_help_guide, stringResId = R.string.local_source_help_guide,
icon = Icons.Outlined.HelpOutline, icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onLocalSourceHelpClick, onClick = onLocalSourceHelpClick,
), ),
) )
} else { } else {
listOf( persistentListOf(
EmptyScreenAction( EmptyScreenAction(
stringResId = R.string.action_retry, stringResId = R.string.action_retry,
icon = Icons.Outlined.Refresh, icon = Icons.Outlined.Refresh,
@ -97,7 +99,7 @@ fun BrowseSourceContent(
), ),
EmptyScreenAction( EmptyScreenAction(
stringResId = R.string.label_help, stringResId = R.string.label_help,
icon = Icons.Outlined.HelpOutline, icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onHelpClick, onClick = onHelpClick,
), ),
) )
@ -145,7 +147,7 @@ fun BrowseSourceContent(
} }
@Composable @Composable
fun MissingSourceScreen( internal fun MissingSourceScreen(
source: StubMangaSource, source: StubMangaSource,
navigateUp: () -> Unit, navigateUp: () -> Unit,
) { ) {

View file

@ -1,34 +1,30 @@
package eu.kanade.presentation.browse.manga package eu.kanade.presentation.browse.manga
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.browse.GlobalSearchErrorResultItem import eu.kanade.presentation.browse.GlobalSearchErrorResultItem
import eu.kanade.presentation.browse.GlobalSearchLoadingResultItem import eu.kanade.presentation.browse.GlobalSearchLoadingResultItem
import eu.kanade.presentation.browse.GlobalSearchResultItem import eu.kanade.presentation.browse.GlobalSearchResultItem
import eu.kanade.presentation.browse.GlobalSearchToolbar
import eu.kanade.presentation.browse.manga.components.GlobalMangaSearchCardRow import eu.kanade.presentation.browse.manga.components.GlobalMangaSearchCardRow
import eu.kanade.tachiyomi.R import eu.kanade.presentation.browse.manga.components.GlobalMangaSearchToolbar
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.GlobalMangaSearchState
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.MangaSearchItemResult import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.MangaSearchItemResult
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.MangaSearchScreenModel
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.MangaSourceFilter
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.presentation.core.components.LazyColumn
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
@Composable @Composable
fun GlobalMangaSearchScreen( fun GlobalMangaSearchScreen(
state: GlobalMangaSearchState, state: MangaSearchScreenModel.State,
navigateUp: () -> Unit, navigateUp: () -> Unit,
onChangeSearchQuery: (String?) -> Unit, onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit, onSearch: (String) -> Unit,
onChangeSearchFilter: (MangaSourceFilter) -> Unit,
onToggleResults: () -> Unit,
getManga: @Composable (Manga) -> State<Manga>, getManga: @Composable (Manga) -> State<Manga>,
onClickSource: (CatalogueSource) -> Unit, onClickSource: (CatalogueSource) -> Unit,
onClickItem: (Manga) -> Unit, onClickItem: (Manga) -> Unit,
@ -36,19 +32,23 @@ fun GlobalMangaSearchScreen(
) { ) {
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
GlobalSearchToolbar( GlobalMangaSearchToolbar(
searchQuery = state.searchQuery, searchQuery = state.searchQuery,
progress = state.progress, progress = state.progress,
total = state.total, total = state.total,
navigateUp = navigateUp, navigateUp = navigateUp,
onChangeSearchQuery = onChangeSearchQuery, onChangeSearchQuery = onChangeSearchQuery,
onSearch = onSearch, onSearch = onSearch,
sourceFilter = state.sourceFilter,
onChangeSearchFilter = onChangeSearchFilter,
onlyShowHasResults = state.onlyShowHasResults,
onToggleResults = onToggleResults,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) )
}, },
) { paddingValues -> ) { paddingValues ->
GlobalSearchContent( GlobalSearchContent(
items = state.items, items = state.filteredItems,
contentPadding = paddingValues, contentPadding = paddingValues,
getManga = getManga, getManga = getManga,
onClickSource = onClickSource, onClickSource = onClickSource,
@ -59,13 +59,14 @@ fun GlobalMangaSearchScreen(
} }
@Composable @Composable
private fun GlobalSearchContent( internal fun GlobalSearchContent(
items: Map<CatalogueSource, MangaSearchItemResult>, items: Map<CatalogueSource, MangaSearchItemResult>,
contentPadding: PaddingValues, contentPadding: PaddingValues,
getManga: @Composable (Manga) -> State<Manga>, getManga: @Composable (Manga) -> State<Manga>,
onClickSource: (CatalogueSource) -> Unit, onClickSource: (CatalogueSource) -> Unit,
onClickItem: (Manga) -> Unit, onClickItem: (Manga) -> Unit,
onLongClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit,
fromSourceId: Long? = null,
) { ) {
LazyColumn( LazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
@ -73,7 +74,8 @@ private fun GlobalSearchContent(
items.forEach { (source, result) -> items.forEach { (source, result) ->
item(key = source.id) { item(key = source.id) {
GlobalSearchResultItem( GlobalSearchResultItem(
title = source.name, title = fromSourceId
?.let { "${source.name}".takeIf { source.id == fromSourceId } } ?: source.name,
subtitle = LocaleHelper.getDisplayName(source.lang), subtitle = LocaleHelper.getDisplayName(source.lang),
onClick = { onClickSource(source) }, onClick = { onClickSource(source) },
) { ) {
@ -82,18 +84,6 @@ private fun GlobalSearchContent(
GlobalSearchLoadingResultItem() GlobalSearchLoadingResultItem()
} }
is MangaSearchItemResult.Success -> { is MangaSearchItemResult.Success -> {
if (result.isEmpty) {
Text(
text = stringResource(R.string.no_results_found),
modifier = Modifier
.padding(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
),
)
return@GlobalSearchResultItem
}
GlobalMangaSearchCardRow( GlobalMangaSearchCardRow(
titles = result.result, titles = result.result,
getManga = getManga, getManga = getManga,

View file

@ -10,19 +10,19 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.History import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -30,6 +30,7 @@ import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -53,11 +54,10 @@ import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaExtensionDetailsState import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaExtensionDetailsScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.DIVIDER_ALPHA
import tachiyomi.presentation.core.components.material.Divider
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
@ -65,7 +65,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
@Composable @Composable
fun ExtensionDetailsScreen( fun ExtensionDetailsScreen(
navigateUp: () -> Unit, navigateUp: () -> Unit,
state: MangaExtensionDetailsState, state: MangaExtensionDetailsScreenModel.State,
onClickSourcePreferences: (sourceId: Long) -> Unit, onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickWhatsNew: () -> Unit, onClickWhatsNew: () -> Unit,
onClickReadme: () -> Unit, onClickReadme: () -> Unit,
@ -82,40 +82,42 @@ fun ExtensionDetailsScreen(
navigateUp = navigateUp, navigateUp = navigateUp,
actions = { actions = {
AppBarActions( AppBarActions(
actions = buildList { actions = persistentListOf<AppBar.AppBarAction>().builder()
if (state.extension?.isUnofficial == false) { .apply {
add( if (state.extension?.isUnofficial == false) {
AppBar.Action( add(
title = stringResource(R.string.whats_new), AppBar.Action(
icon = Icons.Outlined.History, title = stringResource(R.string.whats_new),
onClick = onClickWhatsNew, icon = Icons.Outlined.History,
), onClick = onClickWhatsNew,
) ),
add( )
AppBar.Action( add(
title = stringResource(R.string.action_faq_and_guides), AppBar.Action(
icon = Icons.Outlined.HelpOutline, title = stringResource(R.string.action_faq_and_guides),
onClick = onClickReadme, icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onClickReadme,
),
)
}
addAll(
listOf(
AppBar.OverflowAction(
title = stringResource(R.string.action_enable_all),
onClick = onClickEnableAll,
),
AppBar.OverflowAction(
title = stringResource(R.string.action_disable_all),
onClick = onClickDisableAll,
),
AppBar.OverflowAction(
title = stringResource(R.string.pref_clear_cookies),
onClick = onClickClearCookies,
),
), ),
) )
} }
addAll( .build(),
listOf(
AppBar.OverflowAction(
title = stringResource(R.string.action_enable_all),
onClick = onClickEnableAll,
),
AppBar.OverflowAction(
title = stringResource(R.string.action_disable_all),
onClick = onClickDisableAll,
),
AppBar.OverflowAction(
title = stringResource(R.string.pref_clear_cookies),
onClick = onClickClearCookies,
),
),
)
},
) )
}, },
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
@ -176,7 +178,8 @@ private fun ExtensionDetails(
data = Uri.fromParts("package", extension.pkgName, null) data = Uri.fromParts("package", extension.pkgName, null)
context.startActivity(this) context.startActivity(this)
} }
}, Unit
}.takeIf { extension.isShared },
onClickAgeRating = { onClickAgeRating = {
showNsfwWarning = true showNsfwWarning = true
}, },
@ -188,7 +191,7 @@ private fun ExtensionDetails(
key = { it.source.id }, key = { it.source.id },
) { source -> ) { source ->
SourceSwitchPreference( SourceSwitchPreference(
modifier = Modifier.animateItemPlacement(),
source = source, source = source,
onClickSourcePreferences = onClickSourcePreferences, onClickSourcePreferences = onClickSourcePreferences,
onClickSource = onClickSource, onClickSource = onClickSource,
@ -209,7 +212,7 @@ private fun DetailsHeader(
extension: MangaExtension, extension: MangaExtension,
onClickAgeRating: () -> Unit, onClickAgeRating: () -> Unit,
onClickUninstall: () -> Unit, onClickUninstall: () -> Unit,
onClickAppInfo: () -> Unit, onClickAppInfo: (() -> Unit)?,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -293,6 +296,7 @@ private fun DetailsHeader(
top = MaterialTheme.padding.small, top = MaterialTheme.padding.small,
bottom = MaterialTheme.padding.medium, bottom = MaterialTheme.padding.medium,
), ),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
OutlinedButton( OutlinedButton(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
@ -301,29 +305,29 @@ private fun DetailsHeader(
Text(stringResource(R.string.ext_uninstall)) Text(stringResource(R.string.ext_uninstall))
} }
Spacer(Modifier.width(16.dp)) if (onClickAppInfo != null) {
Button(
Button( modifier = Modifier.weight(1f),
modifier = Modifier.weight(1f), onClick = onClickAppInfo,
onClick = onClickAppInfo, ) {
) { Text(
Text( text = stringResource(R.string.ext_app_info),
text = stringResource(R.string.ext_app_info), color = MaterialTheme.colorScheme.onPrimary,
color = MaterialTheme.colorScheme.onPrimary, )
) }
} }
} }
Divider() HorizontalDivider()
} }
} }
@Composable @Composable
private fun InfoText( private fun InfoText(
modifier: Modifier,
primaryText: String, primaryText: String,
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
secondaryText: String, secondaryText: String,
modifier: Modifier = Modifier,
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
) { ) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
@ -356,20 +360,17 @@ private fun InfoText(
@Composable @Composable
private fun InfoDivider() { private fun InfoDivider() {
Divider( VerticalDivider(
modifier = Modifier modifier = Modifier.height(20.dp),
.height(20.dp)
.width(1.dp),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = DIVIDER_ALPHA),
) )
} }
@Composable @Composable
private fun SourceSwitchPreference( private fun SourceSwitchPreference(
modifier: Modifier = Modifier,
source: MangaExtensionSourceItem, source: MangaExtensionSourceItem,
onClickSourcePreferences: (sourceId: Long) -> Unit, onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickSource: (sourceId: Long) -> Unit, onClickSource: (sourceId: Long) -> Unit,
modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -415,7 +416,7 @@ fun NsfwWarningDialog(
}, },
confirmButton = { confirmButton = {
TextButton(onClick = onClickConfirm) { TextButton(onClick = onClickConfirm) {
Text(text = stringResource(android.R.string.ok)) Text(text = stringResource(R.string.action_ok))
} }
}, },
onDismissRequest = onClickConfirm, onDismissRequest = onClickConfirm,

View file

@ -2,6 +2,7 @@ package eu.kanade.presentation.browse.manga
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -12,7 +13,6 @@ import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.manga.extension.MangaExtensionFilterState import eu.kanade.tachiyomi.ui.browse.manga.extension.MangaExtensionFilterState
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
@ -53,12 +53,12 @@ private fun ExtensionFilterContent(
onClickLang: (String) -> Unit, onClickLang: (String) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
FastScrollLazyColumn( LazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
) { ) {
items(state.languages) { language -> items(state.languages) { language ->
SwitchPreferenceWidget( SwitchPreferenceWidget(
modifier = Modifier.animateItemPlacement(),
title = LocaleHelper.getSourceDisplayName(language, context), title = LocaleHelper.getSourceDisplayName(language, context),
checked = language in state.enabledLanguages, checked = language in state.enabledLanguages,
onCheckedChanged = { onClickLang(language) }, onCheckedChanged = { onClickLang(language) },

View file

@ -43,7 +43,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.InstallStep import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.ui.browse.manga.extension.MangaExtensionUiModel import eu.kanade.tachiyomi.ui.browse.manga.extension.MangaExtensionUiModel
import eu.kanade.tachiyomi.ui.browse.manga.extension.MangaExtensionsState import eu.kanade.tachiyomi.ui.browse.manga.extension.MangaExtensionsScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.PullRefresh import tachiyomi.presentation.core.components.material.PullRefresh
@ -57,7 +57,7 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable @Composable
fun MangaExtensionScreen( fun MangaExtensionScreen(
state: MangaExtensionsState, state: MangaExtensionsScreenModel.State,
contentPadding: PaddingValues, contentPadding: PaddingValues,
searchQuery: String?, searchQuery: String?,
onLongClickItem: (MangaExtension) -> Unit, onLongClickItem: (MangaExtension) -> Unit,
@ -73,10 +73,10 @@ fun MangaExtensionScreen(
PullRefresh( PullRefresh(
refreshing = state.isRefreshing, refreshing = state.isRefreshing,
onRefresh = onRefresh, onRefresh = onRefresh,
enabled = !state.isLoading, enabled = { !state.isLoading },
) { ) {
when { when {
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
state.isEmpty -> { state.isEmpty -> {
val msg = if (!searchQuery.isNullOrEmpty()) { val msg = if (!searchQuery.isNullOrEmpty()) {
R.string.no_results_found R.string.no_results_found
@ -108,7 +108,7 @@ fun MangaExtensionScreen(
@Composable @Composable
private fun ExtensionContent( private fun ExtensionContent(
state: MangaExtensionsState, state: MangaExtensionsScreenModel.State,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onLongClickItem: (MangaExtension) -> Unit, onLongClickItem: (MangaExtension) -> Unit,
onClickItemCancel: (MangaExtension) -> Unit, onClickItemCancel: (MangaExtension) -> Unit,
@ -148,14 +148,14 @@ private fun ExtensionContent(
} }
ExtensionHeader( ExtensionHeader(
textRes = header.textRes, textRes = header.textRes,
modifier = Modifier.animateItemPlacement(),
action = action, action = action,
) )
} }
is MangaExtensionUiModel.Header.Text -> { is MangaExtensionUiModel.Header.Text -> {
ExtensionHeader( ExtensionHeader(
text = header.text, text = header.text,
modifier = Modifier.animateItemPlacement(),
) )
} }
} }
@ -167,7 +167,7 @@ private fun ExtensionContent(
key = { "extension-${it.hashCode()}" }, key = { "extension-${it.hashCode()}" },
) { item -> ) { item ->
ExtensionItem( ExtensionItem(
modifier = Modifier.animateItemPlacement(),
item = item, item = item,
onClickItem = { onClickItem = {
when (it) { when (it) {
@ -217,12 +217,12 @@ private fun ExtensionContent(
@Composable @Composable
private fun ExtensionItem( private fun ExtensionItem(
modifier: Modifier = Modifier,
item: MangaExtensionUiModel.Item, item: MangaExtensionUiModel.Item,
onClickItem: (MangaExtension) -> Unit, onClickItem: (MangaExtension) -> Unit,
onLongClickItem: (MangaExtension) -> Unit, onLongClickItem: (MangaExtension) -> Unit,
onClickItemCancel: (MangaExtension) -> Unit, onClickItemCancel: (MangaExtension) -> Unit,
onClickItemAction: (MangaExtension) -> Unit, onClickItemAction: (MangaExtension) -> Unit,
modifier: Modifier = Modifier,
) { ) {
val (extension, installStep) = item val (extension, installStep) = item
BaseBrowseItem( BaseBrowseItem(
@ -247,7 +247,10 @@ private fun ExtensionItem(
) )
} }
val padding by animateDpAsState(targetValue = if (idle) 0.dp else 8.dp) val padding by animateDpAsState(
targetValue = if (idle) 0.dp else 8.dp,
label = "iconPadding",
)
MangaExtensionIcon( MangaExtensionIcon(
extension = extension, extension = extension,
modifier = Modifier modifier = Modifier
@ -296,7 +299,10 @@ private fun ExtensionItemContent(
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) { ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
if (extension is MangaExtension.Installed && extension.lang.isNotEmpty()) { if (extension is MangaExtension.Installed && extension.lang.isNotEmpty()) {
Text( Text(
text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current), text = LocaleHelper.getSourceDisplayName(
extension.lang,
LocalContext.current,
),
) )
} }

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