diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4c9f7011..7fe87256 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,7 +6,7 @@ on: types: [closed] env: DEVELOPER_DIR: /Applications/Xcode_14.1.app - APP_VERSION: '2.4.5' + APP_VERSION: '2.5.0' SCHEME_NAME: 'EhPanda' ALTSTORE_JSON_PATH: './AltStore.json' BUILDS_PATH: '/tmp/action-builds' diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index d422694c..dd717a04 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -177,6 +177,11 @@ AB86AC1327856F2700E61E6A /* AppLockStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86AC1227856F2700E61E6A /* AppLockStore.swift */; }; AB86AC1A2785C2B300E61E6A /* HomeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86AC192785C2B300E61E6A /* HomeStore.swift */; }; AB8C821926BF801700E8C5E6 /* EhSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB8C821826BF801700E8C5E6 /* EhSetting.swift */; }; + AB90276B291F548700697256 /* AppIcon_NotMyPresident@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = AB902766291F548600697256 /* AppIcon_NotMyPresident@3x.png */; }; + AB90276C291F548700697256 /* AppIcon_NotMyPresident_iPad@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AB902767291F548600697256 /* AppIcon_NotMyPresident_iPad@2x.png */; }; + AB90276D291F548700697256 /* AppIcon_NotMyPresident_iPad_Pro@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AB902768291F548700697256 /* AppIcon_NotMyPresident_iPad_Pro@2x.png */; }; + AB90276E291F548700697256 /* AppIcon_NotMyPresident_iPad.png in Resources */ = {isa = PBXBuildFile; fileRef = AB902769291F548700697256 /* AppIcon_NotMyPresident_iPad.png */; }; + AB90276F291F548700697256 /* AppIcon_NotMyPresident@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AB90276A291F548700697256 /* AppIcon_NotMyPresident@2x.png */; }; ABA12F3227D49CEB0021922D /* AccountSettingStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA12F3127D49CEB0021922D /* AccountSettingStoreTests.swift */; }; ABA732D925A8018A00B3D9AB /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA732D825A8018A00B3D9AB /* Extensions.swift */; }; ABA732DF25A852D800B3D9AB /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA732DE25A852D800B3D9AB /* Filter.swift */; }; @@ -473,6 +478,11 @@ AB86AC1227856F2700E61E6A /* AppLockStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockStore.swift; sourceTree = ""; }; AB86AC192785C2B300E61E6A /* HomeStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeStore.swift; sourceTree = ""; }; AB8C821826BF801700E8C5E6 /* EhSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EhSetting.swift; sourceTree = ""; }; + AB902766291F548600697256 /* AppIcon_NotMyPresident@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIcon_NotMyPresident@3x.png"; sourceTree = ""; }; + AB902767291F548600697256 /* AppIcon_NotMyPresident_iPad@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIcon_NotMyPresident_iPad@2x.png"; sourceTree = ""; }; + AB902768291F548700697256 /* AppIcon_NotMyPresident_iPad_Pro@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIcon_NotMyPresident_iPad_Pro@2x.png"; sourceTree = ""; }; + AB902769291F548700697256 /* AppIcon_NotMyPresident_iPad.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = AppIcon_NotMyPresident_iPad.png; sourceTree = ""; }; + AB90276A291F548700697256 /* AppIcon_NotMyPresident@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIcon_NotMyPresident@2x.png"; sourceTree = ""; }; AB994DBB25986F7A00E9A367 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; ABA12F3127D49CEB0021922D /* AccountSettingStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingStoreTests.swift; sourceTree = ""; }; ABA732D825A8018A00B3D9AB /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; @@ -884,6 +894,11 @@ AB0CFB8527BBD2D7004BD372 /* AppIcon_Developer_iPad.png */, AB0CFB8327BBD2D7004BD372 /* AppIcon_Developer_iPad@2x.png */, AB0CFB8427BBD2D7004BD372 /* AppIcon_Developer@2x.png */, + AB902768291F548700697256 /* AppIcon_NotMyPresident_iPad_Pro@2x.png */, + AB902769291F548700697256 /* AppIcon_NotMyPresident_iPad.png */, + AB902767291F548600697256 /* AppIcon_NotMyPresident_iPad@2x.png */, + AB90276A291F548700697256 /* AppIcon_NotMyPresident@2x.png */, + AB902766291F548600697256 /* AppIcon_NotMyPresident@3x.png */, ABE9012027F722D100F3651D /* AppIcon_StandWithUkraine2022_iPad_Pro@2x.png */, ABE9011E27F722D100F3651D /* AppIcon_StandWithUkraine2022_iPad.png */, ABE9012127F722D100F3651D /* AppIcon_StandWithUkraine2022_iPad@2x.png */, @@ -1512,13 +1527,17 @@ files = ( AB0CFB8C27BBD2D7004BD372 /* AppIcon_Developer@3x.png in Resources */, AB0CFB9627BBD323004BD372 /* AppIcon_Ukiyoe@2x.png in Resources */, + AB90276E291F548700697256 /* AppIcon_NotMyPresident_iPad.png in Resources */, + AB90276D291F548700697256 /* AppIcon_NotMyPresident_iPad_Pro@2x.png in Resources */, AB0CFB7A27BAB9D0004BD372 /* AppIcon_Default@2x.png in Resources */, AB0CFB7527BAB9D0004BD372 /* AppIcon_Default_iPad@2x.png in Resources */, AB0CFB9227BBD323004BD372 /* AppIcon_Ukiyoe_iPad@2x.png in Resources */, ABC3C7852593699B00E0C11B /* Assets.xcassets in Resources */, AB0CFB7B27BAB9D0004BD372 /* AppIcon_Default_iPad.png in Resources */, + AB90276C291F548700697256 /* AppIcon_NotMyPresident_iPad@2x.png in Resources */, AB0CFB9527BBD323004BD372 /* AppIcon_Ukiyoe_iPad.png in Resources */, ABE9012427F722D100F3651D /* AppIcon_StandWithUkraine2022@3x.png in Resources */, + AB90276B291F548700697256 /* AppIcon_NotMyPresident@3x.png in Resources */, AB0CFB8827BBD2D7004BD372 /* AppIcon_Developer_iPad@2x.png in Resources */, ABE9012227F722D100F3651D /* AppIcon_StandWithUkraine2022@2x.png in Resources */, AB0CFB7427BAB9D0004BD372 /* AppIcon_Default@3x.png in Resources */, @@ -1526,6 +1545,7 @@ AB0CFB9327BBD323004BD372 /* AppIcon_Ukiyoe@3x.png in Resources */, AB0CFB8927BBD2D7004BD372 /* AppIcon_Developer@2x.png in Resources */, ABEE0AFA2595C6F800C997AE /* Localizable.strings in Resources */, + AB90276F291F548700697256 /* AppIcon_NotMyPresident@2x.png in Resources */, ABD5FDD4263D05110021A4C6 /* .swiftlint.yml in Resources */, AB26F59B27AD125A00AB3468 /* Constant.strings in Resources */, ABE9012527F722D100F3651D /* AppIcon_StandWithUkraine2022_iPad_Pro@2x.png in Resources */, diff --git a/EhPanda/App/Icons/AppIcon_NotMyPresident@2x.png b/EhPanda/App/Icons/AppIcon_NotMyPresident@2x.png new file mode 100644 index 00000000..379899ca Binary files /dev/null and b/EhPanda/App/Icons/AppIcon_NotMyPresident@2x.png differ diff --git a/EhPanda/App/Icons/AppIcon_NotMyPresident@3x.png b/EhPanda/App/Icons/AppIcon_NotMyPresident@3x.png new file mode 100644 index 00000000..5e177cc7 Binary files /dev/null and b/EhPanda/App/Icons/AppIcon_NotMyPresident@3x.png differ diff --git a/EhPanda/App/Icons/AppIcon_NotMyPresident_iPad.png b/EhPanda/App/Icons/AppIcon_NotMyPresident_iPad.png new file mode 100644 index 00000000..aef7fbfa Binary files /dev/null and b/EhPanda/App/Icons/AppIcon_NotMyPresident_iPad.png differ diff --git a/EhPanda/App/Icons/AppIcon_NotMyPresident_iPad@2x.png b/EhPanda/App/Icons/AppIcon_NotMyPresident_iPad@2x.png new file mode 100644 index 00000000..3fb7a02f Binary files /dev/null and b/EhPanda/App/Icons/AppIcon_NotMyPresident_iPad@2x.png differ diff --git a/EhPanda/App/Icons/AppIcon_NotMyPresident_iPad_Pro@2x.png b/EhPanda/App/Icons/AppIcon_NotMyPresident_iPad_Pro@2x.png new file mode 100644 index 00000000..ab1558c1 Binary files /dev/null and b/EhPanda/App/Icons/AppIcon_NotMyPresident_iPad_Pro@2x.png differ diff --git a/EhPanda/App/Info.plist b/EhPanda/App/Info.plist index ac83771e..d8e97ffc 100644 --- a/EhPanda/App/Info.plist +++ b/EhPanda/App/Info.plist @@ -40,6 +40,13 @@ AppIcon_StandWithUkraine2022 + AppIcon_NotMyPresident + + CFBundleIconFiles + + AppIcon_NotMyPresident + + CFBundlePrimaryIcon @@ -85,6 +92,14 @@ AppIcon_StandWithUkraine2022_iPad_Pro + AppIcon_NotMyPresident + + CFBundleIconFiles + + AppIcon_NotMyPresident_iPad + AppIcon_NotMyPresident_iPad_Pro + + CFBundlePrimaryIcon diff --git a/EhPanda/App/Tools/Defaults.swift b/EhPanda/App/Tools/Defaults.swift index ad0a61c7..8d5070d5 100644 --- a/EhPanda/App/Tools/Defaults.swift +++ b/EhPanda/App/Tools/Defaults.swift @@ -79,17 +79,17 @@ struct Defaults { static let login = forum.appending(queryItems: [.act: .loginAct, .code: .zeroOne]) static let webLogin = forum.appending(queryItems: [.act: .loginAct]) - static let api = host.appendingPathComponent("api.php") - static let myTags = host.appendingPathComponent("mytags") + static var api: Foundation.URL { host.appendingPathComponent("api.php") } + static var myTags: Foundation.URL { host.appendingPathComponent("mytags") } static let news = ehentai.appendingPathComponent("news.php") - static let uConfig = host.appendingPathComponent("uconfig.php") - static let galleryPopups = host.appendingPathComponent("gallerypopups.php") - static let galleryTorrents = host.appendingPathComponent("gallerytorrents.php") + static var uConfig: Foundation.URL { host.appendingPathComponent("uconfig.php") } + static var galleryPopups: Foundation.URL { host.appendingPathComponent("gallerypopups.php") } + static var galleryTorrents: Foundation.URL { host.appendingPathComponent("gallerytorrents.php") } - static let popular = host.appendingPathComponent("popular") - static let watched = host.appendingPathComponent("watched") + static var popular: Foundation.URL { host.appendingPathComponent("popular") } + static var watched: Foundation.URL { host.appendingPathComponent("watched") } static let toplist = ehentai.appendingPathComponent("toplist.php") - static let favorites = host.appendingPathComponent("favorites.php") + static var favorites: Foundation.URL { host.appendingPathComponent("favorites.php") } // GitHub static let github: Foundation.URL = .init(string: "https://github.com/").forceUnwrapped diff --git a/EhPanda/App/Tools/Parser.swift b/EhPanda/App/Tools/Parser.swift index a42dcf71..9b02dc0b 100644 --- a/EhPanda/App/Tools/Parser.swift +++ b/EhPanda/App/Tools/Parser.swift @@ -1109,7 +1109,7 @@ extension Parser { let form = tmpForm else { throw AppError.parseFailed } // swiftlint:disable line_length - var ehProfiles = [EhProfile](); var isCapableOfCreatingNewProfile: Bool?; var capableLoadThroughHathSetting: EhSetting.LoadThroughHathSetting?; var capableImageResolution: EhSetting.ImageResolution?; var capableSearchResultCount: EhSetting.SearchResultCount?; var capableThumbnailConfigSize: EhSetting.ThumbnailSize?; var capableThumbnailConfigRowCount: EhSetting.ThumbnailRowCount?; var loadThroughHathSetting: EhSetting.LoadThroughHathSetting?; var browsingCountry: EhSetting.BrowsingCountry?; var imageResolution: EhSetting.ImageResolution?; var imageSizeWidth: Float?; var imageSizeHeight: Float?; var galleryName: EhSetting.GalleryName?; var literalBrowsingCountry: String?; var archiverBehavior: EhSetting.ArchiverBehavior?; var displayMode: EhSetting.DisplayMode?; var disabledCategories = [Bool](); var favoriteCategories = [String](); var favoritesSortOrder: EhSetting.FavoritesSortOrder?; var ratingsColor: String?; var excludedNamespaces = [Bool](); var tagFilteringThreshold: Float?; var tagWatchingThreshold: Float?; var excludedLanguages = [Bool](); var excludedUploaders: String?; var searchResultCount: EhSetting.SearchResultCount?; var thumbnailLoadTiming: EhSetting.ThumbnailLoadTiming?; var thumbnailConfigSize: EhSetting.ThumbnailSize?; var thumbnailConfigRows: EhSetting.ThumbnailRowCount?; var thumbnailScaleFactor: Float?; var viewportVirtualWidth: Float?; var commentsSortOrder: EhSetting.CommentsSortOrder?; var commentVotesShowTiming: EhSetting.CommentVotesShowTiming?; var tagsSortOrder: EhSetting.TagsSortOrder?; var galleryShowPageNumbers: Bool?; var useOriginalImages: Bool?; var useMultiplePageViewer: Bool?; var multiplePageViewerStyle: EhSetting.MultiplePageViewerStyle?; var multiplePageViewerShowThumbnailPane: Bool? + var ehProfiles = [EhProfile](); var isCapableOfCreatingNewProfile: Bool?; var capableLoadThroughHathSetting: EhSetting.LoadThroughHathSetting?; var capableImageResolution: EhSetting.ImageResolution?; var capableSearchResultCount: EhSetting.SearchResultCount?; var capableThumbnailConfigSize: EhSetting.ThumbnailSize?; var capableThumbnailConfigRowCount: EhSetting.ThumbnailRowCount?; var loadThroughHathSetting: EhSetting.LoadThroughHathSetting?; var browsingCountry: EhSetting.BrowsingCountry?; var imageResolution: EhSetting.ImageResolution?; var imageSizeWidth: Float?; var imageSizeHeight: Float?; var galleryName: EhSetting.GalleryName?; var literalBrowsingCountry: String?; var archiverBehavior: EhSetting.ArchiverBehavior?; var displayMode: EhSetting.DisplayMode?; var disabledCategories = [Bool](); var favoriteCategories = [String](); var favoritesSortOrder: EhSetting.FavoritesSortOrder?; var ratingsColor: String?; var excludedNamespaces = [Bool](); var tagFilteringThreshold: Float?; var tagWatchingThreshold: Float?; var showFilteredRemovalCount: Bool?; var excludedLanguages = [Bool](); var excludedUploaders: String?; var searchResultCount: EhSetting.SearchResultCount?; var thumbnailLoadTiming: EhSetting.ThumbnailLoadTiming?; var thumbnailConfigSize: EhSetting.ThumbnailSize?; var thumbnailConfigRows: EhSetting.ThumbnailRowCount?; var thumbnailScaleFactor: Float?; var viewportVirtualWidth: Float?; var commentsSortOrder: EhSetting.CommentsSortOrder?; var commentVotesShowTiming: EhSetting.CommentVotesShowTiming?; var tagsSortOrder: EhSetting.TagsSortOrder?; var galleryShowPageNumbers: Bool?; var useOriginalImages: Bool?; var useMultiplePageViewer: Bool?; var multiplePageViewerStyle: EhSetting.MultiplePageViewerStyle?; var multiplePageViewerShowThumbnailPane: Bool? // swiftlint:enable line_length ehProfiles = parseSelections(node: profileOuter, name: "profile_set") @@ -1193,6 +1193,9 @@ extension Parser { tagWatchingThreshold = Float(parseString(node: optouter, name: "wt") ?? "0") if tagWatchingThreshold == nil { tagWatchingThreshold = 0 } } + if optouter.at_xpath("//input [@name='tf']") != nil { + showFilteredRemovalCount = parseInt(node: optouter, name: "tf") == 0 + } if optouter.at_xpath("//div [@id='xlasel']") != nil { excludedLanguages = Array(0...49) .map { "xl_\(EhSetting.languageValues[$0])" } @@ -1254,7 +1257,7 @@ extension Parser { guard !ehProfiles.filter(\.isSelected).isEmpty, let isCapableOfCreatingNewProfile, let capableLoadThroughHathSetting, let capableImageResolution, let capableSearchResultCount, let capableThumbnailConfigSize, let capableThumbnailConfigRowCount, let loadThroughHathSetting, let browsingCountry, let literalBrowsingCountry, let imageResolution, let imageSizeWidth, let imageSizeHeight, let galleryName, let archiverBehavior, let displayMode, disabledCategories.count == 10, favoriteCategories.count == 10, let favoritesSortOrder, let ratingsColor, excludedNamespaces.count == 11, let tagFilteringThreshold, let tagWatchingThreshold, excludedLanguages.count == 50, let excludedUploaders, let searchResultCount, let thumbnailLoadTiming, let thumbnailConfigSize, let thumbnailConfigRows, let thumbnailScaleFactor, let viewportVirtualWidth, let commentsSortOrder, let commentVotesShowTiming, let tagsSortOrder, let galleryShowPageNumbers else { throw AppError.parseFailed } - return EhSetting(ehProfiles: ehProfiles.sorted(), isCapableOfCreatingNewProfile: isCapableOfCreatingNewProfile, capableLoadThroughHathSetting: capableLoadThroughHathSetting, capableImageResolution: capableImageResolution, capableSearchResultCount: capableSearchResultCount, capableThumbnailConfigSize: capableThumbnailConfigSize, capableThumbnailConfigRowCount: capableThumbnailConfigRowCount, loadThroughHathSetting: loadThroughHathSetting, browsingCountry: browsingCountry, literalBrowsingCountry: literalBrowsingCountry, imageResolution: imageResolution, imageSizeWidth: imageSizeWidth, imageSizeHeight: imageSizeHeight, galleryName: galleryName, archiverBehavior: archiverBehavior, displayMode: displayMode, disabledCategories: disabledCategories, favoriteCategories: favoriteCategories, favoritesSortOrder: favoritesSortOrder, ratingsColor: ratingsColor, excludedNamespaces: excludedNamespaces, tagFilteringThreshold: tagFilteringThreshold, tagWatchingThreshold: tagWatchingThreshold, excludedLanguages: excludedLanguages, excludedUploaders: excludedUploaders, searchResultCount: searchResultCount, thumbnailLoadTiming: thumbnailLoadTiming, thumbnailConfigSize: thumbnailConfigSize, thumbnailConfigRows: thumbnailConfigRows, thumbnailScaleFactor: thumbnailScaleFactor, viewportVirtualWidth: viewportVirtualWidth, commentsSortOrder: commentsSortOrder, commentVotesShowTiming: commentVotesShowTiming, tagsSortOrder: tagsSortOrder, galleryShowPageNumbers: galleryShowPageNumbers, useOriginalImages: useOriginalImages, useMultiplePageViewer: useMultiplePageViewer, multiplePageViewerStyle: multiplePageViewerStyle, multiplePageViewerShowThumbnailPane: multiplePageViewerShowThumbnailPane + return EhSetting(ehProfiles: ehProfiles.sorted(), isCapableOfCreatingNewProfile: isCapableOfCreatingNewProfile, capableLoadThroughHathSetting: capableLoadThroughHathSetting, capableImageResolution: capableImageResolution, capableSearchResultCount: capableSearchResultCount, capableThumbnailConfigSize: capableThumbnailConfigSize, capableThumbnailConfigRowCount: capableThumbnailConfigRowCount, loadThroughHathSetting: loadThroughHathSetting, browsingCountry: browsingCountry, literalBrowsingCountry: literalBrowsingCountry, imageResolution: imageResolution, imageSizeWidth: imageSizeWidth, imageSizeHeight: imageSizeHeight, galleryName: galleryName, archiverBehavior: archiverBehavior, displayMode: displayMode, disabledCategories: disabledCategories, favoriteCategories: favoriteCategories, favoritesSortOrder: favoritesSortOrder, ratingsColor: ratingsColor, excludedNamespaces: excludedNamespaces, tagFilteringThreshold: tagFilteringThreshold, tagWatchingThreshold: tagWatchingThreshold, showFilteredRemovalCount: showFilteredRemovalCount, excludedLanguages: excludedLanguages, excludedUploaders: excludedUploaders, searchResultCount: searchResultCount, thumbnailLoadTiming: thumbnailLoadTiming, thumbnailConfigSize: thumbnailConfigSize, thumbnailConfigRows: thumbnailConfigRows, thumbnailScaleFactor: thumbnailScaleFactor, viewportVirtualWidth: viewportVirtualWidth, commentsSortOrder: commentsSortOrder, commentVotesShowTiming: commentVotesShowTiming, tagsSortOrder: tagsSortOrder, galleryShowPageNumbers: galleryShowPageNumbers, useOriginalImages: useOriginalImages, useMultiplePageViewer: useMultiplePageViewer, multiplePageViewerStyle: multiplePageViewerStyle, multiplePageViewerShowThumbnailPane: multiplePageViewerShowThumbnailPane ) // swiftlint:enable line_length } @@ -1343,14 +1346,24 @@ extension Parser { let currentStr = link.at_xpath("//td [@class='ptds']")?.text else { if let link = doc.at_xpath("//div [@class='searchnav']") { + var timestamp: String? var isEnabled = false for aLink in link.xpath("//a") where aLink.text?.contains("Next") == true { + timestamp = aLink["href"] + .map(URLComponents.init)?? + .queryItems? + .first(where: { $0.name == "next" })? + .value? + .split(separator: "-") + .last + .map(String.init) + isEnabled = true break } - return PageNumber(isNextButtonEnabled: isEnabled) + return PageNumber(lastItemTimestamp: timestamp, isNextButtonEnabled: isEnabled) } else { return PageNumber(isNextButtonEnabled: false) } diff --git a/EhPanda/App/Tools/Utilities/URLUtil.swift b/EhPanda/App/Tools/Utilities/URLUtil.swift index 981d4d3d..ebd5b419 100644 --- a/EhPanda/App/Tools/Utilities/URLUtil.swift +++ b/EhPanda/App/Tools/Utilities/URLUtil.swift @@ -16,6 +16,7 @@ struct URLUtil { } return Defaults.URL.host.appending(queryItems: queryItems).applyingFilter(filter) } + static func moreSearchList(keyword: String, filter: Filter, pageNum: Int, lastID: String) -> URL { var queryItems: [Defaults.URL.Component.Key: String] = [.fSearch: keyword] if AppUtil.galleryHost == .ehentai { @@ -26,6 +27,7 @@ struct URLUtil { } return Defaults.URL.host.appending(queryItems: queryItems).applyingFilter(filter) } + static func frontpageList(filter: Filter, pageNum: Int? = nil) -> URL { var url = Defaults.URL.host if let pageNum = pageNum { @@ -33,6 +35,7 @@ struct URLUtil { } return url.applyingFilter(filter) } + static func moreFrontpageList(filter: Filter, pageNum: Int, lastID: String) -> URL { var queryItems = [Defaults.URL.Component.Key: String]() if AppUtil.galleryHost == .ehentai { @@ -43,9 +46,11 @@ struct URLUtil { } return Defaults.URL.host.appending(queryItems: queryItems).applyingFilter(filter) } + static func popularList(filter: Filter) -> URL { Defaults.URL.popular.applyingFilter(filter) } + static func watchedList(filter: Filter, pageNum: Int? = nil, keyword: String = "") -> URL { var url = Defaults.URL.watched if let pageNum = pageNum { @@ -56,6 +61,7 @@ struct URLUtil { } return url.applyingFilter(filter) } + static func moreWatchedList(filter: Filter, pageNum: Int, lastID: String, keyword: String = "") -> URL { var url: URL if AppUtil.galleryHost == .ehentai { @@ -68,8 +74,11 @@ struct URLUtil { } return url.applyingFilter(filter) } + static func favoritesList( - favIndex: Int, pageNum: Int? = nil, keyword: String = "", + favIndex: Int, + pageNum: Int? = nil, + keyword: String = "", sortOrder: FavoritesSortOrder? = nil ) -> URL { var url = Defaults.URL.favorites @@ -93,12 +102,25 @@ struct URLUtil { } return url } - static func moreFavoritesList(favIndex: Int, pageNum: Int, lastID: String, keyword: String = "") -> URL { + + static func moreFavoritesList( + favIndex: Int, + pageNum: Int, + lastID: String, + lastTimestamp: String? = nil, + keyword: String = "" + ) -> URL { var url: URL if AppUtil.galleryHost == .ehentai { url = Defaults.URL.favorites.appending(queryItems: [.page: String(pageNum), .from: lastID]) } else { - url = Defaults.URL.favorites.appending(queryItems: [.next: lastID]) + var queryItems: [Defaults.URL.Component.Key: String] + if let lastTimestamp { + queryItems = [.next: [lastID, lastTimestamp].joined(separator: "-")] + } else { + queryItems = [.next: lastID] + } + url = Defaults.URL.favorites.appending(queryItems: queryItems) } if favIndex != -1 { url.append(queryItems: [.favcat: String(favIndex)]) @@ -111,6 +133,7 @@ struct URLUtil { } return url } + static func toplistsList(catIndex: Int, pageNum: Int? = nil) -> URL { var url = Defaults.URL.toplist.appending(queryItems: [.topcat: String(catIndex)]) if let pageNum = pageNum { @@ -118,12 +141,15 @@ struct URLUtil { } return url } + static func moreToplistsList(catIndex: Int, pageNum: Int) -> URL { Defaults.URL.toplist.appending(queryItems: [.topcat: String(catIndex), .letterP: String(pageNum)]) } + static func galleryDetail(url: URL) -> URL { url.appending(queryItems: [.showComments: .one]) } + static func galleryTorrents(gid: String, token: String) -> URL { Defaults.URL.galleryTorrents.appending(queryItems: [.gid: gid, .token: token]) } @@ -134,6 +160,7 @@ struct URLUtil { .appending(queryItems: [.gid: gid, .token: token]) .appending(queryItems: [.act: .addFavAct]) } + static func userInfo(uid: String) -> URL { Defaults.URL.forum.appending(queryItems: [.showUser: uid]) } @@ -142,6 +169,7 @@ struct URLUtil { static func detailPage(url: URL, pageNum: Int) -> URL { url.appending(queryItems: [.letterP: String(pageNum)]) } + static func normalPreviewURL(plainURL: URL, width: String, height: String, offset: String) -> URL { plainURL.appending(queryItems: [.ehpandaWidth: width, .ehpandaHeight: height, .ehpandaOffset: offset]) } @@ -150,6 +178,7 @@ struct URLUtil { static func githubAPI(repoName: String) -> URL { Defaults.URL.githubAPI.appendingPathComponent("\(repoName)/releases/latest") } + static func githubDownload(repoName: String, fileName: String) -> URL { Defaults.URL.github.appendingPathComponent("\(repoName)/releases/latest/download/\(fileName)") } diff --git a/EhPanda/App/de.lproj/Localizable.strings b/EhPanda/App/de.lproj/Localizable.strings index c34e6153..021e4d46 100644 --- a/EhPanda/App/de.lproj/Localizable.strings +++ b/EhPanda/App/de.lproj/Localizable.strings @@ -228,6 +228,7 @@ "enum.app.icon.type.value.ukiyoe" = "Ukiyo-e"; "enum.app.icon.type.value.developer" = "Developer"; "enum.app.icon.type.value.standWithUkraine2022" = "Stand With Ukraine (2022)"; +"enum.app.icon.type.value.notMyPresident" = "NOT MY PRESIDENT"; // ListDisplayMode "enum.display.mode.value.detail" = "Detail"; "enum.display.mode.value.thumbnail" = "Thumbnail"; @@ -472,6 +473,10 @@ "eh.setting.view.title.tagWatchingThreshold" = "Tag Watching Threshold"; "eh.setting.view.description.tagWatchingThreshold" = "Recently uploaded galleries will be included on the watched screen if it has at least one watched tag with positive weight, and the sum of weights on its watched tags add up to this value or higher. This threshold can be set between 0 and 9999."; +"eh.setting.view.section.title.filteredRemovalCount" = "Show Filtered Removal Count"; +"eh.setting.view.description.filteredRemovalCount" = "Show the \"Your default filters removed XX galleries from this page\" readout?"; +"eh.setting.view.title.showFilteredRemovalCount" = "Show filtered removal count"; + "eh.setting.view.section.title.excludedLanguages" = "Excluded Languages"; "eh.setting.view.description.excludedLanguages" = "If you wish to hide galleries in certain languages from the gallery list and searches, select them from the list below. Note that matching galleries will never appear regardless of your search query."; // EhSetting.ExcludedLanguagesCategory diff --git a/EhPanda/App/en.lproj/Localizable.strings b/EhPanda/App/en.lproj/Localizable.strings index d1f33d07..347925d0 100644 --- a/EhPanda/App/en.lproj/Localizable.strings +++ b/EhPanda/App/en.lproj/Localizable.strings @@ -228,6 +228,7 @@ "enum.app.icon.type.value.ukiyoe" = "Ukiyo-e"; "enum.app.icon.type.value.developer" = "Developer"; "enum.app.icon.type.value.standWithUkraine2022" = "Stand With Ukraine (2022)"; +"enum.app.icon.type.value.notMyPresident" = "NOT MY PRESIDENT"; // ListDisplayMode "enum.display.mode.value.detail" = "Detail"; "enum.display.mode.value.thumbnail" = "Thumbnail"; @@ -472,6 +473,10 @@ "eh.setting.view.title.tagWatchingThreshold" = "Tag Watching Threshold"; "eh.setting.view.description.tagWatchingThreshold" = "Recently uploaded galleries will be included on the watched screen if it has at least one watched tag with positive weight, and the sum of weights on its watched tags add up to this value or higher. This threshold can be set between 0 and 9999."; +"eh.setting.view.section.title.filteredRemovalCount" = "Show Filtered Removal Count"; +"eh.setting.view.description.filteredRemovalCount" = "Show the \"Your default filters removed XX galleries from this page\" readout?"; +"eh.setting.view.title.showFilteredRemovalCount" = "Show filtered removal count"; + "eh.setting.view.section.title.excludedLanguages" = "Excluded Languages"; "eh.setting.view.description.excludedLanguages" = "If you wish to hide galleries in certain languages from the gallery list and searches, select them from the list below. Note that matching galleries will never appear regardless of your search query."; // EhSetting.ExcludedLanguagesCategory diff --git a/EhPanda/App/ja.lproj/Localizable.strings b/EhPanda/App/ja.lproj/Localizable.strings index d711fefc..be2f6ea6 100644 --- a/EhPanda/App/ja.lproj/Localizable.strings +++ b/EhPanda/App/ja.lproj/Localizable.strings @@ -228,8 +228,9 @@ "enum.app.icon.type.value.ukiyoe" = "浮世絵"; "enum.app.icon.type.value.developer" = "デベロッパー"; "enum.app.icon.type.value.standWithUkraine2022" = "ウクライナと共に (2022)"; +"enum.app.icon.type.value.notMyPresident" = "私の大統領ではない"; // ListDisplayMode -"enum.display.mode.value.detail" = "デフォルト"; +"enum.display.mode.value.detail" = "詳細"; "enum.display.mode.value.thumbnail" = "サムネイル"; // MARK: AppIconView @@ -472,6 +473,10 @@ "eh.setting.view.title.tagWatchingThreshold" = "タグ購読しきい値"; "eh.setting.view.description.tagWatchingThreshold" = "もしあるギャラリーは最近投稿されたもので、少なくても一つの正の重みの購読タグを持っていて、購読タグの重み総和がこのしきい値と同じまたはより高ければ、そのギャラリーは購読画面で表示されます。このしきい値はゼロから 9999 まで設定できます。"; +"eh.setting.view.section.title.filteredRemovalCount" = "フィルター除去数"; +"eh.setting.view.description.filteredRemovalCount" = "「既定フィルターにより本ページから XX 個のギャラリーが除去されました」を表示しますか?"; +"eh.setting.view.title.showFilteredRemovalCount" = "フィルター除去数を表示"; + "eh.setting.view.section.title.excludedLanguages" = "排除された言語"; "eh.setting.view.description.excludedLanguages" = "特定の言語のギャラリーをリストと検索結果から隠したい場合、下に選択してください。注意:どんな検索クエリーを使ってもこれらの言語のギャラリーは表示されません。"; // EhSetting.ExcludedLanguagesCategory diff --git a/EhPanda/App/ko.lproj/Localizable.strings b/EhPanda/App/ko.lproj/Localizable.strings index f16424d0..3180b18a 100644 --- a/EhPanda/App/ko.lproj/Localizable.strings +++ b/EhPanda/App/ko.lproj/Localizable.strings @@ -228,6 +228,7 @@ "enum.app.icon.type.value.ukiyoe" = "Ukiyo-e"; "enum.app.icon.type.value.developer" = "Developer"; "enum.app.icon.type.value.standWithUkraine2022" = "Stand With Ukraine (2022)"; +"enum.app.icon.type.value.notMyPresident" = "NOT MY PRESIDENT"; // ListDisplayMode "enum.display.mode.value.detail" = "자세히"; "enum.display.mode.value.thumbnail" = "썸네일"; @@ -472,6 +473,10 @@ "eh.setting.view.title.tagWatchingThreshold" = "태그 보여주기 임계값"; "eh.setting.view.description.tagWatchingThreshold" = "최근에 업로드된 갤러리는 최소 1개의 Watched 태그가 있고 Watched 태그의 가중치의 합이 이 값 이상이 될 경우 Watched 화면에 포함되어요. 이 임계값은 0과 9999 사이에서 설정할 수 있어요."; +"eh.setting.view.section.title.filteredRemovalCount" = "Show Filtered Removal Count"; +"eh.setting.view.description.filteredRemovalCount" = "Show the \"Your default filters removed XX galleries from this page\" readout?"; +"eh.setting.view.title.showFilteredRemovalCount" = "Show filtered removal count"; + "eh.setting.view.section.title.excludedLanguages" = "제외된 언어"; "eh.setting.view.description.excludedLanguages" = "갤러리 목록에서 특정 언어로 된 갤러리를 숨기고 검색하려면 아래 목록에서 해당 갤러리를 선택해주세요. 검색어에 관계없이 일치하는 갤러리는 나타나지 않아요."; // EhSetting.ExcludedLanguagesCategory diff --git a/EhPanda/App/zh-Hans.lproj/Localizable.strings b/EhPanda/App/zh-Hans.lproj/Localizable.strings index 9e438a84..87c56e5b 100644 --- a/EhPanda/App/zh-Hans.lproj/Localizable.strings +++ b/EhPanda/App/zh-Hans.lproj/Localizable.strings @@ -228,6 +228,7 @@ "enum.app.icon.type.value.ukiyoe" = "浮世绘"; "enum.app.icon.type.value.developer" = "开发者"; "enum.app.icon.type.value.standWithUkraine2022" = "与乌克兰同在 (2022)"; +"enum.app.icon.type.value.notMyPresident" = "他不是我的主席"; // ListDisplayMode "enum.display.mode.value.detail" = "详情"; "enum.display.mode.value.thumbnail" = "缩略图"; @@ -472,6 +473,10 @@ "eh.setting.view.title.tagWatchingThreshold" = "标签订阅阈值"; "eh.setting.view.description.tagWatchingThreshold" = "你可以通过将标签加入“我的标签”并设置一个正权重来关注它们。如果一个最近上传的作品所有标签的权重之和高于设定值,则它将会被包含在“关注”里。这个值可以设定为 0 ~ 9999。"; +"eh.setting.view.section.title.filteredRemovalCount" = "筛选器移除数"; +"eh.setting.view.description.filteredRemovalCount" = "要显示“你的默认筛选器从本页移除了 XX 个画廊”提示吗?"; +"eh.setting.view.title.showFilteredRemovalCount" = "显示筛选器移除数"; + "eh.setting.view.section.title.excludedLanguages" = "屏蔽的语言"; "eh.setting.view.description.excludedLanguages" = "如果你希望以从列表或搜索结果中隐藏特定语言的画廊,请从下面的列表中选择。注意:无论搜索条件为何,这些画廊都不会出现。"; // EhSetting.ExcludedLanguagesCategory diff --git a/EhPanda/App/zh-Hant.lproj/Localizable.strings b/EhPanda/App/zh-Hant.lproj/Localizable.strings index cabfe8d7..9b96706f 100644 --- a/EhPanda/App/zh-Hant.lproj/Localizable.strings +++ b/EhPanda/App/zh-Hant.lproj/Localizable.strings @@ -228,6 +228,7 @@ "enum.app.icon.type.value.ukiyoe" = "Ukiyo-e"; "enum.app.icon.type.value.developer" = "Developer"; "enum.app.icon.type.value.standWithUkraine2022" = "Stand With Ukraine (2022)"; +"enum.app.icon.type.value.notMyPresident" = "NOT MY PRESIDENT"; // ListDisplayMode "enum.display.mode.value.detail" = "詳情"; "enum.display.mode.value.thumbnail" = "縮略圖"; @@ -472,6 +473,10 @@ "eh.setting.view.title.tagWatchingThreshold" = "Tag Watching Threshold"; "eh.setting.view.description.tagWatchingThreshold" = "Recently uploaded galleries will be included on the watched screen if it has at least one watched tag with positive weight, and the sum of weights on its watched tags add up to this value or higher. This threshold can be set between 0 and 9999."; +"eh.setting.view.section.title.filteredRemovalCount" = "Show Filtered Removal Count"; +"eh.setting.view.description.filteredRemovalCount" = "Show the \"Your default filters removed XX galleries from this page\" readout?"; +"eh.setting.view.title.showFilteredRemovalCount" = "Show filtered removal count"; + "eh.setting.view.section.title.excludedLanguages" = "Excluded Languages"; "eh.setting.view.description.excludedLanguages" = "If you wish to hide galleries in certain languages from the gallery list and searches, select them from the list below. Note that matching galleries will never appear regardless of your search query."; // EhSetting.ExcludedLanguagesCategory diff --git a/EhPanda/DataFlow/AppStore.swift b/EhPanda/DataFlow/AppStore.swift index 5034b0c4..70661104 100644 --- a/EhPanda/DataFlow/AppStore.swift +++ b/EhPanda/DataFlow/AppStore.swift @@ -292,6 +292,7 @@ let appReducer = Reducer.combine( databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient, + userDefaultsClient: $0.userDefaultsClient, uiApplicationClient: $0.uiApplicationClient ) } diff --git a/EhPanda/Models/Support/EhSetting.swift b/EhPanda/Models/Support/EhSetting.swift index da70ab3a..b928d25d 100644 --- a/EhPanda/Models/Support/EhSetting.swift +++ b/EhPanda/Models/Support/EhSetting.swift @@ -83,6 +83,7 @@ struct EhSetting: Equatable { var excludedNamespaces: [Bool] var tagFilteringThreshold: Float var tagWatchingThreshold: Float + var showFilteredRemovalCount: Bool? var excludedLanguages: [Bool] var excludedUploaders: String var searchResultCount: SearchResultCount diff --git a/EhPanda/Models/Support/Misc.swift b/EhPanda/Models/Support/Misc.swift index 0c450298..6c59902c 100644 --- a/EhPanda/Models/Support/Misc.swift +++ b/EhPanda/Models/Support/Misc.swift @@ -28,6 +28,7 @@ extension DateFormattable { struct PageNumber: Equatable { var current = 0 var maximum = 0 + var lastItemTimestamp: String? var isNextButtonEnabled = true var isSinglePage: Bool { diff --git a/EhPanda/Network/Request.swift b/EhPanda/Network/Request.swift index 79905100..98f6e1fa 100644 --- a/EhPanda/Network/Request.swift +++ b/EhPanda/Network/Request.swift @@ -19,14 +19,18 @@ extension Request { var effect: Effect, Never> { publisher.receive(on: DispatchQueue.main).catchToEffect() } + func mapAppError(error: Error) -> AppError { switch error { case is ParseError: return .parseFailed + case is URLError: return .networkingFailed + case is DecodingError: return .parseFailed + default: return error as? AppError ?? .unknown } @@ -40,10 +44,7 @@ private extension Publisher { } private extension URLRequest { mutating func setURLEncodedContentType() { - setValue( - "application/x-www-form-urlencoded", - forHTTPHeaderField: "Content-Type" - ) + setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") } } private extension Dictionary where Key == String, Value == String { @@ -60,8 +61,11 @@ private extension Dictionary where Key == String, Value == String { struct GreetingRequest: Request { var publisher: AnyPublisher { URLSession.shared.dataTaskPublisher(for: Defaults.URL.news) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .tryMap(Parser.parseGreeting).mapError(mapAppError).eraseToAnyPublisher() + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + .tryMap(Parser.parseGreeting) + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -70,16 +74,22 @@ struct UserInfoRequest: Request { var publisher: AnyPublisher { URLSession.shared.dataTaskPublisher(for: URLUtil.userInfo(uid: uid)) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .tryMap(Parser.parseUserInfo).mapError(mapAppError).eraseToAnyPublisher() + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + .tryMap(Parser.parseUserInfo) + .mapError(mapAppError) + .eraseToAnyPublisher() } } struct FavoriteCategoriesRequest: Request { var publisher: AnyPublisher<[Int: String], AppError> { URLSession.shared.dataTaskPublisher(for: Defaults.URL.uConfig) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .tryMap(Parser.parseFavoriteCategories).mapError(mapAppError).eraseToAnyPublisher() + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + .tryMap(Parser.parseFavoriteCategories) + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -110,9 +120,7 @@ struct TagTranslatorRequest: Request { .flatMap { date in URLSession.shared.dataTaskPublisher(for: language.downloadURL) .tryMap { data, _ in - let response = try JSONDecoder().decode( - EhTagTranslationDatabaseResponse.self, from: data - ) + let response = try JSONDecoder().decode(EhTagTranslationDatabaseResponse.self, from: data) var translations = response.tagTranslations guard !translations.isEmpty else { throw AppError.parseFailed } if language == .traditionalChinese { @@ -121,7 +129,8 @@ struct TagTranslatorRequest: Request { return TagTranslator(language: language, updatedDate: date, translations: translations) } } - .mapError(mapAppError).eraseToAnyPublisher() + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -135,9 +144,11 @@ struct SearchGalleriesRequest: Request { URLSession.shared.dataTaskPublisher( for: URLUtil.searchList(keyword: keyword, filter: filter, pageNum: pageNum) ) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } .tryMap { (Parser.parsePageNum(doc: $0), try Parser.parseGalleries(doc: $0)) } - .mapError(mapAppError).eraseToAnyPublisher() + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -148,12 +159,16 @@ struct MoreSearchGalleriesRequest: Request { let pageNum: Int var publisher: AnyPublisher<(PageNumber, [Gallery]), AppError> { - URLSession.shared.dataTaskPublisher(for: URLUtil.moreSearchList( - keyword: keyword, filter: filter, pageNum: pageNum, lastID: lastID - )) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + URLSession.shared.dataTaskPublisher( + for: URLUtil.moreSearchList( + keyword: keyword, filter: filter, pageNum: pageNum, lastID: lastID + ) + ) + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } .tryMap { (Parser.parsePageNum(doc: $0), try Parser.parseGalleries(doc: $0)) } - .mapError(mapAppError).eraseToAnyPublisher() + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -163,9 +178,11 @@ struct FrontpageGalleriesRequest: Request { var publisher: AnyPublisher<(PageNumber, [Gallery]), AppError> { URLSession.shared.dataTaskPublisher(for: URLUtil.frontpageList(filter: filter, pageNum: pageNum)) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } .tryMap { (Parser.parsePageNum(doc: $0), try Parser.parseGalleries(doc: $0)) } - .mapError(mapAppError).eraseToAnyPublisher() + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -175,12 +192,16 @@ struct MoreFrontpageGalleriesRequest: Request { let pageNum: Int var publisher: AnyPublisher<(PageNumber, [Gallery]), AppError> { - URLSession.shared.dataTaskPublisher(for: URLUtil.moreFrontpageList( - filter: filter, pageNum: pageNum, lastID: lastID - )) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + URLSession.shared.dataTaskPublisher( + for: URLUtil.moreFrontpageList( + filter: filter, pageNum: pageNum, lastID: lastID + ) + ) + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } .tryMap { (Parser.parsePageNum(doc: $0), try Parser.parseGalleries(doc: $0)) } - .mapError(mapAppError).eraseToAnyPublisher() + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -189,23 +210,30 @@ struct PopularGalleriesRequest: Request { var publisher: AnyPublisher<[Gallery], AppError> { URLSession.shared.dataTaskPublisher(for: URLUtil.popularList(filter: filter)) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .tryMap(Parser.parseGalleries).mapError(mapAppError).eraseToAnyPublisher() + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + .tryMap(Parser.parseGalleries) + .mapError(mapAppError) + .eraseToAnyPublisher() } } struct WatchedGalleriesRequest: Request { let filter: Filter var pageNum: Int? - var keyword: String + let keyword: String var publisher: AnyPublisher<(PageNumber, [Gallery]), AppError> { - URLSession.shared.dataTaskPublisher(for: URLUtil.watchedList( - filter: filter, pageNum: pageNum, keyword: keyword - )) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + URLSession.shared.dataTaskPublisher( + for: URLUtil.watchedList( + filter: filter, pageNum: pageNum, keyword: keyword + ) + ) + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } .tryMap { (Parser.parsePageNum(doc: $0), try Parser.parseGalleries(doc: $0)) } - .mapError(mapAppError).eraseToAnyPublisher() + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -213,55 +241,70 @@ struct MoreWatchedGalleriesRequest: Request { let filter: Filter let lastID: String let pageNum: Int - var keyword: String + let keyword: String var publisher: AnyPublisher<(PageNumber, [Gallery]), AppError> { - URLSession.shared.dataTaskPublisher(for: URLUtil.moreWatchedList( - filter: filter, pageNum: pageNum, lastID: lastID, keyword: keyword - )) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + URLSession.shared.dataTaskPublisher( + for: URLUtil.moreWatchedList( + filter: filter, pageNum: pageNum, lastID: lastID, keyword: keyword + ) + ) + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } .tryMap { (Parser.parsePageNum(doc: $0), try Parser.parseGalleries(doc: $0)) } - .mapError(mapAppError).eraseToAnyPublisher() + .mapError(mapAppError) + .eraseToAnyPublisher() } } struct FavoritesGalleriesRequest: Request { let favIndex: Int var pageNum: Int? - var keyword: String + let keyword: String var sortOrder: FavoritesSortOrder? var publisher: AnyPublisher<(PageNumber, FavoritesSortOrder?, [Gallery]), AppError> { URLSession.shared.dataTaskPublisher( for: URLUtil.favoritesList(favIndex: favIndex, pageNum: pageNum, keyword: keyword, sortOrder: sortOrder) ) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .tryMap { ( - Parser.parsePageNum(doc: $0), - Parser.parseFavoritesSortOrder(doc: $0), - try Parser.parseGalleries(doc: $0) - ) } - .mapError(mapAppError).eraseToAnyPublisher() + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + .tryMap { + ( + Parser.parsePageNum(doc: $0), + Parser.parseFavoritesSortOrder(doc: $0), + try Parser.parseGalleries(doc: $0) + ) + } + .mapError(mapAppError) + .eraseToAnyPublisher() } } struct MoreFavoritesGalleriesRequest: Request { let favIndex: Int let lastID: String + var lastTimestamp: String? let pageNum: Int - var keyword: String + let keyword: String var publisher: AnyPublisher<(PageNumber, FavoritesSortOrder?, [Gallery]), AppError> { - URLSession.shared.dataTaskPublisher(for: URLUtil.moreFavoritesList( - favIndex: favIndex, pageNum: pageNum, lastID: lastID, keyword: keyword - )) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .tryMap { ( - Parser.parsePageNum(doc: $0), - Parser.parseFavoritesSortOrder(doc: $0), - try Parser.parseGalleries(doc: $0) - ) } - .mapError(mapAppError).eraseToAnyPublisher() + URLSession.shared.dataTaskPublisher( + for: URLUtil.moreFavoritesList( + favIndex: favIndex, pageNum: pageNum, lastID: lastID, lastTimestamp: lastTimestamp, keyword: keyword + ) + ) + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + .tryMap { + ( + Parser.parsePageNum(doc: $0), + Parser.parseFavoritesSortOrder(doc: $0), + try Parser.parseGalleries(doc: $0) + ) + } + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -273,9 +316,11 @@ struct ToplistsGalleriesRequest: Request { URLSession.shared.dataTaskPublisher( for: URLUtil.toplistsList(catIndex: catIndex, pageNum: pageNum) ) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } .tryMap { (Parser.parsePageNum(doc: $0), try Parser.parseGalleries(doc: $0)) } - .mapError(mapAppError).eraseToAnyPublisher() + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -284,12 +329,16 @@ struct MoreToplistsGalleriesRequest: Request { let pageNum: Int var publisher: AnyPublisher<(PageNumber, [Gallery]), AppError> { - URLSession.shared.dataTaskPublisher(for: URLUtil.moreToplistsList( - catIndex: catIndex, pageNum: pageNum - )) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + URLSession.shared.dataTaskPublisher( + for: URLUtil.moreToplistsList( + catIndex: catIndex, pageNum: pageNum + ) + ) + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } .tryMap { (Parser.parsePageNum(doc: $0), try Parser.parseGalleries(doc: $0)) } - .mapError(mapAppError).eraseToAnyPublisher() + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -300,7 +349,8 @@ struct GalleryDetailRequest: Request { var publisher: AnyPublisher<(GalleryDetail, GalleryState, String, Greeting?), AppError> { URLSession.shared.dataTaskPublisher(for: URLUtil.galleryDetail(url: galleryURL)) - .genericRetry().compactMap { resp -> HTMLDocument? in + .genericRetry() + .compactMap { resp -> HTMLDocument? in var htmlDocument: HTMLDocument? do { htmlDocument = try Kanna.HTML(html: resp.data, encoding: .utf8) @@ -319,7 +369,8 @@ struct GalleryDetailRequest: Request { .map { doc, detail, state, apiKey in (detail, state, apiKey, try? Parser.parseGreeting(doc: doc)) } - .mapError(mapAppError).eraseToAnyPublisher() + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -330,11 +381,17 @@ struct GalleryReverseRequest: Request { func getGallery(from detail: GalleryDetail?, and url: URL) -> Gallery? { if let detail = detail { return Gallery( - gid: url.pathComponents[2], token: url.pathComponents[3], - title: detail.title, rating: detail.rating, tags: [], - category: detail.category, uploader: detail.uploader, - pageCount: detail.pageCount, postedDate: detail.postedDate, - coverURL: detail.coverURL, galleryURL: url + gid: url.pathComponents[2], + token: url.pathComponents[3], + title: detail.title, + rating: detail.rating, + tags: [], + category: detail.category, + uploader: detail.uploader, + pageCount: detail.pageCount, + postedDate: detail.postedDate, + coverURL: detail.coverURL, + galleryURL: url ) } else { return nil @@ -342,7 +399,10 @@ struct GalleryReverseRequest: Request { } var publisher: AnyPublisher { - galleryURL(url: url).genericRetry().flatMap(gallery).eraseToAnyPublisher() + galleryURL(url: url) + .genericRetry() + .flatMap(gallery) + .eraseToAnyPublisher() } func galleryURL(url: URL) -> AnyPublisher { @@ -350,10 +410,14 @@ struct GalleryReverseRequest: Request { case true: return URLSession.shared.dataTaskPublisher(for: url) .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .tryMap(Parser.parseGalleryURL).mapError(mapAppError) + .tryMap(Parser.parseGalleryURL) + .mapError(mapAppError) .eraseToAnyPublisher() + case false: - return Just(url).setFailureType(to: AppError.self).eraseToAnyPublisher() + return Just(url) + .setFailureType(to: AppError.self) + .eraseToAnyPublisher() } } @@ -361,14 +425,13 @@ struct GalleryReverseRequest: Request { URLSession.shared.dataTaskPublisher(for: url) .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } .compactMap { - guard let (detail, _) = try? Parser.parseGalleryDetail( - doc: $0, gid: url.pathComponents[2] - ) + guard let (detail, _) = try? Parser.parseGalleryDetail(doc: $0, gid: url.pathComponents[2]) else { return nil } return getGallery(from: detail, and: url) } - .mapError(mapAppError).eraseToAnyPublisher() + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -376,19 +439,20 @@ struct GalleryArchiveRequest: Request { let archiveURL: URL var publisher: AnyPublisher<(GalleryArchive, String?, String?), AppError> { - URLSession.shared.dataTaskPublisher(for: archiveURL).genericRetry() + URLSession.shared.dataTaskPublisher(for: archiveURL) + .genericRetry() .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } .tryMap { (html: HTMLDocument) -> (HTMLDocument, GalleryArchive) in let archive = try Parser.parseGalleryArchive(doc: html) return (html, archive) } .map { html, archive in - guard let (currentGP, currentCredits) = - try? Parser.parseCurrentFunds(doc: html) + guard let (currentGP, currentCredits) = try? Parser.parseCurrentFunds(doc: html) else { return (archive, nil, nil) } return (archive, currentGP, currentCredits) } - .mapError(mapAppError).eraseToAnyPublisher() + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -397,21 +461,25 @@ struct GalleryArchiveFundsRequest: Request { let galleryURL: URL var publisher: AnyPublisher<(String, String), AppError> { - archiveURL(url: galleryURL).genericRetry() - .flatMap(funds).eraseToAnyPublisher() + archiveURL(url: galleryURL) + .genericRetry() + .flatMap(funds) + .eraseToAnyPublisher() } func archiveURL(url: URL) -> AnyPublisher { URLSession.shared.dataTaskPublisher(for: url) .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } .compactMap { try? Parser.parseGalleryDetail(doc: $0, gid: gid).0.archiveURL } - .mapError(mapAppError).eraseToAnyPublisher() + .mapError(mapAppError) + .eraseToAnyPublisher() } func funds(url: URL) -> AnyPublisher<(String, String), AppError> { URLSession.shared.dataTaskPublisher(for: url) .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .tryMap(Parser.parseCurrentFunds).mapError(mapAppError) + .tryMap(Parser.parseCurrentFunds) + .mapError(mapAppError) .eraseToAnyPublisher() } } @@ -421,11 +489,12 @@ struct GalleryTorrentsRequest: Request { let token: String var publisher: AnyPublisher<[GalleryTorrent], AppError> { - URLSession.shared.dataTaskPublisher( - for: URLUtil.galleryTorrents(gid: gid, token: token) - ) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .map(Parser.parseGalleryTorrents).mapError(mapAppError).eraseToAnyPublisher() + URLSession.shared.dataTaskPublisher(for: URLUtil.galleryTorrents(gid: gid, token: token)) + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + .map(Parser.parseGalleryTorrents) + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -435,8 +504,11 @@ struct GalleryPreviewURLsRequest: Request { var publisher: AnyPublisher<[Int: URL], AppError> { URLSession.shared.dataTaskPublisher(for: URLUtil.detailPage(url: galleryURL, pageNum: pageNum)) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .tryMap(Parser.parsePreviewURLs).mapError(mapAppError).eraseToAnyPublisher() + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + .tryMap(Parser.parsePreviewURLs) + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -445,8 +517,11 @@ struct MPVKeysRequest: Request { var publisher: AnyPublisher<(String, [Int: String]), AppError> { URLSession.shared.dataTaskPublisher(for: mpvURL) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .tryMap(Parser.parseMPVKeys).mapError(mapAppError).eraseToAnyPublisher() + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + .tryMap(Parser.parseMPVKeys) + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -456,8 +531,11 @@ struct ThumbnailURLsRequest: Request { var publisher: AnyPublisher<[Int: URL], AppError> { URLSession.shared.dataTaskPublisher(for: URLUtil.detailPage(url: galleryURL, pageNum: pageNum)) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .tryMap(Parser.parseThumbnailURLs).mapError(mapAppError).eraseToAnyPublisher() + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + .tryMap(Parser.parseThumbnailURLs) + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -467,11 +545,13 @@ struct GalleryNormalImageURLsRequest: Request { var publisher: AnyPublisher<([Int: URL], [Int: URL]), AppError> { thumbnailURLs.publisher .flatMap { index, url in - URLSession.shared.dataTaskPublisher(for: url).genericRetry() + URLSession.shared.dataTaskPublisher(for: url) + .genericRetry() .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } .tryMap { try Parser.parseGalleryNormalImageURL(doc: $0, index: index) } } - .collect().map { tuples in + .collect() + .map { tuples in var imageURLs = [Int: URL]() var originalImageURLs = [Int: URL]() for (index, imageURL, originalImageURL) in tuples { @@ -480,7 +560,8 @@ struct GalleryNormalImageURLsRequest: Request { } return (imageURLs, originalImageURLs) } - .mapError(mapAppError).eraseToAnyPublisher() + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -492,8 +573,11 @@ struct GalleryNormalImageURLRefetchRequest: Request { let storedImageURL: URL var publisher: AnyPublisher<([Int: URL], HTTPURLResponse?), AppError> { - storedThumbnailURL().flatMap(renewThumbnailURL).flatMap(imageURL) - .genericRetry().map { imageURL1, imageURL2, response in + storedThumbnailURL() + .flatMap(renewThumbnailURL) + .flatMap(imageURL) + .genericRetry() + .map { imageURL1, imageURL2, response in ([index: imageURL1 != storedImageURL ? imageURL1 : imageURL2], response) } .eraseToAnyPublisher() @@ -501,12 +585,16 @@ struct GalleryNormalImageURLRefetchRequest: Request { func storedThumbnailURL() -> AnyPublisher { if let thumbnailURL = thumbnailURL { - return Just(thumbnailURL).setFailureType(to: AppError.self).eraseToAnyPublisher() + return Just(thumbnailURL) + .setFailureType(to: AppError.self) + .eraseToAnyPublisher() } else { return URLSession.shared.dataTaskPublisher(for: URLUtil.detailPage(url: galleryURL, pageNum: pageNum)) - .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) }.tryMap(Parser.parseThumbnailURLs) + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + .tryMap(Parser.parseThumbnailURLs) .compactMap({ thumbnailURLs in thumbnailURLs[index] }) - .mapError(mapAppError).eraseToAnyPublisher() + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -518,7 +606,8 @@ struct GalleryNormalImageURLRefetchRequest: Request { let imageURL = try Parser.parseGalleryNormalImageURL(doc: $0, index: index).1 return (stored.appending(queryItems: [.skipServerIdentifier: identifier]), imageURL) } - .mapError(mapAppError).eraseToAnyPublisher() + .mapError(mapAppError) + .eraseToAnyPublisher() } func imageURL(thumbnailURL: URL, anotherImageURL: URL) @@ -533,7 +622,8 @@ struct GalleryNormalImageURLRefetchRequest: Request { .map { imageURL, response in (anotherImageURL, imageURL.1, response) } - .mapError(mapAppError).eraseToAnyPublisher() + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -546,8 +636,11 @@ struct GalleryMPVImageURLRequest: Request { var publisher: AnyPublisher<(URL, URL?, String), AppError> { var params: [String: Any] = [ - "method": "imagedispatch", "gid": gid, - "page": index, "imgkey": mpvImageKey, "mpvkey": mpvKey + "method": "imagedispatch", + "gid": gid, + "page": index, + "imgkey": mpvImageKey, + "mpvkey": mpvKey ] if let skipServerIdentifier = skipServerIdentifier { params["nl"] = skipServerIdentifier @@ -555,11 +648,12 @@ struct GalleryMPVImageURLRequest: Request { var request = URLRequest(url: Defaults.URL.api) request.httpMethod = "POST" - request.httpBody = try? JSONSerialization - .data(withJSONObject: params, options: []) + request.httpBody = try? JSONSerialization.data(withJSONObject: params, options: []) return URLSession.shared.dataTaskPublisher(for: request) - .genericRetry().map(\.data).tryMap { data in + .genericRetry() + .map(\.data) + .tryMap { data in guard let dict = try JSONSerialization .jsonObject(with: data) as? [String: Any], let imageURLString = dict["i"] as? String, @@ -568,15 +662,14 @@ struct GalleryMPVImageURLRequest: Request { else { throw AppError.parseFailed } if let originalImageURLStringSlice = dict["lf"] as? String { - let originalImageURL = Defaults.URL.host.appendingPathComponent( - originalImageURLStringSlice - ) + let originalImageURL = Defaults.URL.host.appendingPathComponent(originalImageURLStringSlice) return (imageURL, originalImageURL, skipServerIdentifier) } else { return (imageURL, nil, skipServerIdentifier) } } - .mapError(mapAppError).eraseToAnyPublisher() + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -586,7 +679,10 @@ struct DataRequest: Request { var publisher: AnyPublisher { URLSession.shared.dataTaskPublisher(for: url) - .genericRetry().map(\.data).mapError(mapAppError).eraseToAnyPublisher() + .genericRetry() + .map(\.data) + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -597,40 +693,45 @@ struct LoginRequest: Request { var publisher: AnyPublisher { let params: [String: String] = [ - "b": "d", "bt": "1-1", "CookieDate": "1", - "UserName": username, "PassWord": password, + "b": "d", + "bt": "1-1", + "CookieDate": "1", + "UserName": username, + "PassWord": password, "ipb_login_submit": "Login!" ] var request = URLRequest(url: Defaults.URL.login) request.httpMethod = "POST" - request.httpBody = params.dictString() - .urlEncoded.data(using: .utf8) + request.httpBody = params.dictString().urlEncoded.data(using: .utf8) request.setURLEncodedContentType() return URLSession.shared.dataTaskPublisher(for: request) - .genericRetry().map { - $0.response as? HTTPURLResponse - } - .mapError(mapAppError).eraseToAnyPublisher() + .genericRetry() + .map { $0.response as? HTTPURLResponse } + .mapError(mapAppError) + .eraseToAnyPublisher() } } struct IgneousRequest: Request { var publisher: AnyPublisher { URLSession.shared.dataTaskPublisher(for: Defaults.URL.exhentai) - .genericRetry().compactMap { - $0.response as? HTTPURLResponse - } - .mapError(mapAppError).eraseToAnyPublisher() + .genericRetry() + .compactMap { $0.response as? HTTPURLResponse } + .mapError(mapAppError) + .eraseToAnyPublisher() } } struct VerifyEhProfileRequest: Request { var publisher: AnyPublisher<(Int?, Bool), AppError> { URLSession.shared.dataTaskPublisher(for: Defaults.URL.uConfig) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .tryMap(Parser.parseProfileIndex).mapError(mapAppError).eraseToAnyPublisher() + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + .tryMap(Parser.parseProfileIndex) + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -654,21 +755,26 @@ struct EhProfileRequest: Request { var request = URLRequest(url: Defaults.URL.uConfig) request.httpMethod = "POST" - request.httpBody = params.dictString() - .urlEncoded.data(using: .utf8) + request.httpBody = params.dictString().urlEncoded.data(using: .utf8) request.setURLEncodedContentType() return URLSession.shared.dataTaskPublisher(for: request) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .tryMap(Parser.parseEhSetting).mapError(mapAppError).eraseToAnyPublisher() + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + .tryMap(Parser.parseEhSetting) + .mapError(mapAppError) + .eraseToAnyPublisher() } } struct EhSettingRequest: Request { var publisher: AnyPublisher { URLSession.shared.dataTaskPublisher(for: Defaults.URL.uConfig) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .tryMap(Parser.parseEhSetting).mapError(mapAppError).eraseToAnyPublisher() + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + .tryMap(Parser.parseEhSetting) + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -719,6 +825,9 @@ struct SubmitEhSettingChangesRequest: Request { } } + if let showFilteredRemovalCount = ehSetting.showFilteredRemovalCount { + params["tf"] = showFilteredRemovalCount ? "0" : "1" + } if let useOriginalImages = ehSetting.useOriginalImages { params["oi"] = useOriginalImages ? "1" : "0" } @@ -734,13 +843,15 @@ struct SubmitEhSettingChangesRequest: Request { var request = URLRequest(url: url) request.httpMethod = "POST" - request.httpBody = params.dictString() - .urlEncoded.data(using: .utf8) + request.httpBody = params.dictString().urlEncoded.data(using: .utf8) request.setURLEncodedContentType() return URLSession.shared.dataTaskPublisher(for: request) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .tryMap(Parser.parseEhSetting).mapError(mapAppError).eraseToAnyPublisher() + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + .tryMap(Parser.parseEhSetting) + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -752,18 +863,21 @@ struct FavorGalleryRequest: Request { var publisher: AnyPublisher { let url = URLUtil.addFavorite(gid: gid, token: token) let params: [String: String] = [ - "favcat": "\(favIndex)", "favnote": "", - "apply": "Add to Favorites", "update": "1" + "favcat": "\(favIndex)", + "favnote": "", + "apply": "Add to Favorites", + "update": "1" ] var request = URLRequest(url: url) request.httpMethod = "POST" - request.httpBody = params.dictString() - .urlEncoded.data(using: .utf8) + request.httpBody = params.dictString().urlEncoded.data(using: .utf8) request.setURLEncodedContentType() return URLSession.shared.dataTaskPublisher(for: request) - .genericRetry().map { $0 }.mapError(mapAppError) + .genericRetry() + .map { $0 } + .mapError(mapAppError) .eraseToAnyPublisher() } } @@ -773,17 +887,20 @@ struct UnfavorGalleryRequest: Request { var publisher: AnyPublisher { let params: [String: String] = [ - "ddact": "delete", "modifygids[]": gid, "apply": "Apply" + "ddact": "delete", + "modifygids[]": gid, + "apply": "Apply" ] var request = URLRequest(url: Defaults.URL.favorites) request.httpMethod = "POST" - request.httpBody = params.dictString() - .urlEncoded.data(using: .utf8) + request.httpBody = params.dictString().urlEncoded.data(using: .utf8) request.setURLEncodedContentType() return URLSession.shared.dataTaskPublisher(for: request) - .genericRetry().map { $0 }.mapError(mapAppError) + .genericRetry() + .map { $0 } + .mapError(mapAppError) .eraseToAnyPublisher() } } @@ -799,13 +916,15 @@ struct SendDownloadCommandRequest: Request { var request = URLRequest(url: archiveURL) request.httpMethod = "POST" - request.httpBody = params.dictString() - .urlEncoded.data(using: .utf8) + request.httpBody = params.dictString().urlEncoded.data(using: .utf8) request.setURLEncodedContentType() return URLSession.shared.dataTaskPublisher(for: request) - .genericRetry().tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .tryMap(Parser.parseDownloadCommandResponse).mapError(mapAppError).eraseToAnyPublisher() + .genericRetry() + .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } + .tryMap(Parser.parseDownloadCommandResponse) + .mapError(mapAppError) + .eraseToAnyPublisher() } } @@ -818,18 +937,22 @@ struct RateGalleryRequest: Request { var publisher: AnyPublisher { let params: [String: Any] = [ - "method": "rategallery", "apiuid": apiuid, - "apikey": apikey, "gid": gid, - "token": token, "rating": rating + "method": "rategallery", + "apiuid": apiuid, + "apikey": apikey, + "gid": gid, + "token": token, + "rating": rating ] var request = URLRequest(url: Defaults.URL.api) request.httpMethod = "POST" - request.httpBody = try? JSONSerialization - .data(withJSONObject: params, options: []) + request.httpBody = try? JSONSerialization.data(withJSONObject: params, options: []) return URLSession.shared.dataTaskPublisher(for: request) - .genericRetry().map { $0 }.mapError(mapAppError) + .genericRetry() + .map { $0 } + .mapError(mapAppError) .eraseToAnyPublisher() } } @@ -840,16 +963,19 @@ struct CommentGalleryRequest: Request { var publisher: AnyPublisher { let fixedContent = content.replacingOccurrences(of: "\n", with: "%0A") - let params: [String: String] = ["commenttext_new": fixedContent] + let params: [String: String] = [ + "commenttext_new": fixedContent + ] var request = URLRequest(url: galleryURL) request.httpMethod = "POST" - request.httpBody = params.dictString() - .urlEncoded.data(using: .utf8) + request.httpBody = params.dictString().urlEncoded.data(using: .utf8) request.setURLEncodedContentType() return URLSession.shared.dataTaskPublisher(for: request) - .genericRetry().map { $0 }.mapError(mapAppError) + .genericRetry() + .map { $0 } + .mapError(mapAppError) .eraseToAnyPublisher() } } @@ -862,17 +988,19 @@ struct EditGalleryCommentRequest: Request { var publisher: AnyPublisher { let fixedContent = content.replacingOccurrences(of: "\n", with: "%0A") let params: [String: String] = [ - "edit_comment": commentID, "commenttext_edit": fixedContent + "edit_comment": commentID, + "commenttext_edit": fixedContent ] var request = URLRequest(url: galleryURL) request.httpMethod = "POST" - request.httpBody = params.dictString() - .urlEncoded.data(using: .utf8) + request.httpBody = params.dictString().urlEncoded.data(using: .utf8) request.setURLEncodedContentType() return URLSession.shared.dataTaskPublisher(for: request) - .genericRetry().map { $0 }.mapError(mapAppError) + .genericRetry() + .map { $0 } + .mapError(mapAppError) .eraseToAnyPublisher() } } @@ -887,18 +1015,23 @@ struct VoteGalleryCommentRequest: Request { var publisher: AnyPublisher { let params: [String: Any] = [ - "method": "votecomment", "apiuid": apiuid, - "apikey": apikey, "gid": gid, "token": token, - "comment_id": commentID, "comment_vote": commentVote + "method": "votecomment", + "apiuid": apiuid, + "apikey": apikey, + "gid": gid, + "token": token, + "comment_id": commentID, + "comment_vote": commentVote ] var request = URLRequest(url: Defaults.URL.api) request.httpMethod = "POST" - request.httpBody = try? JSONSerialization - .data(withJSONObject: params, options: []) + request.httpBody = try? JSONSerialization.data(withJSONObject: params, options: []) return URLSession.shared.dataTaskPublisher(for: request) - .genericRetry().map { $0 }.mapError(mapAppError) + .genericRetry() + .map { $0 } + .mapError(mapAppError) .eraseToAnyPublisher() } } @@ -913,18 +1046,23 @@ struct VoteGalleryTagRequest: Request { var publisher: AnyPublisher { let params: [String: Any] = [ - "method": "taggallery", "apiuid": apiuid, - "apikey": apikey, "gid": gid, "token": token, - "tags": tag, "vote": vote + "method": "taggallery", + "apiuid": apiuid, + "apikey": apikey, + "gid": gid, + "token": token, + "tags": tag, + "vote": vote ] var request = URLRequest(url: Defaults.URL.api) request.httpMethod = "POST" - request.httpBody = try? JSONSerialization - .data(withJSONObject: params, options: []) + request.httpBody = try? JSONSerialization.data(withJSONObject: params, options: []) return URLSession.shared.dataTaskPublisher(for: request) - .genericRetry().map { $0 }.mapError(mapAppError) + .genericRetry() + .map { $0 } + .mapError(mapAppError) .eraseToAnyPublisher() } } diff --git a/EhPanda/View/Favorites/FavoritesStore.swift b/EhPanda/View/Favorites/FavoritesStore.swift index 383889ff..4fce53e6 100644 --- a/EhPanda/View/Favorites/FavoritesStore.swift +++ b/EhPanda/View/Favorites/FavoritesStore.swift @@ -91,6 +91,7 @@ struct FavoritesEnvironment { let databaseClient: DatabaseClient let clipboardClient: ClipboardClient let appDelegateClient: AppDelegateClient + let userDefaultsClient: UserDefaultsClient let uiApplicationClient: UIApplicationClient } @@ -180,13 +181,21 @@ let favoritesReducer = Reducer] = [ environment.databaseClient.cacheGalleries(galleries).fireAndForget() ] - if galleries.isEmpty, pageNumber.current < pageNumber.maximum { + if galleries.isEmpty, pageNumber.hasNextPage { effects.append(.init(value: .fetchMoreGalleries)) } else if !galleries.isEmpty { state.rawLoadingState[type] = .idle diff --git a/EhPanda/View/Setting/AppearanceSettingView.swift b/EhPanda/View/Setting/AppearanceSettingView.swift index 3bdea1cd..58fb82a4 100644 --- a/EhPanda/View/Setting/AppearanceSettingView.swift +++ b/EhPanda/View/Setting/AppearanceSettingView.swift @@ -11,6 +11,7 @@ import ComposableArchitecture struct AppearanceSettingView: View { private let store: Store @ObservedObject private var viewStore: ViewStore + @Binding private var preferredColorScheme: PreferredColorScheme @Binding private var accentColor: Color @Binding private var appIconType: AppIconType @@ -21,9 +22,12 @@ struct AppearanceSettingView: View { init( store: Store, - preferredColorScheme: Binding, accentColor: Binding, - appIconType: Binding, listDisplayMode: Binding, - showsTagsInList: Binding, listTagsNumberMaximum: Binding, + preferredColorScheme: Binding, + accentColor: Binding, + appIconType: Binding, + listDisplayMode: Binding, + showsTagsInList: Binding, + listTagsNumberMaximum: Binding, displaysJapaneseTitle: Binding ) { self.store = store @@ -45,41 +49,47 @@ struct AppearanceSettingView: View { selection: $preferredColorScheme ) { ForEach(PreferredColorScheme.allCases) { colorScheme in - Text(colorScheme.value).tag(colorScheme) + Text(colorScheme.value) + .tag(colorScheme) } } .pickerStyle(.menu) + ColorPicker(R.string.localizable.appearanceSettingViewTitleTintColor(), selection: $accentColor) + Button(R.string.localizable.appearanceSettingViewButtonAppIcon()) { viewStore.send(.setNavigation(.appIcon)) } - .foregroundStyle(.primary).withArrow() + .foregroundStyle(.primary) + .withArrow() } Section(R.string.localizable.appearanceSettingViewSectionTitleList()) { - HStack { - Text(R.string.localizable.appearanceSettingViewTitleDisplayMode()) - Spacer() - Picker( - selection: $listDisplayMode, - label: Text(listDisplayMode.value), - content: { - ForEach(ListDisplayMode.allCases) { listMode in - Text(listMode.value).tag(listMode) - } + Picker( + R.string.localizable.appearanceSettingViewTitleDisplayMode(), + selection: $listDisplayMode, + content: { + ForEach(ListDisplayMode.allCases) { listMode in + Text(listMode.value) + .tag(listMode) } - ) - } + } + ) .pickerStyle(.menu) + Toggle(isOn: $showsTagsInList) { Text(R.string.localizable.appearanceSettingViewTitleShowsTagsInList()) } + Picker( R.string.localizable.appearanceSettingViewTitleMaximumNumberOfTags(), selection: $listTagsNumberMaximum ) { - Text(R.string.localizable.appearanceSettingViewMenuTitleInfite()).tag(0) + Text(R.string.localizable.appearanceSettingViewMenuTitleInfite()) + .tag(0) + ForEach(Array(stride(from: 5, through: 20, by: 5)), id: \.self) { num in - Text("\(num)").tag(num) + Text("\(num)") + .tag(num) } } .pickerStyle(.menu) @@ -95,6 +105,7 @@ struct AppearanceSettingView: View { .background(navigationLink) .navigationTitle(R.string.localizable.appearanceSettingViewTitleAppearance()) } + private var navigationLink: some View { NavigationLink(unwrapping: viewStore.binding(\.$route), case: /AppearanceSettingState.Route.appIcon) { _ in AppIconView(appIconType: $appIconType) @@ -141,14 +152,23 @@ private struct AppIconRow: View { } var body: some View { - HStack { - Image(uiImage: .init(named: filename, in: .main, with: nil) ?? .init()) - .resizable().scaledToFit().frame(width: 60, height: 60).cornerRadius(12) - .padding(.vertical, 10).padding(.trailing, 20) + HStack(spacing: 20) { + UIImage(named: filename, in: .main, with: nil) + .map(Image.init)? + .resizable() + .scaledToFit() + .frame(width: 60, height: 60) + .clipShape(RoundedRectangle(cornerRadius: 15, style: .continuous)) + .padding(.vertical, 10) + Text(iconName) + Spacer() + Image(systemSymbol: .checkmarkCircleFill) - .opacity(isSelected ? 1 : 0).foregroundStyle(.tint).imageScale(.large) + .opacity(isSelected ? 1 : 0) + .foregroundStyle(.tint) + .imageScale(.large) } } } @@ -161,6 +181,7 @@ enum AppIconType: Int, Codable, Identifiable, CaseIterable { case ukiyoe case developer case standWithUkraine2022 + case notMyPresidnet } extension AppIconType { @@ -168,24 +189,37 @@ extension AppIconType { switch self { case .default: return R.string.localizable.enumAppIconTypeValueDefault() + case .ukiyoe: return R.string.localizable.enumAppIconTypeValueUkiyoe() + case .developer: return R.string.localizable.enumAppIconTypeValueDeveloper() + case .standWithUkraine2022: return R.string.localizable.enumAppIconTypeValueStandWithUkraine2022() + + case .notMyPresidnet: + return R.string.localizable.enumAppIconTypeValueNotMyPresident() } } + var filename: String { switch self { case .default: return "AppIcon_Default" + case .ukiyoe: return "AppIcon_Ukiyoe" + case .developer: return "AppIcon_Developer" + case .standWithUkraine2022: return "AppIcon_StandWithUkraine2022" + + case .notMyPresidnet: + return "AppIcon_NotMyPresident" } } } diff --git a/EhPanda/View/Setting/Support/EhSettingView.swift b/EhPanda/View/Setting/Support/EhSettingView.swift index baf29e80..7a5983f3 100644 --- a/EhPanda/View/Setting/Support/EhSettingView.swift +++ b/EhPanda/View/Setting/Support/EhSettingView.swift @@ -26,9 +26,11 @@ struct EhSettingView: View { ZStack { // workaround: Stay if-else approach if viewStore.loadingState == .loading || viewStore.submittingState == .loading { - LoadingView().tint(nil) + LoadingView() + .tint(nil) } else if case .failed(let error) = viewStore.loadingState { - ErrorView(error: error, action: { viewStore.send(.fetchEhSetting) }).tint(nil) + ErrorView(error: error, action: { viewStore.send(.fetchEhSetting) }) + .tint(nil) } // Using `Binding.init` will crash the app else if let ehSetting = Binding(unwrapping: viewStore.binding(\.$ehSetting)), @@ -49,7 +51,8 @@ struct EhSettingView: View { } } .sheet(unwrapping: viewStore.binding(\.$route), case: /EhSettingState.Route.webView) { route in - WebView(url: route.wrappedValue).autoBlur(radius: blurRadius) + WebView(url: route.wrappedValue) + .autoBlur(radius: blurRadius) } .toolbar(content: toolbar) .navigationTitle(R.string.localizable.ehSettingViewTitleHostSettings(AppUtil.galleryHost.rawValue)) @@ -60,7 +63,8 @@ struct EhSettingView: View { Group { EhProfileSection( route: viewStore.binding(\.$route), - ehSetting: ehSetting, ehProfile: ehProfile, + ehSetting: ehSetting, + ehProfile: ehProfile, editingProfileName: viewStore.binding(\.$editingProfileName), deleteAction: { if let value = viewStore.ehProfile?.value { @@ -72,6 +76,7 @@ struct EhSettingView: View { deleteDialogAction: { viewStore.send(.setNavigation(.deleteProfile)) }, performEhProfileAction: { viewStore.send(.performAction($0, $1, $2)) } ) + ImageLoadSettingsSection(ehSetting: ehSetting) ImageSizeSettingsSection(ehSetting: ehSetting) GalleryNameDisplaySection(ehSetting: ehSetting) @@ -84,6 +89,7 @@ struct EhSettingView: View { } Group { TagWatchingThresholdSection(ehSetting: ehSetting) + FilteredRemovalCountSection(ehSetting: ehSetting) ExcludedLanguagesSection(ehSetting: ehSetting) ExcludedUploadersSection(ehSetting: ehSetting) SearchResultCountSection(ehSetting: ehSetting) @@ -92,9 +98,9 @@ struct EhSettingView: View { ViewportOverrideSection(ehSetting: ehSetting) GalleryCommentsSection(ehSetting: ehSetting) GalleryTagsSection(ehSetting: ehSetting) - GalleryPageNumberingSection(ehSetting: ehSetting) } Group { + GalleryPageNumberingSection(ehSetting: ehSetting) OriginalImagesSection(ehSetting: ehSetting) MultiplePageViewerSection(ehSetting: ehSetting) } @@ -111,6 +117,7 @@ struct EhSettingView: View { } .disabled(bypassesSNIFiltering) } + ToolbarItem(placement: .confirmationAction) { Button { viewStore.send(.submitChanges) @@ -119,13 +126,12 @@ struct EhSettingView: View { } .disabled(viewStore.ehSetting == nil) } + ToolbarItem(placement: .keyboard) { - HStack { - Spacer() - Button(R.string.localizable.ehSettingViewToolbarItemButtonDone()) { - viewStore.send(.setKeyboardHidden) - } + Button(R.string.localizable.ehSettingViewToolbarItemButtonDone()) { + viewStore.send(.setKeyboardHidden) } + .frame(maxWidth: .infinity, alignment: .trailing) } } } @@ -162,21 +168,26 @@ private struct EhProfileSection: View { Section(R.string.localizable.ehSettingViewSectionTitleProfileSettings()) { Picker(R.string.localizable.ehSettingViewTitleSelectedProfile(), selection: $ehProfile) { ForEach(ehSetting.ehProfiles) { ehProfile in - Text(ehProfile.name).tag(ehProfile) + Text(ehProfile.name) + .tag(ehProfile) } } .pickerStyle(.menu) + if !ehProfile.isDefault { Button(R.string.localizable.ehSettingViewButtonSetAsDefault()) { performEhProfileAction(.default, nil, ehProfile.value) } + Button( R.string.localizable.ehSettingViewButtonDeleteProfile(), - role: .destructive, action: deleteDialogAction + role: .destructive, + action: deleteDialogAction ) .confirmationDialog( message: R.string.localizable.confirmationDialogTitleDelete(), - unwrapping: $route, case: /EhSettingState.Route.deleteProfile + unwrapping: $route, + case: /EhSettingState.Route.deleteProfile ) { Button( R.string.localizable.confirmationDialogButtonDelete(), @@ -189,15 +200,16 @@ private struct EhProfileSection: View { performEhProfileAction(nil, nil, $0.value) } .textCase(nil) + Section { - SettingTextField( - text: $editingProfileName, width: nil, alignment: .leading, background: .clear - ) - .focused($isFocused) + SettingTextField(text: $editingProfileName, width: nil, alignment: .leading, background: .clear) + .focused($isFocused) + Button(R.string.localizable.ehSettingViewButtonRename()) { performEhProfileAction(.rename, editingProfileName, ehProfile.value) } .disabled(isFocused) + if ehSetting.isCapableOfCreatingNewProfile { Button(R.string.localizable.ehSettingViewButtonCreateNew()) { performEhProfileAction(.create, editingProfileName, ehProfile.value) @@ -217,21 +229,24 @@ private struct ImageLoadSettingsSection: View { } var body: some View { - Section( - header: Text(R.string.localizable.ehSettingViewSectionTitleImageLoadSettings()), - footer: Text(ehSetting.loadThroughHathSetting.description) - ) { + Section { Picker( R.string.localizable.ehSettingViewTitleLoadImagesThroughTheHathNetwork(), selection: $ehSetting.loadThroughHathSetting ) { ForEach(ehSetting.capableLoadThroughHathSettings) { setting in - Text(setting.value).tag(setting) + Text(setting.value) + .tag(setting) } } .pickerStyle(.menu) + } header: { + Text(R.string.localizable.ehSettingViewSectionTitleImageLoadSettings()) + } footer: { + Text(ehSetting.loadThroughHathSetting.description) } .textCase(nil) + Section( R.string.localizable.ehSettingViewDescriptionBrowsingCountry( ehSetting.localizedLiteralBrowsingCountry ?? ehSetting.literalBrowsingCountry @@ -240,7 +255,8 @@ private struct ImageLoadSettingsSection: View { ) { Picker(R.string.localizable.ehSettingViewTitleBrowsingCountry(), selection: $ehSetting.browsingCountry) { ForEach(EhSetting.BrowsingCountry.allCases) { country in - Text(country.name).tag(country) + Text(country.name) + .tag(country) .foregroundColor(country == ehSetting.browsingCountry ? .accentColor : .primary) } } @@ -258,24 +274,29 @@ private struct ImageSizeSettingsSection: View { } var body: some View { - Section( - header: Text(R.string.localizable.ehSettingViewSectionTitleImageSizeSettings()).newlineBold() - + Text(R.string.localizable.ehSettingViewDescriptionImageResolution()) - ) { + Section { Picker(R.string.localizable.ehSettingViewTitleImageResolution(), selection: $ehSetting.imageResolution) { ForEach(ehSetting.capableImageResolutions) { setting in - Text(setting.value).tag(setting) + Text(setting.value) + .tag(setting) } } .pickerStyle(.menu) + } header: { + Text(R.string.localizable.ehSettingViewSectionTitleImageSizeSettings()) + .newlineBold() + .appending(R.string.localizable.ehSettingViewDescriptionImageResolution()) } .textCase(nil) + Section(R.string.localizable.ehSettingViewDescriptionImageSize()) { Text(R.string.localizable.ehSettingViewTitleImageSize()) + ValuePicker( title: R.string.localizable.ehSettingViewTitleHorizontal(), value: $ehSetting.imageSizeWidth, range: 0...65535, unit: "px" ) + ValuePicker( title: R.string.localizable.ehSettingViewTitleVertical(), value: $ehSetting.imageSizeHeight, range: 0...65535, unit: "px" @@ -294,16 +315,18 @@ private struct GalleryNameDisplaySection: View { } var body: some View { - Section( - header: Text(R.string.localizable.ehSettingViewSectionTitleGalleryNameDisplay()).newlineBold() - + Text(R.string.localizable.ehSettingViewDescriptionGalleryName()) - ) { + Section { Picker(R.string.localizable.ehSettingViewTitleGalleryName(), selection: $ehSetting.galleryName) { ForEach(EhSetting.GalleryName.allCases) { name in - Text(name.value).tag(name) + Text(name.value) + .tag(name) } } .pickerStyle(.menu) + } header: { + Text(R.string.localizable.ehSettingViewSectionTitleGalleryNameDisplay()) + .newlineBold() + .appending(R.string.localizable.ehSettingViewDescriptionGalleryName()) } .textCase(nil) } @@ -318,16 +341,18 @@ private struct ArchiverSettingsSection: View { } var body: some View { - Section( - header: Text(R.string.localizable.ehSettingViewSectionTitleArchiverSettings()).newlineBold() - + Text(R.string.localizable.ehSettingViewDescriptionArchiverBehavior()) - ) { + Section { Picker(R.string.localizable.ehSettingViewTitleArchiverBehavior(), selection: $ehSetting.archiverBehavior) { ForEach(EhSetting.ArchiverBehavior.allCases) { behavior in - Text(behavior.value).tag(behavior) + Text(behavior.value) + .tag(behavior) } } .pickerStyle(.menu) + } header: { + Text(R.string.localizable.ehSettingViewSectionTitleArchiverSettings()) + .newlineBold() + .appending(R.string.localizable.ehSettingViewDescriptionArchiverBehavior()) } .textCase(nil) } @@ -346,18 +371,21 @@ private struct FrontPageSettingsSection: View { } var body: some View { - Section( - header: Text(R.string.localizable.ehSettingViewSectionTitleFrontPageSettings()).newlineBold() - + Text(R.string.localizable.ehSettingViewDescriptionDisplayMode()) - ) { + Section { Picker(R.string.localizable.ehSettingViewTitleDisplayMode(), selection: $ehSetting.displayMode) { ForEach(EhSetting.DisplayMode.allCases) { mode in - Text(mode.value).tag(mode) + Text(mode.value) + .tag(mode) } } .pickerStyle(.menu) + } header: { + Text(R.string.localizable.ehSettingViewSectionTitleFrontPageSettings()) + .newlineBold() + .appending(R.string.localizable.ehSettingViewDescriptionDisplayMode()) } .textCase(nil) + Section(R.string.localizable.ehSettingViewDescriptionGalleryCategory()) { CategoryView(bindings: categoryBindings) } @@ -381,29 +409,33 @@ private struct FavoritesSection: View { } var body: some View { - Section( - header: Text(R.string.localizable.ehSettingViewSectionTitleFavorites()).newlineBold() - + Text(R.string.localizable.ehSettingViewDescriptionFavoriteCategories()) - ) { + Section { ForEach(tuples, id: \.0) { category, nameBinding in HStack(spacing: 30) { - Circle().foregroundColor(category.color).frame(width: 10) - SettingTextField( - text: nameBinding, width: nil, alignment: .leading, background: .clear - ) - .focused($isFocused) + Circle() + .foregroundColor(category.color) + .frame(width: 10) + + SettingTextField(text: nameBinding, width: nil, alignment: .leading, background: .clear) + .focused($isFocused) } .padding(.leading) } + } header: { + Text(R.string.localizable.ehSettingViewSectionTitleFavorites()) + .newlineBold() + .appending(R.string.localizable.ehSettingViewDescriptionFavoriteCategories()) } .textCase(nil) + Section(R.string.localizable.ehSettingViewDescriptionFavoritesSortOrder()) { Picker( R.string.localizable.ehSettingViewTitleFavoritesSortOrder(), selection: $ehSetting.favoritesSortOrder ) { ForEach(EhSetting.FavoritesSortOrder.allCases) { order in - Text(order.value).tag(order) + Text(order.value) + .tag(order) } } .pickerStyle(.menu) @@ -422,19 +454,19 @@ private struct RatingsSection: View { } var body: some View { - Section( - header: Text(R.string.localizable.ehSettingViewSectionTitleRatings()).newlineBold() - + Text(R.string.localizable.ehSettingViewDescriptionRatingsColor()) - ) { - HStack { - Text(R.string.localizable.ehSettingViewTitleRatingsColor()) - Spacer() + Section { + LabeledContent(R.string.localizable.ehSettingViewTitleRatingsColor()) { SettingTextField( - text: $ehSetting.ratingsColor, promptText: R.string.localizable - .ehSettingViewPromtRatingsColor(), width: 80 + text: $ehSetting.ratingsColor, + promptText: R.string.localizable.ehSettingViewPromtRatingsColor(), + width: 80 ) .focused($isFocused) } + } header: { + Text(R.string.localizable.ehSettingViewSectionTitleRatings()) + .newlineBold() + .appending(R.string.localizable.ehSettingViewDescriptionRatingsColor()) } .textCase(nil) } @@ -455,11 +487,12 @@ private struct TagNamespacesSection: View { } var body: some View { - Section( - header: Text(R.string.localizable.ehSettingViewSectionTitleTagsNamespaces()).newlineBold() - + Text(R.string.localizable.ehSettingViewDescriptionTagsNamespaces()) - ) { + Section { ExcludeView(tuples: tuples) + } header: { + Text(R.string.localizable.ehSettingViewSectionTitleTagsNamespaces()) + .newlineBold() + .appending(R.string.localizable.ehSettingViewDescriptionTagsNamespaces()) } .textCase(nil) } @@ -473,26 +506,34 @@ private struct ExcludeView: View { } private let gridItems = [ - GridItem(.adaptive( - minimum: DeviceUtil.isPadWidth ? 100 : 80, maximum: 100 - )) + GridItem( + .adaptive( + minimum: DeviceUtil.isPadWidth ? 100 : 80, + maximum: 100 + ) + ) ] var body: some View { LazyVGrid(columns: gridItems) { ForEach(tuples, id: \.0) { text, isExcluded in ZStack { - Text(text).bold().opacity(isExcluded.wrappedValue ? 0 : 1) + Text(text) + .bold() + .opacity(isExcluded.wrappedValue ? 0 : 1) ZStack { Text(text) + let width = (CGFloat(text.count) * 8) + 8 - let line = Rectangle().frame(width: width, height: 1) + let line = Rectangle() + .frame(width: width, height: 1) VStack(spacing: 2) { line line } } - .foregroundColor(.red).opacity(isExcluded.wrappedValue ? 1 : 0) + .foregroundColor(.red) + .opacity(isExcluded.wrappedValue ? 1 : 0) } .onTapGesture { HapticUtil.generateFeedback(style: .soft) @@ -513,14 +554,15 @@ private struct TagFilteringThresholdSection: View { } var body: some View { - Section( - header: Text(R.string.localizable.ehSettingViewSectionTitleTagFilteringThreshold()).newlineBold() - + Text(R.string.localizable.ehSettingViewDescriptionTagFilteringThreshold()) - ) { + Section { ValuePicker( title: R.string.localizable.ehSettingViewTitleTagFilteringThreshold(), value: $ehSetting.tagFilteringThreshold, range: -9999...0 ) + } header: { + Text(R.string.localizable.ehSettingViewSectionTitleTagFilteringThreshold()) + .newlineBold() + .appending(R.string.localizable.ehSettingViewDescriptionTagFilteringThreshold()) } .textCase(nil) } @@ -535,19 +577,43 @@ private struct TagWatchingThresholdSection: View { } var body: some View { - Section( - header: Text(R.string.localizable.ehSettingViewSectionTitleTagWatchingThreshold()).newlineBold() - + Text(R.string.localizable.ehSettingViewDescriptionTagWatchingThreshold()) - ) { + Section { ValuePicker( title: R.string.localizable.ehSettingViewTitleTagWatchingThreshold(), value: $ehSetting.tagWatchingThreshold, range: 0...9999 ) + } header: { + Text(R.string.localizable.ehSettingViewSectionTitleTagWatchingThreshold()) + .newlineBold() + .appending(R.string.localizable.ehSettingViewDescriptionTagWatchingThreshold()) } .textCase(nil) } } +// MARK: FilteredRemovalCountSection +private struct FilteredRemovalCountSection: View { + @Binding private var ehSetting: EhSetting + + init(ehSetting: Binding) { + _ehSetting = ehSetting + } + + var body: some View { + if let showFilteredRemovalCountBinding = Binding($ehSetting.showFilteredRemovalCount) { + Section { + Toggle( + R.string.localizable.ehSettingViewTitleShowFilteredRemovalCount(), + isOn: showFilteredRemovalCountBinding + ) + } header: { + Text(R.string.localizable.ehSettingViewSectionTitleFilteredRemovalCount()).newlineBold() + + Text(R.string.localizable.ehSettingViewDescriptionFilteredRemovalCount()) + } + } + } +} + // MARK: ExcludedLanguagesSection private struct ExcludedLanguagesSection: View { @Binding private var ehSetting: EhSetting @@ -572,18 +638,22 @@ private struct ExcludedLanguagesSection: View { } var body: some View { - Section( - header: Text(R.string.localizable.ehSettingViewSectionTitleExcludedLanguages()).newlineBold() - + Text(R.string.localizable.ehSettingViewDescriptionExcludedLanguages()) - ) { + Section { HStack { - Text("").frame(width: DeviceUtil.windowW * 0.25) + Text("") + .frame(width: DeviceUtil.windowW * 0.25) + ForEach(EhSetting.ExcludedLanguagesCategory.allCases) { category in - Color.clear.overlay { - Text(category.value).lineLimit(1).font(.subheadline).fixedSize() - } + Color.clear + .overlay { + Text(category.value) + .lineLimit(1) + .font(.subheadline) + .fixedSize() + } } } + ForEach(0..<(languageBindings.count / 3) + 1, id: \.self) { index in ExcludeRow( title: languages[index], @@ -591,6 +661,10 @@ private struct ExcludedLanguagesSection: View { isFirstRow: index == 0 ) } + } header: { + Text(R.string.localizable.ehSettingViewSectionTitleExcludedLanguages()) + .newlineBold() + .appending(R.string.localizable.ehSettingViewDescriptionExcludedLanguages()) } .textCase(nil) } @@ -609,11 +683,13 @@ private struct ExcludeRow: View { var body: some View { HStack { - HStack { - Text(title).lineLimit(1).font(.subheadline).fixedSize() - Spacer() - } - .frame(width: DeviceUtil.windowW * 0.25) + Text(title) + .lineLimit(1) + .font(.subheadline) + .fixedSize() + .frame(maxWidth: .infinity, alignment: .leading) + .frame(width: DeviceUtil.windowW * 0.25) + ForEach(0.. Text { bold() + Text("\n") } + + func appending(_ string: some StringProtocol) -> Text { + self + Text(string) + } } struct EhSettingView_Previews: PreviewProvider { diff --git a/EhPanda/View/Support/Components/GenericList.swift b/EhPanda/View/Support/Components/GenericList.swift index 6b05d4dc..b5c13631 100644 --- a/EhPanda/View/Support/Components/GenericList.swift +++ b/EhPanda/View/Support/Components/GenericList.swift @@ -99,10 +99,10 @@ private struct DetailList: View { guard let pageNumber = pageNumber else { return false } let isLastGallery = gallery == galleries.last + let isPageNumberValid = pageNumber.hasNextPage let isLoadingStateIdle = footerLoadingState == .idle - let isPageNumberValid = pageNumber.current + 1 <= pageNumber.maximum - return isLastGallery && !isLoadingStateIdle && isPageNumberValid + return isLastGallery && isPageNumberValid && !isLoadingStateIdle } var body: some View { @@ -145,7 +145,7 @@ private struct WaterfallList: View { private var shouldShowFooter: Bool { guard let pageNumber = pageNumber else { return false } - let isPageNumberValid = pageNumber.current + 1 <= pageNumber.maximum + let isPageNumberValid = pageNumber.hasNextPage let isLoadingStateIdle = footerLoadingState == .idle return !isLoadingStateIdle && isPageNumberValid