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,4 +1,5 @@
[*.{kt,kts}]
max_line_length = 120
indent_size = 4
insert_final_newline = true
ij_kotlin_allow_trailing_comma = true

View file

@ -5,7 +5,7 @@ I acknowledge that:
- I have updated:
- To the latest version of the app (stable is v0.12.3.10)
- 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
- 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

View file

@ -4,7 +4,7 @@ contact_links:
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
- name: 📦 Aniyomi extensions
url: https://aniyomi.org/extensions
url: https://aniyomi.org/extensions/
about: Anime extensions and sources
- name: 🧑‍💻 Aniyomi help discord
url: https://discord.gg/F32UjdJZrR

View file

@ -95,7 +95,7 @@ body:
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).
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
- label: I have updated the app to version **[0.12.3.10](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
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:
- name: Clone repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
@ -37,4 +37,4 @@ jobs:
- name: Build app and run unit tests
uses: gradle/gradle-command-action@v2
with:
arguments: lintKotlin assembleStandardRelease testStandardReleaseUnitTest
arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest

View file

@ -22,7 +22,7 @@ jobs:
steps:
- name: Clone repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
@ -46,7 +46,7 @@ jobs:
- name: Build app and run unit tests
uses: gradle/gradle-command-action@v2
with:
arguments: lintKotlin assembleStandardRelease testStandardReleaseUnitTest
arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest
# Sign APK and create release for tags
@ -120,3 +120,14 @@ jobs:
prerelease: false
env:
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",
"regex": ".*\\* (Aniyomi version|Android version|Device): \\?.*",
"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

View file

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

3
.gitignore vendored
View file

@ -3,7 +3,8 @@
/acra.properties
/.idea/workspace.xml
.DS_Store
.idea/
.idea/*
!.idea/icon.png
*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)
- Emulator or phone with developer options enabled to test changes.
## Linting
To auto-fix some linting errors, run the `ktlintFormat` Gradle task.
## Getting help
- Join [the Discord server](https://discord.gg/F32UjdJZrR) for online help and to ask questions while developing.
# 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

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>
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)
</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)
* 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 screenshot (if needed)
* If it could be device-dependent, try reproducing on another device (if possible)

1
app/.gitignore vendored
View file

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

View file

@ -11,10 +11,11 @@
-keep,allowoptimization class kotlin.** { public protected *; }
-keep,allowoptimization class kotlinx.coroutines.** { 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 okio.** { public protected *; }
-keep,allowoptimization class rx.** { 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 uy.kohesive.injekt.** { public protected *; }
-keep,allowoptimization class is.xyz.mpv.** { public protected *; }

View file

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

View file

@ -29,21 +29,17 @@
<application
android:name=".App"
android:allowBackup="false"
android:enableOnBackInvokedCallback="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.Tachiyomi"
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config">
<!-- enable profiling by macrobenchmark -->
<profileable
android:shell="true"
tools:targetApi="q" />
android:theme="@style/Theme.Tachiyomi">
<activity
android:name=".ui.main.MainActivity"
@ -67,10 +63,10 @@
<activity
android:name=".ui.main.DeepLinkAnimeActivity"
android:name=".ui.deeplink.anime.DeepLinkAnimeActivity"
android:launchMode="singleTask"
android:theme="@android:style/Theme.NoDisplay"
android:label="@string/action_global_anime_search"
android:label="@string/action_search"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
@ -94,10 +90,10 @@
</activity>
<activity
android:name=".ui.main.DeepLinkMangaActivity"
android:name=".ui.deeplink.manga.DeepLinkMangaActivity"
android:launchMode="singleTask"
android:theme="@android:style/Theme.NoDisplay"
android:label="@string/action_global_manga_search"
android:label="@string/action_search"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
@ -172,8 +168,8 @@
android:exported="false" />
<activity
android:name=".ui.setting.track.AnilistLoginActivity"
android:label="Anilist"
android:name=".ui.setting.track.TrackLoginActivity"
android:label="@string/track_activity_name"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@ -181,69 +177,21 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="anilist-auth"
android:scheme="tachiyomi" />
<data android:host="anilist-auth"/>
<data android:host="bangumi-auth"/>
<data android:host="myanimelist-auth"/>
<data android:host="shikimori-auth"/>
<data android:scheme="tachiyomi"/>
</intent-filter>
</activity>
<activity
android:name=".ui.setting.track.MyAnimeListLoginActivity"
android:label="MyAnimeList"
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="myanimelist-auth"
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" />
<data android:host="simkl-auth"/>
<data android:scheme="aniyomi"/>
</intent-filter>
</activity>
@ -251,34 +199,6 @@
android:name=".data.notification.NotificationReceiver"
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
android:name=".data.download.manga.MangaDownloadService"
android:exported="false" />
@ -288,13 +208,11 @@
android:exported="false" />
<service
android:name=".data.updater.AppUpdateService"
android:name=".extension.manga.util.MangaExtensionInstallService"
android:exported="false" />
<service android:name=".extension.manga.util.MangaExtensionInstallService"
android:exported="false" />
<service android:name=".extension.anime.util.AnimeExtensionInstallService"
<service
android:name=".extension.anime.util.AnimeExtensionInstallService"
android:exported="false" />
<service
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 okhttp3.OkHttpClient
import okhttp3.Response
import rx.Observable
import tachiyomi.core.preference.Preference
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 {
val imageUrl = page.imageUrl ?: return getImage(page)
page.imageUrl = dataSaver.compress(imageUrl)
@ -74,7 +64,13 @@ private class BandwidthHeroDataSaver(preferences: SourcePreferences) : DataSaver
override fun compress(imageUrl: String): String {
return if (dataSavedServer.isNotBlank() && !imageUrl.contains(dataSavedServer)) {
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)
else -> getUrl(imageUrl)
}
@ -100,7 +96,13 @@ private class WsrvNlDataSaver(preferences: SourcePreferences) : DataSaver {
override fun compress(imageUrl: String): String {
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)
else -> getUrl(imageUrl)
}
@ -108,7 +110,11 @@ private class WsrvNlDataSaver(preferences: SourcePreferences) : DataSaver {
private fun getUrl(imageUrl: String): String {
// 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) {
// Preserve output image extension for animated images(.webp and .gif)
"&q=$quality&n=-1"
@ -140,7 +146,13 @@ private class ReSmushItDataSaver(preferences: SourcePreferences) : DataSaver {
override fun compress(imageUrl: String): String {
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)
else -> getUrl(imageUrl)
}

View file

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

View file

@ -1,7 +1,6 @@
package eu.kanade.core.util
import androidx.compose.ui.util.fastForEach
import java.util.concurrent.ConcurrentHashMap
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@ -20,15 +19,6 @@ fun <T : R, R : Any> List<T>.insertSeparators(
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) {
if (shouldAdd) {
add(value)

View file

@ -1,9 +1,11 @@
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.entries.anime.interactor.SetAnimeViewerFlags
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.UpdateManga
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.GetMangaExtensionLanguages
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.SyncChaptersWithSource
import eu.kanade.domain.items.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
import eu.kanade.domain.items.episode.interactor.SetSeenStatus
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.GetEnabledAnimeSources
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.service.SetMigrateSorting
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.manga.MangaCategoryRepositoryImpl
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.items.chapter.ChapterRepositoryImpl
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.manga.MangaSourceDataRepositoryImpl
import tachiyomi.data.source.anime.AnimeStubSourceRepositoryImpl
import tachiyomi.data.source.manga.MangaSourceRepositoryImpl
import tachiyomi.data.source.manga.MangaStubSourceRepositoryImpl
import tachiyomi.data.track.anime.AnimeTrackRepositoryImpl
import tachiyomi.data.track.manga.MangaTrackRepositoryImpl
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.ResetAnimeCategoryFlags
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.UpdateAnimeCategory
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.ReorderMangaCategory
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.SetMangaDisplayMode
import tachiyomi.domain.category.manga.interactor.SetSortModeForMangaCategory
import tachiyomi.domain.category.manga.interactor.UpdateMangaCategory
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.GetAnimeFavorites
import tachiyomi.domain.entries.anime.interactor.GetAnimeWithEpisodes
import tachiyomi.domain.entries.anime.interactor.GetDuplicateLibraryAnime
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.ResetAnimeViewerFlags
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
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.GetLibraryManga
import tachiyomi.domain.entries.manga.interactor.GetManga
import tachiyomi.domain.entries.manga.interactor.GetMangaFavorites
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.ResetMangaViewerFlags
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.repository.MangaHistoryRepository
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.ShouldUpdateDbChapter
import tachiyomi.domain.items.chapter.interactor.UpdateChapter
import tachiyomi.domain.items.chapter.repository.ChapterRepository
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.ShouldUpdateDbEpisode
import tachiyomi.domain.items.episode.interactor.UpdateEpisode
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.GetRemoteAnime
import tachiyomi.domain.source.anime.repository.AnimeSourceDataRepository
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.GetRemoteManga
import tachiyomi.domain.source.manga.repository.MangaSourceDataRepository
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.GetAnimeTracks
import tachiyomi.domain.track.anime.interactor.GetTracksPerAnime
@ -148,7 +166,7 @@ class DomainModule : InjektModule {
addFactory { GetAnimeCategories(get()) }
addFactory { GetVisibleAnimeCategories(get()) }
addFactory { ResetAnimeCategoryFlags(get(), get()) }
addFactory { SetDisplayModeForAnimeCategory(get(), get()) }
addFactory { SetAnimeDisplayMode(get()) }
addFactory { SetSortModeForAnimeCategory(get(), get()) }
addFactory { CreateAnimeCategoryWithName(get(), get()) }
addFactory { RenameAnimeCategory(get()) }
@ -161,7 +179,7 @@ class DomainModule : InjektModule {
addFactory { GetMangaCategories(get()) }
addFactory { GetVisibleMangaCategories(get()) }
addFactory { ResetMangaCategoryFlags(get(), get()) }
addFactory { SetDisplayModeForMangaCategory(get(), get()) }
addFactory { SetMangaDisplayMode(get()) }
addFactory { SetSortModeForMangaCategory(get(), get()) }
addFactory { CreateMangaCategoryWithName(get(), get()) }
addFactory { RenameMangaCategory(get()) }
@ -175,14 +193,16 @@ class DomainModule : InjektModule {
addFactory { GetAnimeFavorites(get()) }
addFactory { GetLibraryAnime(get()) }
addFactory { GetAnimeWithEpisodes(get(), get()) }
addFactory { GetAnimeByUrlAndSourceId(get()) }
addFactory { GetAnime(get()) }
addFactory { GetNextEpisodes(get(), get(), get()) }
addFactory { ResetAnimeViewerFlags(get()) }
addFactory { SetAnimeEpisodeFlags(get()) }
addFactory { AnimeFetchInterval(get()) }
addFactory { SetAnimeDefaultEpisodeFlags(get(), get(), get()) }
addFactory { SetAnimeViewerFlags(get()) }
addFactory { NetworkToLocalAnime(get()) }
addFactory { UpdateAnime(get()) }
addFactory { UpdateAnime(get(), get()) }
addFactory { SetAnimeCategories(get()) }
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
@ -190,10 +210,12 @@ class DomainModule : InjektModule {
addFactory { GetMangaFavorites(get()) }
addFactory { GetLibraryManga(get()) }
addFactory { GetMangaWithChapters(get(), get()) }
addFactory { GetMangaByUrlAndSourceId(get()) }
addFactory { GetManga(get()) }
addFactory { GetNextChapters(get(), get(), get()) }
addFactory { ResetMangaViewerFlags(get()) }
addFactory { SetMangaChapterFlags(get()) }
addFactory { MangaFetchInterval(get()) }
addFactory {
SetMangaDefaultChapterFlags(
get(),
@ -203,45 +225,59 @@ class DomainModule : InjektModule {
}
addFactory { SetMangaViewerFlags(get()) }
addFactory { NetworkToLocalManga(get()) }
addFactory { UpdateManga(get()) }
addFactory { UpdateManga(get(), get()) }
addFactory { SetMangaCategories(get()) }
addFactory { GetExcludedScanlators(get()) }
addFactory { SetExcludedScanlators(get()) }
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
addFactory { GetApplicationRelease(get(), 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 { GetTracksPerAnime(get()) }
addFactory { GetAnimeTracks(get()) }
addFactory { InsertAnimeTrack(get()) }
addFactory { SyncEpisodeProgressWithTrack(get(), get(), 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 { GetTracksPerManga(get()) }
addFactory { GetMangaTracks(get()) }
addFactory { InsertMangaTrack(get()) }
addFactory { SyncChapterProgressWithTrack(get(), get(), get()) }
addSingletonFactory<EpisodeRepository> { EpisodeRepositoryImpl(get()) }
addFactory { GetEpisode(get()) }
addFactory { GetEpisodeByAnimeId(get()) }
addFactory { GetEpisodesByAnimeId(get()) }
addFactory { GetEpisodeByUrlAndAnimeId(get()) }
addFactory { UpdateEpisode(get()) }
addFactory { SetSeenStatus(get(), get(), get(), get()) }
addFactory { ShouldUpdateDbEpisode() }
addFactory { SyncEpisodesWithSource(get(), get(), get(), get()) }
addFactory { SyncEpisodesWithTrackServiceTwoWay(get(), get()) }
addFactory { SyncEpisodesWithSource(get(), get(), get(), get(), get(), get(), get()) }
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
addFactory { GetChapter(get()) }
addFactory { GetChapterByMangaId(get()) }
addFactory { GetChaptersByMangaId(get()) }
addFactory { GetChapterByUrlAndMangaId(get()) }
addFactory { UpdateChapter(get()) }
addFactory { SetReadStatus(get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() }
addFactory { SyncChaptersWithSource(get(), get(), get(), get()) }
addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get()) }
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) }
addFactory { GetAvailableScanlators(get()) }
addSingletonFactory<AnimeHistoryRepository> { AnimeHistoryRepositoryImpl(get()) }
addFactory { GetAnimeHistory(get()) }
addFactory { UpsertAnimeHistory(get()) }
addFactory { RemoveAnimeHistory(get()) }
addFactory { DeleteAnimeDownload(get(), get()) }
addFactory { DeleteEpisodeDownload(get(), get()) }
addFactory { GetAnimeExtensionsByType(get(), get()) }
addFactory { GetAnimeExtensionSources(get()) }
@ -266,7 +302,7 @@ class DomainModule : InjektModule {
addFactory { GetMangaUpdates(get()) }
addSingletonFactory<AnimeSourceRepository> { AnimeSourceRepositoryImpl(get(), get()) }
addSingletonFactory<AnimeSourceDataRepository> { AnimeSourceDataRepositoryImpl(get()) }
addSingletonFactory<AnimeStubSourceRepository> { AnimeStubSourceRepositoryImpl(get()) }
addFactory { GetEnabledAnimeSources(get(), get()) }
addFactory { GetLanguagesWithAnimeSources(get(), get()) }
addFactory { GetRemoteAnime(get()) }
@ -276,7 +312,7 @@ class DomainModule : InjektModule {
addFactory { ToggleAnimeSourcePin(get()) }
addSingletonFactory<MangaSourceRepository> { MangaSourceRepositoryImpl(get(), get()) }
addSingletonFactory<MangaSourceDataRepository> { MangaSourceDataRepositoryImpl(get()) }
addSingletonFactory<MangaStubSourceRepository> { MangaStubSourceRepositoryImpl(get()) }
addFactory { GetEnabledMangaSources(get(), get()) }
addFactory { GetLanguagesWithMangaSources(get(), get()) }
addFactory { GetRemoteManga(get()) }

View file

@ -3,9 +3,11 @@ package eu.kanade.domain.base
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
class BasePreferences(
@ -13,21 +15,28 @@ class BasePreferences(
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("incognito_mode", false)
fun incognitoMode() = preferenceStore.getBoolean(Preference.appStateKey("incognito_mode"), false)
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),
PACKAGEINSTALLER(R.string.ext_installer_packageinstaller),
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"
val entries get() = ExtensionInstaller.values().run {
val entries get() = ExtensionInstaller.entries.run {
if (context.hasMiuiPackageInstaller) {
filter { it != ExtensionInstaller.PACKAGEINSTALLER }
} else {

View file

@ -6,7 +6,7 @@ import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.items.episode.model.Episode
import tachiyomi.domain.source.anime.service.AnimeSourceManager
class DeleteAnimeDownload(
class DeleteEpisodeDownload(
private val sourceManager: AnimeSourceManager,
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.tachiyomi.animesource.model.SAnime
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.AnimeUpdate
import tachiyomi.domain.entries.anime.repository.AnimeRepository
import tachiyomi.source.local.entries.anime.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.ZonedDateTime
import java.util.Date
class UpdateAnime(
private val animeRepository: AnimeRepository,
private val animeFetchInterval: AnimeFetchInterval,
) {
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 {
return animeRepository.updateAnime(AnimeUpdate(id = animeId, lastUpdate = Date().time))
}
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 {

View file

@ -3,24 +3,24 @@ package eu.kanade.domain.entries.anime.model
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.animesource.model.SAnime
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 uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
val Anime.downloadedFilter: TriStateFilter
val Anime.downloadedFilter: TriState
get() {
if (forceDownloaded()) return TriStateFilter.ENABLED_IS
if (forceDownloaded()) return TriState.ENABLED_IS
return when (downloadedFilterRaw) {
Anime.EPISODE_SHOW_DOWNLOADED -> TriStateFilter.ENABLED_IS
Anime.EPISODE_SHOW_NOT_DOWNLOADED -> TriStateFilter.ENABLED_NOT
else -> TriStateFilter.DISABLED
Anime.EPISODE_SHOW_DOWNLOADED -> TriState.ENABLED_IS
Anime.EPISODE_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
else -> TriState.DISABLED
}
}
fun Anime.episodesFiltered(): Boolean {
return unseenFilter != TriStateFilter.DISABLED ||
downloadedFilter != TriStateFilter.DISABLED ||
bookmarkedFilter != TriStateFilter.DISABLED
return unseenFilter != TriState.DISABLED ||
downloadedFilter != TriState.DISABLED ||
bookmarkedFilter != TriState.DISABLED
}
fun Anime.forceDownloaded(): Boolean {
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
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import tachiyomi.domain.entries.manga.model.MangaUpdate
import tachiyomi.domain.entries.manga.repository.MangaRepository
@ -9,22 +9,22 @@ class SetMangaViewerFlags(
private val mangaRepository: MangaRepository,
) {
suspend fun awaitSetMangaReadingMode(id: Long, flag: Long) {
suspend fun awaitSetReadingMode(id: Long, flag: Long) {
val manga = mangaRepository.getMangaById(id)
mangaRepository.updateManga(
MangaUpdate(
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)
mangaRepository.updateManga(
MangaUpdate(
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.tachiyomi.data.cache.MangaCoverCache
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.MangaUpdate
import tachiyomi.domain.entries.manga.repository.MangaRepository
import tachiyomi.source.local.entries.manga.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.ZonedDateTime
import java.util.Date
class UpdateManga(
private val mangaRepository: MangaRepository,
private val mangaFetchInterval: MangaFetchInterval,
) {
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 {
return mangaRepository.updateManga(MangaUpdate(id = mangaId, lastUpdate = Date().time))
}
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 {

View file

@ -3,36 +3,36 @@ package eu.kanade.domain.entries.manga.model
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.cache.MangaCoverCache
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import tachiyomi.core.metadata.comicinfo.ComicInfo
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.items.chapter.model.Chapter
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
// TODO: move these into the domain model
val Manga.readingModeType: Long
get() = viewerFlags and ReadingModeType.MASK.toLong()
val Manga.readingMode: Long
get() = viewerFlags and ReadingMode.MASK.toLong()
val Manga.orientationType: Long
get() = viewerFlags and OrientationType.MASK.toLong()
val Manga.readerOrientation: Long
get() = viewerFlags and ReaderOrientation.MASK.toLong()
val Manga.downloadedFilter: TriStateFilter
val Manga.downloadedFilter: TriState
get() {
if (forceDownloaded()) return TriStateFilter.ENABLED_IS
if (forceDownloaded()) return TriState.ENABLED_IS
return when (downloadedFilterRaw) {
Manga.CHAPTER_SHOW_DOWNLOADED -> TriStateFilter.ENABLED_IS
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriStateFilter.ENABLED_NOT
else -> TriStateFilter.DISABLED
Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
else -> TriState.DISABLED
}
}
fun Manga.chaptersFiltered(): Boolean {
return unreadFilter != TriStateFilter.DISABLED ||
downloadedFilter != TriStateFilter.DISABLED ||
bookmarkedFilter != TriStateFilter.DISABLED
return unreadFilter != TriState.DISABLED ||
downloadedFilter != TriState.DISABLED ||
bookmarkedFilter != TriState.DISABLED
}
fun Manga.forceDownloaded(): Boolean {
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.
*/
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),
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),
summary = manga.description?.let { ComicInfo.Summary(it) },
writer = manga.author?.let { ComicInfo.Writer(it) },
@ -107,6 +114,7 @@ fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String) = ComicInfo
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
),
categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) },
inker = null,
colorist = 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) =
await(manga.id, read)
sealed class Result {
object Success : Result()
object NoChapters : Result()
data class InternalError(val error: Throwable) : Result()
sealed interface Result {
data object Success : Result
data object NoChapters : Result
data class InternalError(val error: Throwable) : Result
}
}

View file

@ -1,5 +1,6 @@
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.model.toSManga
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 tachiyomi.data.items.chapter.ChapterSanitizer
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.UpdateChapter
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.service.ChapterRecognition
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.time.ZonedDateTime
import java.util.Date
import java.util.TreeSet
class SyncChaptersWithSource(
private val downloadManager: MangaDownloadManager = Injekt.get(),
private val downloadProvider: MangaDownloadProvider = Injekt.get(),
private val chapterRepository: ChapterRepository = Injekt.get(),
private val shouldUpdateDbChapter: ShouldUpdateDbChapter = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
private val updateChapter: UpdateChapter = Injekt.get(),
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
private val downloadManager: MangaDownloadManager,
private val downloadProvider: MangaDownloadProvider,
private val chapterRepository: ChapterRepository,
private val shouldUpdateDbChapter: ShouldUpdateDbChapter,
private val updateManga: UpdateManga,
private val updateChapter: UpdateChapter,
private val getChaptersByMangaId: GetChaptersByMangaId,
private val getExcludedScanlators: GetExcludedScanlators,
) {
/**
@ -48,11 +49,15 @@ class SyncChaptersWithSource(
rawSourceChapters: List<SChapter>,
manga: Manga,
source: MangaSource,
manualFetch: Boolean = false,
fetchWindow: Pair<Long, Long> = Pair(0, 0),
): List<Chapter> {
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
throw NoChaptersException()
}
val now = ZonedDateTime.now()
val sourceChapters = rawSourceChapters
.distinctBy { it.url }
.mapIndexed { i, sChapter ->
@ -63,7 +68,7 @@ class SyncChaptersWithSource(
}
// Chapters from db.
val dbChapters = getChapterByMangaId.await(manga.id)
val dbChapters = getChaptersByMangaId.await(manga.id)
// Chapters from the source not in db.
val toAdd = mutableListOf<Chapter>()
@ -96,7 +101,11 @@ class SyncChaptersWithSource(
}
// 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)
val dbChapter = dbChapters.find { it.url == chapter.url }
@ -112,8 +121,16 @@ class SyncChaptersWithSource(
toAdd.add(toAddChapter)
} else {
if (shouldUpdateDbChapter.await(dbChapter, chapter)) {
val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(dbChapter, chapter) &&
downloadManager.isChapterDownloaded(dbChapter.name, dbChapter.scanlator, manga.title, manga.source)
val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(
dbChapter,
chapter,
) &&
downloadManager.isChapterDownloaded(
dbChapter.name,
dbChapter.scanlator,
manga.title,
manga.source,
)
if (shouldRenameChapter) {
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.
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
updateManga.awaitUpdateFetchInterval(
manga,
now,
fetchWindow,
)
}
return emptyList()
}
val reAdded = mutableListOf<Chapter>()
val deletedChapterNumbers = TreeSet<Float>()
val deletedReadChapterNumbers = TreeSet<Float>()
val deletedBookmarkedChapterNumbers = TreeSet<Float>()
val deletedChapterNumbers = TreeSet<Double>()
val deletedReadChapterNumbers = TreeSet<Double>()
val deletedBookmarkedChapterNumbers = TreeSet<Double>()
toDelete.forEach { chapter ->
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
@ -188,6 +212,7 @@ class SyncChaptersWithSource(
val chapterUpdates = toChange.map { it.toChapterUpdate() }
updateChapter.awaitAll(chapterUpdates)
}
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
// Set this manga as updated since chapters were changed
// 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()
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
import data.Chapters
import eu.kanade.tachiyomi.data.database.models.manga.ChapterImpl
import eu.kanade.tachiyomi.source.model.SChapter
import tachiyomi.domain.items.chapter.model.Chapter
@ -12,7 +11,7 @@ fun Chapter.toSChapter(): SChapter {
it.url = url
it.name = name
it.date_upload = dateUpload
it.chapter_number = chapterNumber
it.chapter_number = chapterNumber.toFloat()
it.scanlator = scanlator
}
}
@ -22,18 +21,8 @@ fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter {
name = sChapter.name,
url = sChapter.url,
dateUpload = sChapter.date_upload,
chapterNumber = sChapter.chapter_number,
scanlator = sChapter.scanlator?.ifBlank { null },
)
}
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 },
chapterNumber = sChapter.chapter_number.toDouble(),
scanlator = sChapter.scanlator?.ifBlank { null }?.trim(),
)
}
@ -48,6 +37,6 @@ fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also {
it.last_page_read = lastPageRead.toInt()
it.date_fetch = dateFetch
it.date_upload = dateUpload
it.chapter_number = chapterNumber
it.chapter_number = chapterNumber.toFloat()
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.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.manga.model.Manga
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(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
}
}
@ -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.
* @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 unreadFilter = manga.unreadFilter
val downloadedFilter = manga.downloadedFilter

View file

@ -1,6 +1,6 @@
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 tachiyomi.core.util.lang.withNonCancellableContext
import tachiyomi.core.util.system.logcat
@ -13,7 +13,7 @@ import tachiyomi.domain.items.episode.repository.EpisodeRepository
class SetSeenStatus(
private val downloadPreferences: DownloadPreferences,
private val deleteDownload: DeleteAnimeDownload,
private val deleteDownload: DeleteEpisodeDownload,
private val animeRepository: AnimeRepository,
private val episodeRepository: EpisodeRepository,
) {
@ -72,9 +72,9 @@ class SetSeenStatus(
suspend fun await(anime: Anime, seen: Boolean) =
await(anime.id, seen)
sealed class Result {
object Success : Result()
object NoEpisodes : Result()
data class InternalError(val error: Throwable) : Result()
sealed interface Result {
data object Success : Result
data object NoEpisodes : 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 tachiyomi.data.items.episode.EpisodeSanitizer
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.UpdateEpisode
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.service.EpisodeRecognition
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.time.ZonedDateTime
import java.util.Date
import java.util.TreeSet
class SyncEpisodesWithSource(
private val downloadManager: AnimeDownloadManager = Injekt.get(),
private val downloadProvider: AnimeDownloadProvider = Injekt.get(),
private val episodeRepository: EpisodeRepository = Injekt.get(),
private val shouldUpdateDbEpisode: ShouldUpdateDbEpisode = Injekt.get(),
private val updateAnime: UpdateAnime = Injekt.get(),
private val updateEpisode: UpdateEpisode = Injekt.get(),
private val getEpisodeByAnimeId: GetEpisodeByAnimeId = Injekt.get(),
private val downloadManager: AnimeDownloadManager,
private val downloadProvider: AnimeDownloadProvider,
private val episodeRepository: EpisodeRepository,
private val shouldUpdateDbEpisode: ShouldUpdateDbEpisode,
private val updateAnime: UpdateAnime,
private val updateEpisode: UpdateEpisode,
private val getEpisodesByAnimeId: GetEpisodesByAnimeId,
) {
/**
@ -48,11 +47,15 @@ class SyncEpisodesWithSource(
rawSourceEpisodes: List<SEpisode>,
anime: Anime,
source: AnimeSource,
manualFetch: Boolean = false,
fetchWindow: Pair<Long, Long> = Pair(0, 0),
): List<Episode> {
if (rawSourceEpisodes.isEmpty() && !source.isLocal()) {
throw NoEpisodesException()
}
val now = ZonedDateTime.now()
val sourceEpisodes = rawSourceEpisodes
.distinctBy { it.url }
.mapIndexed { i, sEpisode ->
@ -63,7 +66,7 @@ class SyncEpisodesWithSource(
}
// Episodes from db.
val dbEpisodes = getEpisodeByAnimeId.await(anime.id)
val dbEpisodes = getEpisodesByAnimeId.await(anime.id)
// Episodes from the source not in db.
val toAdd = mutableListOf<Episode>()
@ -96,7 +99,11 @@ class SyncEpisodesWithSource(
}
// 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)
val dbEpisode = dbEpisodes.find { it.url == episode.url }
@ -112,8 +119,16 @@ class SyncEpisodesWithSource(
toAdd.add(toAddEpisode)
} else {
if (shouldUpdateDbEpisode.await(dbEpisode, episode)) {
val shouldRenameEpisode = downloadProvider.isEpisodeDirNameChanged(dbEpisode, episode) &&
downloadManager.isEpisodeDownloaded(dbEpisode.name, dbEpisode.scanlator, anime.title, anime.source)
val shouldRenameEpisode = downloadProvider.isEpisodeDirNameChanged(
dbEpisode,
episode,
) &&
downloadManager.isEpisodeDownloaded(
dbEpisode.name,
dbEpisode.scanlator,
anime.title,
anime.source,
)
if (shouldRenameEpisode) {
downloadManager.renameEpisode(source, anime, dbEpisode, episode)
@ -125,7 +140,9 @@ class SyncEpisodesWithSource(
sourceOrder = episode.sourceOrder,
)
if (episode.dateUpload != 0L) {
toChangeEpisode = toChangeEpisode.copy(dateUpload = sourceEpisode.dateUpload)
toChangeEpisode = toChangeEpisode.copy(
dateUpload = sourceEpisode.dateUpload,
)
}
toChange.add(toChangeEpisode)
}
@ -134,14 +151,21 @@ class SyncEpisodesWithSource(
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
if (manualFetch || anime.fetchInterval == 0 || anime.nextUpdate < fetchWindow.first) {
updateAnime.awaitUpdateFetchInterval(
anime,
now,
fetchWindow,
)
}
return emptyList()
}
val reAdded = mutableListOf<Episode>()
val deletedEpisodeNumbers = TreeSet<Float>()
val deletedSeenEpisodeNumbers = TreeSet<Float>()
val deletedBookmarkedEpisodeNumbers = TreeSet<Float>()
val deletedEpisodeNumbers = TreeSet<Double>()
val deletedSeenEpisodeNumbers = TreeSet<Double>()
val deletedBookmarkedEpisodeNumbers = TreeSet<Double>()
toDelete.forEach { episode ->
if (episode.seen) deletedSeenEpisodeNumbers.add(episode.episodeNumber)
@ -188,6 +212,7 @@ class SyncEpisodesWithSource(
val episodeUpdates = toChange.map { it.toEpisodeUpdate() }
updateEpisode.awaitAll(episodeUpdates)
}
updateAnime.awaitUpdateFetchInterval(anime, now, fetchWindow)
// Set this anime as updated since episodes were changed
// 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
import dataanime.Episodes
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.data.database.models.anime.EpisodeImpl
import tachiyomi.domain.items.episode.model.Episode
@ -12,7 +11,7 @@ fun Episode.toSEpisode(): SEpisode {
it.url = url
it.name = name
it.date_upload = dateUpload
it.episode_number = episodeNumber
it.episode_number = episodeNumber.toFloat()
it.scanlator = scanlator
}
}
@ -22,21 +21,11 @@ fun Episode.copyFromSEpisode(sEpisode: SEpisode): Episode {
name = sEpisode.name,
url = sEpisode.url,
dateUpload = sEpisode.date_upload,
episodeNumber = sEpisode.episode_number,
episodeNumber = sEpisode.episode_number.toDouble(),
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 {
it.id = id
it.anime_id = animeId
@ -49,6 +38,6 @@ fun Episode.toDbEpisode(): DbEpisode = EpisodeImpl().also {
it.total_seconds = totalSeconds
it.date_fetch = dateFetch
it.date_upload = dateUpload
it.episode_number = episodeNumber
it.episode_number = episodeNumber.toFloat()
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.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.applyFilter
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(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
}
}
@ -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.
* @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 unseenFilter = anime.unseenFilter
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 kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import tachiyomi.core.util.lang.compareToWithCollator
import tachiyomi.domain.source.anime.model.AnimeSource
import tachiyomi.domain.source.anime.repository.AnimeSourceRepository
import tachiyomi.source.local.entries.anime.LocalAnimeSource
import java.text.Collator
import java.util.Collections
import java.util.Locale
class GetAnimeSourcesWithFavoriteCount(
private val repository: AnimeSourceRepository,
@ -32,17 +31,13 @@ class GetAnimeSourcesWithFavoriteCount(
direction: SetMigrateSorting.Direction,
sorting: SetMigrateSorting.Mode,
): 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 ->
when (sorting) {
SetMigrateSorting.Mode.ALPHABETICAL -> {
when {
a.first.isStub && b.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 -> {

View file

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

View file

@ -21,7 +21,13 @@ class ToggleAnimeSource(
fun await(sourceIds: List<Long>, enable: Boolean) {
val transformedSourceIds = sourceIds.map { it.toString() }
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 tachiyomi.domain.source.manga.model.Source
import tachiyomi.domain.source.manga.repository.MangaSourceRepository
import java.util.SortedMap
class GetLanguagesWithMangaSources(
private val repository: MangaSourceRepository,
private val preferences: SourcePreferences,
) {
fun subscribe(): Flow<Map<String, List<Source>>> {
fun subscribe(): Flow<SortedMap<String, List<Source>>> {
return combine(
preferences.enabledLanguages().changes(),
preferences.disabledMangaSources().changes(),
@ -23,7 +24,8 @@ class GetLanguagesWithMangaSources(
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
)
sortedSources.groupBy { it.lang }
sortedSources
.groupBy { it.lang }
.toSortedMap(
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 kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import tachiyomi.core.util.lang.compareToWithCollator
import tachiyomi.domain.source.manga.model.Source
import tachiyomi.domain.source.manga.repository.MangaSourceRepository
import tachiyomi.source.local.entries.manga.LocalMangaSource
import java.text.Collator
import java.util.Collections
import java.util.Locale
class GetMangaSourcesWithFavoriteCount(
private val repository: MangaSourceRepository,
@ -32,17 +31,13 @@ class GetMangaSourcesWithFavoriteCount(
direction: SetMigrateSorting.Direction,
sorting: SetMigrateSorting.Mode,
): 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 ->
when (sorting) {
SetMigrateSorting.Mode.ALPHABETICAL -> {
when {
a.first.isStub && b.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 -> {

View file

@ -21,7 +21,13 @@ class ToggleMangaSource(
fun await(sourceIds: List<Long>, enable: Boolean) {
val transformedSourceIds = sourceIds.map { it.toString() }
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
import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.preference.getEnum
import tachiyomi.domain.library.model.LibraryDisplayMode
@ -11,17 +12,31 @@ class SourcePreferences(
// 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 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
@ -31,18 +46,27 @@ class SourcePreferences(
fun pinnedAnimeSources() = preferenceStore.getStringSet("pinned_anime_catalogues", emptySet())
fun pinnedMangaSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
fun lastUsedAnimeSource() = preferenceStore.getLong("last_anime_catalogue_source", -1)
fun lastUsedMangaSource() = preferenceStore.getLong("last_catalogue_source", -1)
fun lastUsedAnimeSource() = preferenceStore.getLong(
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 mangaExtensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
fun searchPinnedAnimeSourcesOnly() = preferenceStore.getBoolean("search_pinned_anime_sources_only", false)
fun searchPinnedMangaSourcesOnly() = preferenceStore.getBoolean("search_pinned_sources_only", false)
fun hideInAnimeLibraryItems() = preferenceStore.getBoolean(
"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", false)
fun hideInMangaLibraryItems() = preferenceStore.getBoolean(
"browse_hide_in_library_items",
false,
)
// SY -->
@ -62,7 +86,10 @@ class SourcePreferences(
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", "")

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.tachiyomi.data.track.AnimeTrackService
import eu.kanade.tachiyomi.data.track.AnimeTracker
import eu.kanade.tachiyomi.data.track.EnhancedAnimeTracker
import logcat.LogPriority
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.model.Episode
import tachiyomi.domain.items.episode.model.toEpisodeUpdate
import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
import tachiyomi.domain.track.anime.model.AnimeTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class SyncEpisodesWithTrackServiceTwoWay(
private val updateEpisode: UpdateEpisode = Injekt.get(),
private val insertTrack: InsertAnimeTrack = Injekt.get(),
class SyncEpisodeProgressWithTrack(
private val updateEpisode: UpdateEpisode,
private val insertTrack: InsertAnimeTrack,
private val getEpisodesByAnimeId: GetEpisodesByAnimeId,
) {
suspend fun await(
episodes: List<Episode>,
animeId: Long,
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
.filter { episode -> episode.episodeNumber <= remoteTrack.lastEpisodeSeen && !episode.seen }
.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.anime_id = animeId
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.total_episodes = totalEpisodes.toInt()
it.status = status.toInt()
it.score = score
it.score = score.toFloat()
it.tracking_url = remoteUrl
it.started_watching_date = startDate
it.finished_watching_date = finishDate
@ -40,7 +42,7 @@ fun DbAnimeTrack.toDomainTrack(idRequired: Boolean = true): AnimeTrack? {
lastEpisodeSeen = last_episode_seen.toDouble(),
totalEpisodes = total_episodes.toLong(),
status = status.toLong(),
score = score,
score = score.toDouble(),
remoteUrl = tracking_url,
startDate = started_watching_date,
finishDate = finished_watching_date,

View file

@ -8,30 +8,32 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
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.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.util.system.workManager
import logcat.LogPriority
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat
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.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) {
override suspend fun doWork(): Result {
val getTracks = Injekt.get<GetAnimeTracks>()
val insertTrack = Injekt.get<InsertAnimeTrack>()
if (runAttemptCount > 3) {
return Result.failure()
}
val getTracks = Injekt.get<GetAnimeTracks>()
val trackEpisode = Injekt.get<TrackEpisode>()
val trackManager = Injekt.get<TrackManager>()
val delayedTrackingStore = Injekt.get<DelayedAnimeTrackingStore>()
val results = withIOContext {
withIOContext {
delayedTrackingStore.getAnimeItems()
.mapNotNull {
val track = getTracks.awaitOne(it.trackId)
@ -40,24 +42,16 @@ class DelayedAnimeTrackingUpdateJob(context: Context, workerParams: WorkerParame
}
track?.copy(lastEpisodeSeen = it.lastEpisodeSeen.toDouble())
}
.mapNotNull { animeTrack ->
try {
val service = trackManager.getService(animeTrack.syncId)
if (service != null && service.isLogged) {
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
.forEach { animeTrack ->
logcat(LogPriority.DEBUG) {
"Updating delayed track item: ${animeTrack.animeId}" +
", last chapter read: ${animeTrack.lastEpisodeSeen}"
}
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 {
@ -70,7 +64,7 @@ class DelayedAnimeTrackingUpdateJob(context: Context, workerParams: WorkerParame
val request = OneTimeWorkRequestBuilder<DelayedAnimeTrackingUpdateJob>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 20, TimeUnit.SECONDS)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5.minutes.toJavaDuration())
.addTag(TAG)
.build()

View file

@ -4,7 +4,6 @@ import android.content.Context
import androidx.core.content.edit
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.track.anime.model.AnimeTrack
class DelayedAnimeTrackingStore(context: Context) {
@ -13,13 +12,12 @@ class DelayedAnimeTrackingStore(context: Context) {
*/
private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
fun addAnimeItem(track: AnimeTrack) {
val trackId = track.id.toString()
val lastEpisodeSeen = preferences.getFloat(trackId, 0f)
if (track.lastEpisodeSeen > lastEpisodeSeen) {
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last episode seen: ${track.lastEpisodeSeen}" }
fun addAnime(trackId: Long, lastEpisodeSeen: Double) {
val previousLastChapterRead = preferences.getFloat(trackId.toString(), 0f)
if (lastEpisodeSeen > previousLastChapterRead) {
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last episode seen: $lastEpisodeSeen" }
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.tachiyomi.data.track.MangaTrackService
import eu.kanade.tachiyomi.data.track.EnhancedMangaTracker
import eu.kanade.tachiyomi.data.track.MangaTracker
import logcat.LogPriority
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.model.Chapter
import tachiyomi.domain.items.chapter.model.toChapterUpdate
import tachiyomi.domain.track.manga.interactor.InsertMangaTrack
import tachiyomi.domain.track.manga.model.MangaTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class SyncChaptersWithTrackServiceTwoWay(
private val updateChapter: UpdateChapter = Injekt.get(),
private val insertTrack: InsertMangaTrack = Injekt.get(),
class SyncChapterProgressWithTrack(
private val updateChapter: UpdateChapter,
private val insertTrack: InsertMangaTrack,
private val getChaptersByMangaId: GetChaptersByMangaId,
) {
suspend fun await(
chapters: List<Chapter>,
mangaId: Long,
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
.filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read }
.map { it.copy(read = true).toChapterUpdate() }
@ -32,7 +39,7 @@ class SyncChaptersWithTrackServiceTwoWay(
val updatedTrack = remoteTrack.copy(lastChapterRead = localLastRead.toDouble())
try {
service.update(updatedTrack.toDbTrack())
tracker.update(updatedTrack.toDbTrack())
updateChapter.awaitAll(chapterUpdates)
insertTrack.await(updatedTrack)
} 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.manga_id = mangaId
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.total_chapters = totalChapters.toInt()
it.status = status.toInt()
it.score = score
it.score = score.toFloat()
it.tracking_url = remoteUrl
it.started_reading_date = startDate
it.finished_reading_date = finishDate
@ -40,7 +42,7 @@ fun DbMangaTrack.toDomainTrack(idRequired: Boolean = true): MangaTrack? {
lastChapterRead = last_chapter_read.toDouble(),
totalChapters = total_chapters.toLong(),
status = status.toLong(),
score = score,
score = score.toDouble(),
remoteUrl = tracking_url,
startDate = started_reading_date,
finishDate = finished_reading_date,

View file

@ -8,30 +8,32 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
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.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.util.system.workManager
import logcat.LogPriority
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat
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.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) {
override suspend fun doWork(): Result {
val getTracks = Injekt.get<GetMangaTracks>()
val insertTrack = Injekt.get<InsertMangaTrack>()
if (runAttemptCount > 3) {
return Result.failure()
}
val getTracks = Injekt.get<GetMangaTracks>()
val trackChapter = Injekt.get<TrackChapter>()
val trackManager = Injekt.get<TrackManager>()
val delayedTrackingStore = Injekt.get<DelayedMangaTrackingStore>()
val results = withIOContext {
withIOContext {
delayedTrackingStore.getMangaItems()
.mapNotNull {
val track = getTracks.awaitOne(it.trackId)
@ -40,24 +42,15 @@ class DelayedMangaTrackingUpdateJob(context: Context, workerParams: WorkerParame
}
track?.copy(lastChapterRead = it.lastChapterRead.toDouble())
}
.mapNotNull { track ->
try {
val service = trackManager.getService(track.syncId)
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
.forEach { track ->
logcat(LogPriority.DEBUG) {
"Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}"
}
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 {
@ -70,7 +63,7 @@ class DelayedMangaTrackingUpdateJob(context: Context, workerParams: WorkerParame
val request = OneTimeWorkRequestBuilder<DelayedMangaTrackingUpdateJob>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 20, TimeUnit.SECONDS)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5.minutes.toJavaDuration())
.addTag(TAG)
.build()

View file

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

View file

@ -1,6 +1,6 @@
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 tachiyomi.core.preference.PreferenceStore
@ -8,16 +8,16 @@ class TrackPreferences(
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)
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)
@ -25,7 +25,10 @@ class TrackPreferences(
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 {
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 relativeTime() = preferenceStore.getInt("relative_time", 7)
fun relativeTime() = preferenceStore.getBoolean("relative_time_v2", true)
fun dateFormat() = preferenceStore.getString("app_date_format", "")

View file

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

View file

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

View file

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.Error
import androidx.compose.material3.CircularProgressIndicator
@ -54,25 +55,13 @@ fun GlobalSearchResultItem(
Text(text = subtitle)
}
IconButton(onClick = onClick) {
Icon(imageVector = Icons.Outlined.ArrowForward, contentDescription = null)
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
}
}
content()
}
}
@Composable
fun GlobalSearchEmptyResultItem() {
Text(
text = stringResource(R.string.no_results_found),
modifier = Modifier
.padding(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
),
)
}
@Composable
fun GlobalSearchLoadingResultItem() {
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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items
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.History
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.animesource.ConfigurableAnimeSource
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 kotlinx.collections.immutable.persistentListOf
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.padding
import tachiyomi.presentation.core.screens.EmptyScreen
@ -64,7 +64,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
@Composable
fun AnimeExtensionDetailsScreen(
navigateUp: () -> Unit,
state: AnimeExtensionDetailsState,
state: AnimeExtensionDetailsScreenModel.State,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickWhatsNew: () -> Unit,
onClickReadme: () -> Unit,
@ -81,7 +81,8 @@ fun AnimeExtensionDetailsScreen(
navigateUp = navigateUp,
actions = {
AppBarActions(
actions = buildList {
actions = persistentListOf<AppBar.AppBarAction>().builder()
.apply {
if (state.extension?.isUnofficial == false) {
add(
AppBar.Action(
@ -93,7 +94,7 @@ fun AnimeExtensionDetailsScreen(
add(
AppBar.Action(
title = stringResource(R.string.action_faq_and_guides),
icon = Icons.Outlined.HelpOutline,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onClickReadme,
),
)
@ -114,7 +115,8 @@ fun AnimeExtensionDetailsScreen(
),
),
)
},
}
.build(),
)
},
scrollBehavior = scrollBehavior,
@ -175,7 +177,8 @@ private fun AnimeExtensionDetails(
data = Uri.fromParts("package", extension.pkgName, null)
context.startActivity(this)
}
},
Unit
}.takeIf { extension.isShared },
onClickAgeRating = {
showNsfwWarning = true
},
@ -187,7 +190,6 @@ private fun AnimeExtensionDetails(
key = { it.source.id },
) { source ->
SourceSwitchPreference(
modifier = Modifier.animateItemPlacement(),
source = source,
onClickSourcePreferences = onClickSourcePreferences,
onClickSource = onClickSource,
@ -208,7 +210,7 @@ private fun DetailsHeader(
extension: AnimeExtension,
onClickAgeRating: () -> Unit,
onClickUninstall: () -> Unit,
onClickAppInfo: () -> Unit,
onClickAppInfo: (() -> Unit)?,
) {
val context = LocalContext.current
@ -237,7 +239,9 @@ private fun DetailsHeader(
textAlign = TextAlign.Center,
)
val strippedPkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.animeextension.")
val strippedPkgName = extension.pkgName.substringAfter(
"eu.kanade.tachiyomi.animeextension.",
)
Text(
text = strippedPkgName,
@ -292,6 +296,7 @@ private fun DetailsHeader(
top = MaterialTheme.padding.small,
bottom = MaterialTheme.padding.medium,
),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
OutlinedButton(
modifier = Modifier.weight(1f),
@ -300,8 +305,7 @@ private fun DetailsHeader(
Text(stringResource(R.string.ext_uninstall))
}
Spacer(Modifier.width(16.dp))
if (onClickAppInfo != null) {
Button(
modifier = Modifier.weight(1f),
onClick = onClickAppInfo,
@ -312,17 +316,18 @@ private fun DetailsHeader(
)
}
}
}
Divider()
HorizontalDivider()
}
}
@Composable
private fun InfoText(
modifier: Modifier,
primaryText: String,
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
secondaryText: String,
modifier: Modifier = Modifier,
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
onClick: (() -> Unit)? = null,
) {
val interactionSource = remember { MutableInteractionSource() }
@ -355,20 +360,17 @@ private fun InfoText(
@Composable
private fun InfoDivider() {
Divider(
modifier = Modifier
.height(20.dp)
.width(1.dp),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = DIVIDER_ALPHA),
VerticalDivider(
modifier = Modifier.height(20.dp),
)
}
@Composable
private fun SourceSwitchPreference(
modifier: Modifier = Modifier,
source: AnimeExtensionSourceItem,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickSource: (sourceId: Long) -> Unit,
modifier: Modifier = Modifier,
) {
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.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
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.ui.browse.anime.extension.AnimeExtensionFilterState
import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.EmptyScreen
@ -53,12 +53,11 @@ private fun AnimeExtensionFilterContent(
onClickLang: (String) -> Unit,
) {
val context = LocalContext.current
FastScrollLazyColumn(
LazyColumn(
contentPadding = contentPadding,
) {
items(state.languages) { language ->
SwitchPreferenceWidget(
modifier = Modifier.animateItemPlacement(),
title = LocaleHelper.getSourceDisplayName(language, context),
checked = language in state.enabledLanguages,
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.anime.model.AnimeExtension
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 tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.PullRefresh
@ -56,7 +56,7 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable
fun AnimeExtensionScreen(
state: AnimeExtensionsState,
state: AnimeExtensionsScreenModel.State,
contentPadding: PaddingValues,
searchQuery: String?,
onLongClickItem: (AnimeExtension) -> Unit,
@ -72,10 +72,10 @@ fun AnimeExtensionScreen(
PullRefresh(
refreshing = state.isRefreshing,
onRefresh = onRefresh,
enabled = !state.isLoading,
enabled = { !state.isLoading },
) {
when {
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
state.isEmpty -> {
val msg = if (!searchQuery.isNullOrEmpty()) {
R.string.no_results_found
@ -107,7 +107,7 @@ fun AnimeExtensionScreen(
@Composable
private fun AnimeExtensionContent(
state: AnimeExtensionsState,
state: AnimeExtensionsScreenModel.State,
contentPadding: PaddingValues,
onLongClickItem: (AnimeExtension) -> Unit,
onClickItemCancel: (AnimeExtension) -> Unit,
@ -147,14 +147,13 @@ private fun AnimeExtensionContent(
}
ExtensionHeader(
textRes = header.textRes,
modifier = Modifier.animateItemPlacement(),
action = action,
)
}
is AnimeExtensionUiModel.Header.Text -> {
ExtensionHeader(
text = header.text,
modifier = Modifier.animateItemPlacement(),
)
}
}
@ -166,7 +165,7 @@ private fun AnimeExtensionContent(
key = { "extension-${it.hashCode()}" },
) { item ->
AnimeExtensionItem(
modifier = Modifier.animateItemPlacement(),
item = item,
onClickItem = {
when (it) {
@ -216,12 +215,12 @@ private fun AnimeExtensionContent(
@Composable
private fun AnimeExtensionItem(
modifier: Modifier = Modifier,
item: AnimeExtensionUiModel.Item,
onClickItem: (AnimeExtension) -> Unit,
onLongClickItem: (AnimeExtension) -> Unit,
onClickItemCancel: (AnimeExtension) -> Unit,
onClickItemAction: (AnimeExtension) -> Unit,
modifier: Modifier = Modifier,
) {
val (extension, installStep) = item
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(
extension = extension,
modifier = Modifier
@ -295,7 +297,10 @@ private fun AnimeExtensionItemContent(
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
if (extension is AnimeExtension.Installed && extension.lang.isNotEmpty()) {
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.more.settings.widget.SwitchPreferenceWidget
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 tachiyomi.domain.source.anime.model.AnimeSource
import tachiyomi.presentation.core.components.FastScrollLazyColumn
@ -22,7 +22,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
@Composable
fun AnimeSourcesFilterScreen(
navigateUp: () -> Unit,
state: AnimeSourcesFilterState.Success,
state: AnimeSourcesFilterScreenModel.State.Success,
onClickLanguage: (String) -> Unit,
onClickSource: (AnimeSource) -> Unit,
) {
@ -54,7 +54,7 @@ fun AnimeSourcesFilterScreen(
@Composable
private fun AnimeSourcesFilterContent(
contentPadding: PaddingValues,
state: AnimeSourcesFilterState.Success,
state: AnimeSourcesFilterScreenModel.State.Success,
onClickLanguage: (String) -> Unit,
onClickSource: (AnimeSource) -> Unit,
) {
@ -64,24 +64,24 @@ private fun AnimeSourcesFilterContent(
state.items.forEach { (language, sources) ->
val enabled = language in state.enabledLanguages
item(
key = language.hashCode(),
key = language,
contentType = "source-filter-header",
) {
AnimeSourcesFilterHeader(
modifier = Modifier.animateItemPlacement(),
language = language,
enabled = enabled,
onClickItem = onClickLanguage,
)
}
if (!enabled) return@forEach
if (enabled) {
items(
items = sources,
key = { "source-filter-${it.key()}" },
contentType = { "source-filter-item" },
) { source ->
AnimeSourcesFilterItem(
modifier = Modifier.animateItemPlacement(),
source = source,
isEnabled = "${source.id}" !in state.disabledSources,
onClickItem = onClickSource,
@ -90,13 +90,14 @@ private fun AnimeSourcesFilterContent(
}
}
}
}
@Composable
fun AnimeSourcesFilterHeader(
modifier: Modifier,
language: String,
enabled: Boolean,
onClickItem: (String) -> Unit,
modifier: Modifier = Modifier,
) {
SwitchPreferenceWidget(
modifier = modifier,
@ -108,10 +109,10 @@ fun AnimeSourcesFilterHeader(
@Composable
private fun AnimeSourcesFilterItem(
modifier: Modifier,
source: AnimeSource,
isEnabled: Boolean,
onClickItem: (AnimeSource) -> Unit,
modifier: Modifier = Modifier,
) {
BaseAnimeSourceItem(
modifier = modifier,

View file

@ -23,7 +23,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.browse.anime.components.BaseAnimeSourceItem
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.util.system.LocaleHelper
import tachiyomi.domain.source.anime.model.AnimeSource
@ -40,14 +40,14 @@ import tachiyomi.source.local.entries.anime.LocalAnimeSource
@Composable
fun AnimeSourcesScreen(
state: AnimeSourcesState,
state: AnimeSourcesScreenModel.State,
contentPadding: PaddingValues,
onClickItem: (AnimeSource, Listing) -> Unit,
onClickPin: (AnimeSource) -> Unit,
onLongClickItem: (AnimeSource) -> Unit,
) {
when {
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
state.isEmpty -> EmptyScreen(
textResource = R.string.source_empty_screen,
modifier = Modifier.padding(contentPadding),
@ -74,12 +74,12 @@ fun AnimeSourcesScreen(
when (model) {
is AnimeSourceUiModel.Header -> {
AnimeSourceHeader(
modifier = Modifier.animateItemPlacement(),
language = model.language,
)
}
is AnimeSourceUiModel.Item -> AnimeSourceItem(
modifier = Modifier.animateItemPlacement(),
source = model.source,
onClickItem = onClickItem,
onLongClickItem = onLongClickItem,
@ -94,25 +94,28 @@ fun AnimeSourcesScreen(
@Composable
private fun AnimeSourceHeader(
modifier: Modifier = Modifier,
language: String,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
Text(
text = LocaleHelper.getSourceDisplayName(language, context),
modifier = modifier
.padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small),
.padding(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
),
style = MaterialTheme.typography.header,
)
}
@Composable
private fun AnimeSourceItem(
modifier: Modifier = Modifier,
source: AnimeSource,
onClickItem: (AnimeSource, Listing) -> Unit,
onLongClickItem: (AnimeSource) -> Unit,
onClickPin: (AnimeSource) -> Unit,
modifier: Modifier = Modifier,
) {
BaseAnimeSourceItem(
modifier = modifier,
@ -144,7 +147,13 @@ private fun AnimeSourcePinButton(
onClick: () -> Unit,
) {
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
IconButton(onClick = onClick) {
Icon(
@ -192,7 +201,7 @@ fun AnimeSourceOptionsDialog(
)
}
sealed class AnimeSourceUiModel {
data class Item(val source: AnimeSource) : AnimeSourceUiModel()
data class Header(val language: String) : AnimeSourceUiModel()
sealed interface AnimeSourceUiModel {
data class Item(val source: AnimeSource) : 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.lazy.grid.GridCells
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.Public
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.tachiyomi.R
import eu.kanade.tachiyomi.animesource.AnimeSource
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.StateFlow
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.library.model.LibraryDisplayMode
@ -61,12 +63,12 @@ fun BrowseAnimeSourceContent(
if (animeList.itemCount > 0 && errorState != null && errorState is LoadState.Error) {
val result = snackbarHostState.showSnackbar(
message = getErrorMessage(errorState),
actionLabel = context.getString(R.string.action_webview_refresh),
actionLabel = context.getString(R.string.action_retry),
duration = SnackbarDuration.Indefinite,
)
when (result) {
SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss()
SnackbarResult.ActionPerformed -> animeList.refresh()
SnackbarResult.ActionPerformed -> animeList.retry()
}
}
}
@ -76,15 +78,15 @@ fun BrowseAnimeSourceContent(
modifier = Modifier.padding(contentPadding),
message = getErrorMessage(errorState),
actions = if (source is LocalAnimeSource) {
listOf(
persistentListOf(
EmptyScreenAction(
stringResId = R.string.local_source_help_guide,
icon = Icons.Outlined.HelpOutline,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onLocalAnimeSourceHelpClick,
),
)
} else {
listOf(
persistentListOf(
EmptyScreenAction(
stringResId = R.string.action_retry,
icon = Icons.Outlined.Refresh,
@ -97,7 +99,7 @@ fun BrowseAnimeSourceContent(
),
EmptyScreenAction(
stringResId = R.string.label_help,
icon = Icons.Outlined.HelpOutline,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onHelpClick,
),
)
@ -145,7 +147,7 @@ fun BrowseAnimeSourceContent(
}
@Composable
fun MissingSourceScreen(
internal fun MissingSourceScreen(
source: StubAnimeSource,
navigateUp: () -> Unit,
) {

View file

@ -1,34 +1,30 @@
package eu.kanade.presentation.browse.anime
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
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.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.R
import eu.kanade.presentation.browse.anime.components.GlobalAnimeSearchToolbar
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.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 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.padding
@Composable
fun GlobalAnimeSearchScreen(
state: GlobalAnimeSearchState,
state: AnimeSearchScreenModel.State,
navigateUp: () -> Unit,
onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit,
onChangeSearchFilter: (AnimeSourceFilter) -> Unit,
onToggleResults: () -> Unit,
getAnime: @Composable (Anime) -> State<Anime>,
onClickSource: (AnimeCatalogueSource) -> Unit,
onClickItem: (Anime) -> Unit,
@ -36,19 +32,23 @@ fun GlobalAnimeSearchScreen(
) {
Scaffold(
topBar = { scrollBehavior ->
GlobalSearchToolbar(
GlobalAnimeSearchToolbar(
searchQuery = state.searchQuery,
progress = state.progress,
total = state.total,
navigateUp = navigateUp,
onChangeSearchQuery = onChangeSearchQuery,
onSearch = onSearch,
sourceFilter = state.sourceFilter,
onChangeSearchFilter = onChangeSearchFilter,
onlyShowHasResults = state.onlyShowHasResults,
onToggleResults = onToggleResults,
scrollBehavior = scrollBehavior,
)
},
) { paddingValues ->
GlobalAnimeSearchContent(
items = state.items,
GlobalSearchContent(
items = state.filteredItems,
contentPadding = paddingValues,
getAnime = getAnime,
onClickSource = onClickSource,
@ -59,13 +59,14 @@ fun GlobalAnimeSearchScreen(
}
@Composable
private fun GlobalAnimeSearchContent(
internal fun GlobalSearchContent(
items: Map<AnimeCatalogueSource, AnimeSearchItemResult>,
contentPadding: PaddingValues,
getAnime: @Composable (Anime) -> State<Anime>,
onClickSource: (AnimeCatalogueSource) -> Unit,
onClickItem: (Anime) -> Unit,
onLongClickItem: (Anime) -> Unit,
fromSourceId: Long? = null,
) {
LazyColumn(
contentPadding = contentPadding,
@ -73,7 +74,8 @@ private fun GlobalAnimeSearchContent(
items.forEach { (source, result) ->
item(key = source.id) {
GlobalSearchResultItem(
title = source.name,
title = fromSourceId
?.let { "${source.name}".takeIf { source.id == fromSourceId } } ?: source.name,
subtitle = LocaleHelper.getDisplayName(source.lang),
onClick = { onClickSource(source) },
) {
@ -82,18 +84,6 @@ private fun GlobalAnimeSearchContent(
GlobalSearchLoadingResultItem()
}
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(
titles = result.result,
getAnime = getAnime,

View file

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

View file

@ -1,49 +1,48 @@
package eu.kanade.presentation.browse.anime
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import eu.kanade.presentation.browse.GlobalSearchEmptyResultItem
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.presentation.browse.anime.components.GlobalAnimeSearchToolbar
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.AnimeSearchItemResult
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.AnimeSearchScreenModel
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.AnimeSourceFilter
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.presentation.core.components.LazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
@Composable
fun MigrateAnimeSearchScreen(
state: AnimeSearchScreenModel.State,
fromSourceId: Long?,
navigateUp: () -> Unit,
state: MigrateAnimeSearchState,
getAnime: @Composable (Anime) -> State<Anime>,
onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit,
onChangeSearchFilter: (AnimeSourceFilter) -> Unit,
onToggleResults: () -> Unit,
getAnime: @Composable (Anime) -> State<Anime>,
onClickSource: (AnimeCatalogueSource) -> Unit,
onClickItem: (Anime) -> Unit,
onLongClickItem: (Anime) -> Unit,
) {
Scaffold(
topBar = { scrollBehavior ->
GlobalSearchToolbar(
GlobalAnimeSearchToolbar(
searchQuery = state.searchQuery,
progress = state.progress,
total = state.total,
navigateUp = navigateUp,
onChangeSearchQuery = onChangeSearchQuery,
onSearch = onSearch,
sourceFilter = state.sourceFilter,
onChangeSearchFilter = onChangeSearchFilter,
onlyShowHasResults = state.onlyShowHasResults,
onToggleResults = onToggleResults,
scrollBehavior = scrollBehavior,
)
},
) { paddingValues ->
MigrateAnimeSearchContent(
sourceId = state.anime?.source ?: -1,
items = state.items,
GlobalSearchContent(
fromSourceId = fromSourceId,
items = state.filteredItems,
contentPadding = paddingValues,
getAnime = getAnime,
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.BaseAnimeSourceItem
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 tachiyomi.domain.source.anime.model.AnimeSource
import tachiyomi.presentation.core.components.Badge
@ -43,7 +43,7 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable
fun MigrateAnimeSourceScreen(
state: MigrateAnimeSourceState,
state: MigrateAnimeSourceScreenModel.State,
contentPadding: PaddingValues,
onClickItem: (AnimeSource) -> Unit,
onToggleSortingDirection: () -> Unit,
@ -51,7 +51,7 @@ fun MigrateAnimeSourceScreen(
) {
val context = LocalContext.current
when {
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
state.isEmpty -> EmptyScreen(
textResource = R.string.information_empty_library,
modifier = Modifier.padding(contentPadding),
@ -102,14 +102,26 @@ private fun MigrateAnimeSourceList(
IconButton(onClick = onToggleSortingMode) {
when (sortingMode) {
SetMigrateSorting.Mode.ALPHABETICAL -> Icon(Icons.Outlined.SortByAlpha, contentDescription = stringResource(R.string.action_sort_alpha))
SetMigrateSorting.Mode.TOTAL -> Icon(Icons.Outlined.Numbers, contentDescription = stringResource(R.string.action_sort_count))
SetMigrateSorting.Mode.ALPHABETICAL -> Icon(
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) {
when (sortingDirection) {
SetMigrateSorting.Direction.ASCENDING -> Icon(Icons.Outlined.ArrowUpward, contentDescription = stringResource(R.string.action_asc))
SetMigrateSorting.Direction.DESCENDING -> Icon(Icons.Outlined.ArrowDownward, contentDescription = stringResource(R.string.action_desc))
SetMigrateSorting.Direction.ASCENDING -> Icon(
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}" },
) { (source, count) ->
MigrateAnimeSourceItem(
modifier = Modifier.animateItemPlacement(),
source = source,
count = count,
onClickItem = { onClickItem(source) },
@ -132,11 +144,11 @@ private fun MigrateAnimeSourceList(
@Composable
private fun MigrateAnimeSourceItem(
modifier: Modifier = Modifier,
source: AnimeSource,
count: Long,
onClickItem: () -> Unit,
onLongClickItem: () -> Unit,
modifier: Modifier = Modifier,
) {
BaseAnimeSourceItem(
modifier = modifier,

View file

@ -17,8 +17,8 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable
fun BaseAnimeSourceItem(
modifier: Modifier = Modifier,
source: AnimeSource,
modifier: Modifier = Modifier,
showLanguageInContent: Boolean = true,
onClickItem: () -> Unit = {},
onLongClickItem: () -> Unit = {},
@ -26,7 +26,9 @@ fun BaseAnimeSourceItem(
action: @Composable RowScope.(AnimeSource) -> Unit = {},
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(
modifier = modifier,
onClickItem = onClickItem,

View file

@ -1,6 +1,5 @@
package eu.kanade.presentation.browse.anime.components
import android.content.pm.PackageManager
import android.util.DisplayMetrics
import androidx.compose.foundation.Image
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.tachiyomi.R
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.source.anime.model.AnimeSource
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) {
withIOContext {
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)
Result.Success(
appResources.getDrawableForDensity(appInfo.icon, density, null)!!
@ -142,7 +145,7 @@ private fun AnimeExtension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT
}
sealed class Result<out T> {
object Loading : Result<Nothing>()
object Error : Result<Nothing>()
data object Loading : Result<Nothing>()
data object Error : Result<Nothing>()
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
BrowseAnimeSourceComfortableGridItem(
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
BrowseAnimeSourceCompactGridItem(
anime = anime,

View file

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

View file

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

View file

@ -1,15 +1,26 @@
package eu.kanade.presentation.browse.anime.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
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.AnimeCover
import tachiyomi.domain.entries.anime.model.asAnimeCover
import tachiyomi.presentation.core.components.material.padding
@ -20,13 +31,18 @@ fun GlobalAnimeSearchCardRow(
onClick: (Anime) -> Unit,
onLongClick: (Anime) -> Unit,
) {
if (titles.isEmpty()) {
EmptyResultItem()
return
}
LazyRow(
contentPadding = PaddingValues(MaterialTheme.padding.small),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
) {
items(titles) {
val title by getAnime(it)
GlobalSearchCard(
AnimeItem(
title = title.title,
cover = title.asAnimeCover(),
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.lazy.grid.GridCells
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.Public
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.tachiyomi.R
import eu.kanade.tachiyomi.source.MangaSource
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.StateFlow
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.library.model.LibraryDisplayMode
@ -61,12 +63,12 @@ fun BrowseSourceContent(
if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) {
val result = snackbarHostState.showSnackbar(
message = getErrorMessage(errorState),
actionLabel = context.getString(R.string.action_webview_refresh),
actionLabel = context.getString(R.string.action_retry),
duration = SnackbarDuration.Indefinite,
)
when (result) {
SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss()
SnackbarResult.ActionPerformed -> mangaList.refresh()
SnackbarResult.ActionPerformed -> mangaList.retry()
}
}
}
@ -76,15 +78,15 @@ fun BrowseSourceContent(
modifier = Modifier.padding(contentPadding),
message = getErrorMessage(errorState),
actions = if (source is LocalMangaSource) {
listOf(
persistentListOf(
EmptyScreenAction(
stringResId = R.string.local_source_help_guide,
icon = Icons.Outlined.HelpOutline,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onLocalSourceHelpClick,
),
)
} else {
listOf(
persistentListOf(
EmptyScreenAction(
stringResId = R.string.action_retry,
icon = Icons.Outlined.Refresh,
@ -97,7 +99,7 @@ fun BrowseSourceContent(
),
EmptyScreenAction(
stringResId = R.string.label_help,
icon = Icons.Outlined.HelpOutline,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onHelpClick,
),
)
@ -145,7 +147,7 @@ fun BrowseSourceContent(
}
@Composable
fun MissingSourceScreen(
internal fun MissingSourceScreen(
source: StubMangaSource,
navigateUp: () -> Unit,
) {

View file

@ -1,34 +1,30 @@
package eu.kanade.presentation.browse.manga
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
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.GlobalSearchLoadingResultItem
import eu.kanade.presentation.browse.GlobalSearchResultItem
import eu.kanade.presentation.browse.GlobalSearchToolbar
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.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.MangaSearchScreenModel
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.MangaSourceFilter
import eu.kanade.tachiyomi.util.system.LocaleHelper
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.padding
@Composable
fun GlobalMangaSearchScreen(
state: GlobalMangaSearchState,
state: MangaSearchScreenModel.State,
navigateUp: () -> Unit,
onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit,
onChangeSearchFilter: (MangaSourceFilter) -> Unit,
onToggleResults: () -> Unit,
getManga: @Composable (Manga) -> State<Manga>,
onClickSource: (CatalogueSource) -> Unit,
onClickItem: (Manga) -> Unit,
@ -36,19 +32,23 @@ fun GlobalMangaSearchScreen(
) {
Scaffold(
topBar = { scrollBehavior ->
GlobalSearchToolbar(
GlobalMangaSearchToolbar(
searchQuery = state.searchQuery,
progress = state.progress,
total = state.total,
navigateUp = navigateUp,
onChangeSearchQuery = onChangeSearchQuery,
onSearch = onSearch,
sourceFilter = state.sourceFilter,
onChangeSearchFilter = onChangeSearchFilter,
onlyShowHasResults = state.onlyShowHasResults,
onToggleResults = onToggleResults,
scrollBehavior = scrollBehavior,
)
},
) { paddingValues ->
GlobalSearchContent(
items = state.items,
items = state.filteredItems,
contentPadding = paddingValues,
getManga = getManga,
onClickSource = onClickSource,
@ -59,13 +59,14 @@ fun GlobalMangaSearchScreen(
}
@Composable
private fun GlobalSearchContent(
internal fun GlobalSearchContent(
items: Map<CatalogueSource, MangaSearchItemResult>,
contentPadding: PaddingValues,
getManga: @Composable (Manga) -> State<Manga>,
onClickSource: (CatalogueSource) -> Unit,
onClickItem: (Manga) -> Unit,
onLongClickItem: (Manga) -> Unit,
fromSourceId: Long? = null,
) {
LazyColumn(
contentPadding = contentPadding,
@ -73,7 +74,8 @@ private fun GlobalSearchContent(
items.forEach { (source, result) ->
item(key = source.id) {
GlobalSearchResultItem(
title = source.name,
title = fromSourceId
?.let { "${source.name}".takeIf { source.id == fromSourceId } } ?: source.name,
subtitle = LocaleHelper.getDisplayName(source.lang),
onClick = { onClickSource(source) },
) {
@ -82,18 +84,6 @@ private fun GlobalSearchContent(
GlobalSearchLoadingResultItem()
}
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(
titles = result.result,
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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items
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.History
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -30,6 +30,7 @@ import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.extension.manga.model.MangaExtension
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 kotlinx.collections.immutable.persistentListOf
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.padding
import tachiyomi.presentation.core.screens.EmptyScreen
@ -65,7 +65,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
@Composable
fun ExtensionDetailsScreen(
navigateUp: () -> Unit,
state: MangaExtensionDetailsState,
state: MangaExtensionDetailsScreenModel.State,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickWhatsNew: () -> Unit,
onClickReadme: () -> Unit,
@ -82,7 +82,8 @@ fun ExtensionDetailsScreen(
navigateUp = navigateUp,
actions = {
AppBarActions(
actions = buildList {
actions = persistentListOf<AppBar.AppBarAction>().builder()
.apply {
if (state.extension?.isUnofficial == false) {
add(
AppBar.Action(
@ -94,7 +95,7 @@ fun ExtensionDetailsScreen(
add(
AppBar.Action(
title = stringResource(R.string.action_faq_and_guides),
icon = Icons.Outlined.HelpOutline,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onClickReadme,
),
)
@ -115,7 +116,8 @@ fun ExtensionDetailsScreen(
),
),
)
},
}
.build(),
)
},
scrollBehavior = scrollBehavior,
@ -176,7 +178,8 @@ private fun ExtensionDetails(
data = Uri.fromParts("package", extension.pkgName, null)
context.startActivity(this)
}
},
Unit
}.takeIf { extension.isShared },
onClickAgeRating = {
showNsfwWarning = true
},
@ -188,7 +191,7 @@ private fun ExtensionDetails(
key = { it.source.id },
) { source ->
SourceSwitchPreference(
modifier = Modifier.animateItemPlacement(),
source = source,
onClickSourcePreferences = onClickSourcePreferences,
onClickSource = onClickSource,
@ -209,7 +212,7 @@ private fun DetailsHeader(
extension: MangaExtension,
onClickAgeRating: () -> Unit,
onClickUninstall: () -> Unit,
onClickAppInfo: () -> Unit,
onClickAppInfo: (() -> Unit)?,
) {
val context = LocalContext.current
@ -293,6 +296,7 @@ private fun DetailsHeader(
top = MaterialTheme.padding.small,
bottom = MaterialTheme.padding.medium,
),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
OutlinedButton(
modifier = Modifier.weight(1f),
@ -301,8 +305,7 @@ private fun DetailsHeader(
Text(stringResource(R.string.ext_uninstall))
}
Spacer(Modifier.width(16.dp))
if (onClickAppInfo != null) {
Button(
modifier = Modifier.weight(1f),
onClick = onClickAppInfo,
@ -313,17 +316,18 @@ private fun DetailsHeader(
)
}
}
}
Divider()
HorizontalDivider()
}
}
@Composable
private fun InfoText(
modifier: Modifier,
primaryText: String,
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
secondaryText: String,
modifier: Modifier = Modifier,
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
onClick: (() -> Unit)? = null,
) {
val interactionSource = remember { MutableInteractionSource() }
@ -356,20 +360,17 @@ private fun InfoText(
@Composable
private fun InfoDivider() {
Divider(
modifier = Modifier
.height(20.dp)
.width(1.dp),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = DIVIDER_ALPHA),
VerticalDivider(
modifier = Modifier.height(20.dp),
)
}
@Composable
private fun SourceSwitchPreference(
modifier: Modifier = Modifier,
source: MangaExtensionSourceItem,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickSource: (sourceId: Long) -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
@ -415,7 +416,7 @@ fun NsfwWarningDialog(
},
confirmButton = {
TextButton(onClick = onClickConfirm) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
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.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
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.ui.browse.manga.extension.MangaExtensionFilterState
import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.EmptyScreen
@ -53,12 +53,12 @@ private fun ExtensionFilterContent(
onClickLang: (String) -> Unit,
) {
val context = LocalContext.current
FastScrollLazyColumn(
LazyColumn(
contentPadding = contentPadding,
) {
items(state.languages) { language ->
SwitchPreferenceWidget(
modifier = Modifier.animateItemPlacement(),
title = LocaleHelper.getSourceDisplayName(language, context),
checked = language in state.enabledLanguages,
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.manga.model.MangaExtension
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 tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.PullRefresh
@ -57,7 +57,7 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable
fun MangaExtensionScreen(
state: MangaExtensionsState,
state: MangaExtensionsScreenModel.State,
contentPadding: PaddingValues,
searchQuery: String?,
onLongClickItem: (MangaExtension) -> Unit,
@ -73,10 +73,10 @@ fun MangaExtensionScreen(
PullRefresh(
refreshing = state.isRefreshing,
onRefresh = onRefresh,
enabled = !state.isLoading,
enabled = { !state.isLoading },
) {
when {
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
state.isEmpty -> {
val msg = if (!searchQuery.isNullOrEmpty()) {
R.string.no_results_found
@ -108,7 +108,7 @@ fun MangaExtensionScreen(
@Composable
private fun ExtensionContent(
state: MangaExtensionsState,
state: MangaExtensionsScreenModel.State,
contentPadding: PaddingValues,
onLongClickItem: (MangaExtension) -> Unit,
onClickItemCancel: (MangaExtension) -> Unit,
@ -148,14 +148,14 @@ private fun ExtensionContent(
}
ExtensionHeader(
textRes = header.textRes,
modifier = Modifier.animateItemPlacement(),
action = action,
)
}
is MangaExtensionUiModel.Header.Text -> {
ExtensionHeader(
text = header.text,
modifier = Modifier.animateItemPlacement(),
)
}
}
@ -167,7 +167,7 @@ private fun ExtensionContent(
key = { "extension-${it.hashCode()}" },
) { item ->
ExtensionItem(
modifier = Modifier.animateItemPlacement(),
item = item,
onClickItem = {
when (it) {
@ -217,12 +217,12 @@ private fun ExtensionContent(
@Composable
private fun ExtensionItem(
modifier: Modifier = Modifier,
item: MangaExtensionUiModel.Item,
onClickItem: (MangaExtension) -> Unit,
onLongClickItem: (MangaExtension) -> Unit,
onClickItemCancel: (MangaExtension) -> Unit,
onClickItemAction: (MangaExtension) -> Unit,
modifier: Modifier = Modifier,
) {
val (extension, installStep) = item
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(
extension = extension,
modifier = Modifier
@ -296,7 +299,10 @@ private fun ExtensionItemContent(
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
if (extension is MangaExtension.Installed && extension.lang.isNotEmpty()) {
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.manga.components.BaseMangaSourceItem
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.manga.source.MangaSourcesFilterState
import eu.kanade.tachiyomi.ui.browse.manga.source.MangaSourcesFilterScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.domain.source.manga.model.Source
import tachiyomi.presentation.core.components.FastScrollLazyColumn
@ -22,7 +22,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
@Composable
fun MangaSourcesFilterScreen(
navigateUp: () -> Unit,
state: MangaSourcesFilterState.Success,
state: MangaSourcesFilterScreenModel.State.Success,
onClickLanguage: (String) -> Unit,
onClickSource: (Source) -> Unit,
) {
@ -54,7 +54,7 @@ fun MangaSourcesFilterScreen(
@Composable
private fun SourcesFilterContent(
contentPadding: PaddingValues,
state: MangaSourcesFilterState.Success,
state: MangaSourcesFilterScreenModel.State.Success,
onClickLanguage: (String) -> Unit,
onClickSource: (Source) -> Unit,
) {
@ -64,24 +64,24 @@ private fun SourcesFilterContent(
state.items.forEach { (language, sources) ->
val enabled = language in state.enabledLanguages
item(
key = language.hashCode(),
key = language,
contentType = "source-filter-header",
) {
SourcesFilterHeader(
modifier = Modifier.animateItemPlacement(),
language = language,
enabled = enabled,
onClickItem = onClickLanguage,
)
}
if (!enabled) return@forEach
if (enabled) {
items(
items = sources,
key = { "source-filter-${it.key()}" },
contentType = { "source-filter-item" },
) { source ->
SourcesFilterItem(
modifier = Modifier.animateItemPlacement(),
source = source,
enabled = "${source.id}" !in state.disabledSources,
onClickItem = onClickSource,
@ -90,13 +90,15 @@ private fun SourcesFilterContent(
}
}
}
}
@Composable
private fun SourcesFilterHeader(
modifier: Modifier,
language: String,
enabled: Boolean,
onClickItem: (String) -> Unit,
modifier: Modifier = Modifier,
) {
SwitchPreferenceWidget(
modifier = modifier,
@ -108,10 +110,10 @@ private fun SourcesFilterHeader(
@Composable
private fun SourcesFilterItem(
modifier: Modifier,
source: Source,
enabled: Boolean,
onClickItem: (Source) -> Unit,
modifier: Modifier = Modifier,
) {
BaseMangaSourceItem(
modifier = modifier,

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