diff --git a/.swiftlint.yml b/.swiftlint.yml index a4e2ac1..e06f801 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -10,8 +10,8 @@ identifier_name: min_length: 1 allowed_symbols: ["_"] file_length: - error: 10000 - warning: 3000 + error: 100000 + warning: 10000 cyclomatic_complexity: warning: 50 error: 100 diff --git a/ImageViewer/ViewController.swift b/ImageViewer/ViewController.swift index 9376e1b..b3dcd0e 100644 --- a/ImageViewer/ViewController.swift +++ b/ImageViewer/ViewController.swift @@ -1,6 +1,4 @@ -// TODO: Anilist support? // TODO: Properly avoid swallowing of input from UICollectionView used for scrolling -// TODO: Convert between state for normal and scrolling page turn import FlatBuffers import Foundation @@ -23,13 +21,13 @@ enum ReadProgress: Codable { case scroll(CGPoint) } -struct ProgressIndices { - var v: Int - var c: Int - var i: Int +struct ProgressIndices: Codable { + var v: Int = 0 + var c: Int = 0 + var i: Int = 0 } -enum PageTurnMode: Codable { +enum PageTurnMode: Codable, CaseIterable { case leftToRight case rightToLeft case scroll @@ -45,10 +43,13 @@ struct GlobalState: Codable { var comicName: String? } +/// nil indicates a default should be used, aka what is in settings. struct LocalState: Codable { - var progress: ReadProgress + var progress: ProgressIndices = .init() + var mode: PageTurnMode? var backgroundColor: String? - var useDefaultPageTurnMode: Bool + var aniListTracking: Bool = false + var scrollPos: CGPoint = .init() } struct Settings: Codable { @@ -64,7 +65,6 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { var readerView = UIView() @IBOutlet var scrollingCollectionView: UICollectionView! - var scrollPos: CGPoint! var hasSetContentOffset = false var pendingAlert: UIAlertController? @@ -73,14 +73,14 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { let dragRangeForFullBrightness: CGFloat = 300 var imageView = UIImageView() - var mode = PageTurnMode.leftToRight - var useDefaultPageTurnMode = true - var useDefaultBackgroundColor = true var metadataList: [URL: Metadata] = [:] + // directory name is key, indexPath.item can change. + var state: [String: LocalState] = [:] var metadata: Metadata! - var progress = ProgressIndices(v: 0, c: 0, i: 0) var currentPath: URL! var lastLayoutSize: CGSize = .zero + var aniListToken: String? = getAniListToken() + var currentItemIndex = 0 var leftTap: UITapGestureRecognizer! var rightTap: UITapGestureRecognizer! @@ -124,9 +124,9 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { if let alert = pendingAlert { present(alert, animated: false, completion: nil) } - if !hasSetContentOffset, scrollPos != nil { + if !hasSetContentOffset, let scrollPos = state[currentPath.lastPathComponent]?.scrollPos { let screenSize = UIScreen.main.bounds.size - var offset = scrollPos! + var offset = scrollPos if screenSize.width > screenSize.height { offset.y *= (screenSize.width / screenSize.height) } @@ -140,6 +140,20 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { } } + func getMode() -> PageTurnMode { + if let state = state[currentPath.lastPathComponent] { + return state.mode ?? settings.defaultPageTurnMode + } + return settings.defaultPageTurnMode + } + + func getProgress() -> ProgressIndices { + if let state = state[currentPath.lastPathComponent] { + return state.progress + } + return ProgressIndices() + } + func setup() { do { if try fileManager.contentsOfDirectory(atPath: documentsURL.path).isEmpty { @@ -168,7 +182,6 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { readerView.bottomAnchor.constraint(equalTo: view.bottomAnchor), readerView.widthAnchor.constraint(equalTo: view.widthAnchor), ]) - mode = settings.defaultPageTurnMode setupScrollingCollectionView() loadComics() setupImageView() @@ -283,7 +296,7 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { } metadataList[dir] = metadata loadLocalState() - let volCovP = ProgressIndices(v: progress.v, c: 0, i: 0) + let volCovP = ProgressIndices(v: state[currentPath.lastPathComponent]!.progress.v, c: 0, i: 0) let archivePath = getArchiveURL(volCovP.v) let archive = try Archive(url: archivePath, accessMode: .read) let filename = metadata.volumes[volCovP.v].chapters[volCovP.c].images[volCovP.i].filename! @@ -770,36 +783,30 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { } func saveLocalState() { + guard let currentDir = currentPath?.lastPathComponent else { print("did not save currentPath nil"); return } + guard let currentState = state[currentDir] else { print("did not save currentState nil"); return } + let progress = currentState.progress + let mode = currentState.mode let color = readerView.backgroundColor ?? .black + let aniListTracking = currentState.aniListTracking + let screenSize = UIScreen.main.bounds.size var scrollOffset = scrollingCollectionView.contentOffset if screenSize.width > screenSize.height { scrollOffset.y *= (screenSize.height / screenSize.width) } - var newProgress = - ReadProgress.leftToRight( - volumeIndex: progress.v, chapterIndex: progress.c, - imageIndex: progress.i - ) + // TODO: Calucate the offset based on progress, to allow seamless switching. + if mode != .scroll {} - switch mode { - case .leftToRight: - newProgress = ReadProgress.leftToRight( - volumeIndex: progress.v, chapterIndex: progress.c, - imageIndex: progress.i - ) - case .rightToLeft: newProgress = ReadProgress.rightToLeft( - volumeIndex: progress.v, chapterIndex: progress.c, - imageIndex: progress.i - ) - case .scroll: newProgress = ReadProgress.scroll(scrollOffset) - } queue.async { do { try JSONEncoder().encode( LocalState( - progress: newProgress, backgroundColor: self.convertColorToString(color), - useDefaultPageTurnMode: self.useDefaultPageTurnMode + progress: progress, + mode: mode, + backgroundColor: self.convertColorToString(color), + aniListTracking: aniListTracking, + scrollPos: scrollOffset ) ).write( to: self.currentPath.appendingPathComponent("state.json") @@ -811,58 +818,22 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { } func loadLocalState() { + if state[currentPath.lastPathComponent] != nil { return } do { let path = currentPath.appendingPathComponent("state.json").path if !fileManager.fileExists(atPath: path) { - progress = ProgressIndices(v: 0, c: 0, i: 0) - mode = settings.defaultPageTurnMode + state[currentPath.lastPathComponent] = LocalState() return } let json = try Data(String(contentsOfFile: path).utf8) let local = try JSONDecoder().decode(LocalState.self, from: json) - useDefaultPageTurnMode = local.useDefaultPageTurnMode - switch local.progress { - case let .leftToRight(volumeIndex, chapterIndex, imageIndex): - progress.v = volumeIndex - progress.c = chapterIndex - progress.i = imageIndex - mode = useDefaultPageTurnMode ? settings.defaultPageTurnMode : .leftToRight - case let .rightToLeft(volumeIndex, chapterIndex, imageIndex): - progress.v = volumeIndex - progress.c = chapterIndex - progress.i = imageIndex - mode = useDefaultPageTurnMode ? settings.defaultPageTurnMode : .rightToLeft - case let .scroll(point): - if scrollPos == nil { - scrollPos = point - let screenSize = UIScreen.main.bounds.size - var scrollOffset = point - if screenSize.width > screenSize.height { - scrollOffset.y *= (screenSize.width / screenSize.height) - } - if let indexPath = scrollingCollectionView.indexPathForItem(at: scrollOffset) { - var theProgress = ProgressIndices(v: 0, c: 0, i: 0) - for _ in 0 ..< indexPath.item { - theProgress = getProgressIndicesFromTurn( - turn: .next, progress: theProgress - ) - } - progress = theProgress - } - } - mode = useDefaultPageTurnMode ? settings.defaultPageTurnMode : .scroll - } - if let color = local.backgroundColor { - readerView.backgroundColor = convertStringToColor(color) - useDefaultBackgroundColor = false - } else { - readerView.backgroundColor = convertStringToColor(settings.defaultReadingBackgroundColor) - useDefaultBackgroundColor = true - } + state[currentPath.lastPathComponent] = local } catch let decodingError as DecodingError { - print(decodingError.errorDescription!) + print("Failed to decode state.json, setting default: \(decodingError.errorDescription ?? "")") + state[currentPath.lastPathComponent] = LocalState() } catch { - print("Unexpected error: \(error)") + print("Failed to decode state.json, setting default: \(error)") + state[currentPath.lastPathComponent] = LocalState() } } @@ -895,7 +866,9 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { for _ in 0 ..< indexPath.item { newProgress = getProgressIndicesFromTurn(turn: .next, progress: newProgress) } - progress = newProgress + guard let progress = state[currentPath.lastPathComponent]?.progress else { return } + aniListProgressUpdate(progress: progress, newProgress: newProgress) + state[currentPath.lastPathComponent]!.progress = newProgress } if scrollingCollectionView.isHidden == false { saveLocalState() @@ -906,11 +879,12 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { super.viewDidLayoutSubviews() guard view.bounds.size != lastLayoutSize else { return } lastLayoutSize = UIScreen.main.bounds.size - if scrollingCollectionView.isHidden == true - || mode != .scroll + if scrollingCollectionView.isHidden + || getMode() != .scroll || scrollingCollectionView.contentSize == .zero { if metadata == nil { return } + let progress = getProgress() imageLoader.loadImage( archiveURL: getArchiveURL(progress.v), filename: getImagePath(progress), @@ -931,6 +905,7 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { if let path = getPathFromComicName(name: name) { currentPath = path metadata = metadataList[path] + let mode = getMode() globalState.comicName = metadata.title saveGlobalState() @@ -1182,8 +1157,10 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { let point = gesture.location(in: comicCollectionView) guard let indexPath = comicCollectionView.indexPathForItem(at: point) else { return } + currentPath = comics[indexPath.item].path - showComicOptions(for: indexPath) + loadLocalState() + comicOptionLongPressed(indexPath) } @objc func handleTopTap() { @@ -1191,7 +1168,7 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { } @objc func handleLeftTap() { - switch mode { + switch getMode() { case .rightToLeft: changeImage(turn: .next) case .leftToRight: changeImage(turn: .previous) case .scroll: break @@ -1199,7 +1176,7 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { } @objc func handleRightTap() { - switch mode { + switch getMode() { case .rightToLeft: changeImage(turn: .previous) case .leftToRight: changeImage(turn: .next) case .scroll: break @@ -1209,13 +1186,13 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { @objc func handleSwipe(_ gesture: UISwipeGestureRecognizer) { switch gesture.direction { case .left: - switch mode { + switch getMode() { case .rightToLeft: changeImage(turn: .previous) case .leftToRight: changeImage(turn: .next) case .scroll: break } case .right: - switch mode { + switch getMode() { case .rightToLeft: changeImage(turn: .next) case .leftToRight: changeImage(turn: .previous) case .scroll: break @@ -1254,14 +1231,35 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { ]) } + func aniListProgressUpdate(progress: ProgressIndices, newProgress: ProgressIndices) { + if progress.v > newProgress.v || progress.c > newProgress.c { + if let token = aniListToken, let id = metadata.anilistId { + var cProgress = 0 + var vProgress = 0 + while progress.v > vProgress { + cProgress += metadata.volumes[vProgress].chapters.count + vProgress += 1 + } + cProgress += progress.c + 1 + vProgress += 1 + AniListClient.mutateProgress( + accessToken: token, mediaId: UInt(id), + progress: UInt(cProgress), progressVolumes: UInt(vProgress), completion: { _ in } + ) + } + } + } + func changeImage(turn: PageTurn) { let scaling = UIView.ContentMode.scaleAspectFit + let progress = getProgress() var newProgress = progress newProgress = getProgressIndicesFromTurn(turn: turn, progress: newProgress) if newProgress.v == progress.v, newProgress.c == progress.c, newProgress.i == progress.i { return } - progress = newProgress + aniListProgressUpdate(progress: progress, newProgress: newProgress) + state[currentPath.lastPathComponent]!.progress = newProgress updateInfo() let path = getImagePath(progress) @@ -1289,39 +1287,6 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { saveLocalState() } - func showComicOptions(for indexPath: IndexPath) { - let alert = UIAlertController(title: "Options", message: nil, preferredStyle: .actionSheet) - alert.addAction(UIAlertAction(title: "View Metadata", style: .default) { _ in - let metadataVC = ComicMetadataViewController(comic: self.comics[indexPath.item].metadata) - let nav = UINavigationController(rootViewController: metadataVC) - nav.modalPresentationStyle = .formSheet - self.present(nav, animated: false) - }) - alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { _ in - do { - try self.fileManager.removeItem(at: self.comics[indexPath.item].path) - self.comics.remove(at: indexPath.item) - self.comicCollectionView.reloadData() - } catch { - let failureAlert = UIAlertController( - title: "Failed to remove directory.", - message: "Failed to recursively remove \(self.comics[indexPath.item].path)", - preferredStyle: .alert - ) - self.present(failureAlert, animated: false) - } - }) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) - - // Required for iPad, harmless on iPhone - if let cell = comicCollectionView.cellForItem(at: indexPath) { - alert.popoverPresentationController?.sourceView = cell - alert.popoverPresentationController?.sourceRect = cell.bounds - } - - present(alert, animated: false) - } - func metaValueToString(m: MetaValue) -> String { var r = "" if m.bonus != 0 { @@ -1334,6 +1299,7 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { func updateInfo() { if metadata == nil { return } + let progress = getProgress() var text = "\(metadata.title ?? "")\n" @@ -1398,9 +1364,6 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { func getImagePath(_ progress: ProgressIndices) -> String { let (v, c, i) = (progress.v, progress.c, progress.i) return metadata.volumes[v].chapters[c].images[i].filename! - // if fileManager.fileExists(atPath: modernPath.path) { - // } - // return nil } func getProgressIndicesFromTurn(turn: PageTurn, progress: ProgressIndices) -> ProgressIndices { @@ -1437,14 +1400,9 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { return ProgressIndices(v: v, c: c, i: i) } - func getMetadata(path: URL) -> Metadata? { - guard let data = try? Data(contentsOf: path.appendingPathComponent(metadataFilename)) else { return nil } - var byteBuffer = ByteBuffer(data: data) - guard let metadata: Metadata = try? getCheckedRoot(byteBuffer: &byteBuffer) else { return nil } - return metadata - } - func setImages() { + let mode = getMode() + let progress = getProgress() let scaling: UIView.ContentMode = mode == .scroll ? .scaleAspectFill : .scaleAspectFit imageLoader.loadImage( archiveURL: getArchiveURL(progress.v), @@ -1476,6 +1434,22 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { } } +func getAniListToken() -> String? { + let fileManager = FileManager.default + if let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { + let path = documentsURL.appendingPathComponent("anilist_token.txt").path + if !fileManager.fileExists(atPath: path) { + return nil + } + do { + return try String(contentsOfFile: path).trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + } catch { + print("Error reading directory contents: \(error)") + } + } + return nil +} + func getGlobalState() -> GlobalState { let fileManager = FileManager.default if let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { @@ -1562,7 +1536,7 @@ extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFl else { return ScrollingImageCell() } - if mode != .scroll { return cell } + if getMode() != .scroll { return cell } if metadata == nil { print("metadata is nil, should probably not be the case") return cell @@ -1573,6 +1547,7 @@ extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFl newProgress = getProgressIndicesFromTurn(turn: .next, progress: newProgress) } } + let progress = getProgress() imageLoader.loadImage( archiveURL: getArchiveURL(progress.v), filename: getImagePath(newProgress), @@ -1627,6 +1602,7 @@ extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFl ) { if collectionView == comicCollectionView { let selectedComic = comics[indexPath.item] + currentItemIndex = indexPath.item readComic(name: selectedComic.metadata.title!) } @@ -2000,21 +1976,30 @@ class ComicMetadataViewController: UITableViewController { cell.detailTextLabel?.numberOfLines = 0 return cell } + var anId = "" + if let id = comic.anilistId { + anId = String(id) + } let cell = UITableViewCell(style: .value1, reuseIdentifier: nil) switch indexPath.row { case 0: cell.textLabel?.text = "Title"; cell.detailTextLabel?.text = comic.title - case 1: cell.textLabel?.text = "Format"; cell.detailTextLabel?.text = "\(comic.format)" - case 2: cell.textLabel?.text = "Original Language"; cell.detailTextLabel?.text = comic.originalLanguage - case 3: cell.textLabel?.text = "Publication Demographic" - cell.detailTextLabel?.text = "\(comic.publicationDemographic)" - case 4: cell.textLabel?.text = "Country of Origin"; cell.detailTextLabel?.text = comic.countryOfOrigin - case 5: cell.textLabel?.text = "Status"; cell.detailTextLabel?.text = "\(comic.status)" - case 6: cell.textLabel?.text = "Content Rating"; cell.detailTextLabel?.text = "\(comic.contentRating)" - case 7: cell.textLabel?.text = "First Release" + case 1: cell.textLabel?.text = "AniList ID"; cell.detailTextLabel?.text = anId + case 2: cell.textLabel?.text = "Format"; cell.detailTextLabel?.text = comic.format != nil + ? "\(comic.format!)" : "" + case 3: cell.textLabel?.text = "Original Language"; cell.detailTextLabel?.text = comic.originalLanguage + case 4: cell.textLabel?.text = "Publication Demographic" + cell.detailTextLabel?.text = comic.publicationDemographic != nil + ? "\(comic.publicationDemographic!)" : "" + case 5: cell.textLabel?.text = "Country of Origin"; cell.detailTextLabel?.text = comic.countryOfOrigin + case 6: cell.textLabel?.text = "Status"; cell.detailTextLabel?.text = comic.status != nil + ? "\(comic.status!)" : "" + case 7: cell.textLabel?.text = "Content Rating"; cell.detailTextLabel?.text = comic.contentRating != nil + ? "\(comic.contentRating!)" : "" + case 8: cell.textLabel?.text = "First Release" cell.detailTextLabel?.text = "\(dateToString(comic.startReleaseDate))" - case 8: cell.textLabel?.text = "Last Release" + case 9: cell.textLabel?.text = "Last Release" cell.detailTextLabel?.text = "\(dateToString(comic.endReleaseDate))" - case 9: cell.textLabel?.text = "Tags" + case 10: cell.textLabel?.text = "Tags" cell.detailTextLabel?.text = comic.tags.map { $0.value ?? "" }.joined(separator: ", ") default: break } @@ -2106,7 +2091,7 @@ class SettingsMenuViewController: UITableViewController { override func tableView(_: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { let item = items[indexPath.row] - return (item.selected?() == true) ? nil : indexPath + return item.selected?() == true ? nil : indexPath } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { @@ -2121,12 +2106,88 @@ class SettingsMenuViewController: UITableViewController { } } -func convertPageTurnModeToString(_ pageTurnMode: PageTurnMode) -> String { - switch pageTurnMode { - case .leftToRight: return "Left to Right" - case .rightToLeft: return "Right to Left" - case .scroll: return "Scroll" +struct ComicOptionItem { + let title: String + let children: [ComicOptionItem]? + let destructive: Bool + let available: () -> Bool + let action: (() -> Void)? +} + +class ComicOptionViewController: UITableViewController { + private let items: [ComicOptionItem] + + init(title: String, items: [ComicOptionItem]) { + self.items = items + super.init(style: .plain) + self.title = title } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) not implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") + preferredContentSize = CGSize(width: 330, height: items.count * 44) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + tableView.reloadData() + } + + override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { + return items.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) + let item = items[indexPath.row] + cell.textLabel?.text = item.title + cell.accessoryType = item.children != nil ? .disclosureIndicator : .none + + let available = item.available() + + if available { + cell.accessoryType = item.children != nil ? .disclosureIndicator : .none + cell.selectionStyle = .default + if item.destructive { + cell.textLabel?.textColor = .red + } else { + cell.textLabel?.textColor = .white + } + } else { + cell.textLabel?.textColor = .gray + cell.selectionStyle = .none + } + + return cell + } + + override func tableView(_: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { + let item = items[indexPath.row] + return (item.available()) ? indexPath : nil + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let item = items[indexPath.row] + if let children = item.children { + let next = ComicOptionViewController(title: item.title, items: children) + navigationController?.pushViewController(next, animated: false) + } else { + tableView.deselectRow(at: indexPath, animated: false) + item.action?() + } + } +} + +enum AniListAction: String, CaseIterable { + case track = "Track" + case updateStatus = "Update Status" + case fetchMetadata = "Fetch Metadata" } extension ViewController { @@ -2156,11 +2217,12 @@ extension ViewController { return SettingsMenuItem( title: title, children: nil, action: { [self] in - self.useDefaultBackgroundColor = color == nil + self.state[currentPath.lastPathComponent]!.backgroundColor = color == nil + ? nil : convertColorToString(color!) colorHandler(color ?? self.convertStringToColor(settings.defaultReadingBackgroundColor)) self.dismiss(animated: false) }, selected: { [self] in - if self.useDefaultBackgroundColor { return color == nil } + if self.state[currentPath.lastPathComponent]!.backgroundColor == nil { return color == nil } return color == self.readerView.backgroundColor && color != nil } ) @@ -2168,42 +2230,40 @@ extension ViewController { } private func pageTurnItems(applyTo pageTurnHandler: @escaping (PageTurnMode) -> Void) -> [SettingsMenuItem] { - let items: [PageTurnMode] = [.leftToRight, .rightToLeft, .scroll] + let items = PageTurnMode.allCases return items.map { pageTurnMode in SettingsMenuItem( - title: convertPageTurnModeToString(pageTurnMode), + title: "\(pageTurnMode)", children: nil, action: { [self] in pageTurnHandler(pageTurnMode) self.dismiss(animated: false) }, selected: { [self] in - return self.mode == pageTurnMode + return self.getMode() == pageTurnMode } ) } } private func pageTurnDefaultItems(applyTo pageTurnHandler: @escaping (PageTurnMode) -> Void) -> [SettingsMenuItem] { - let items: [PageTurnMode?] = [.leftToRight, .rightToLeft, .scroll, nil] + let items = PageTurnMode.allCases + [nil] return items.map { pageTurnMode in var title = "" if let mode = pageTurnMode { - title = convertPageTurnModeToString(mode) + title = "\(mode)" } else { - title = "Default (\(convertPageTurnModeToString(settings.defaultPageTurnMode)))" + title = "Default (\(settings.defaultPageTurnMode))" } return SettingsMenuItem( title: title, children: nil, action: { [self] in - self.useDefaultPageTurnMode = pageTurnMode == nil - let mode = pageTurnMode ?? self.settings.defaultPageTurnMode + let mode = getMode() pageTurnHandler(mode) self.dismiss(animated: false) }, selected: { [self] in - if self.useDefaultPageTurnMode { return pageTurnMode == nil } - return self.mode == pageTurnMode && pageTurnMode != nil + return self.getMode() == pageTurnMode } ) } @@ -2246,9 +2306,11 @@ extension ViewController { private func makePageTurnTree() -> [SettingsMenuItem] { return pageTurnDefaultItems { [self] pageTurnMode in + let mode = getMode() + let progress = getProgress() if mode == pageTurnMode { return } let scrollSwitch = mode != .scroll && pageTurnMode != .scroll - mode = pageTurnMode + state[currentPath.lastPathComponent]!.mode = pageTurnMode saveLocalState() if scrollSwitch { return } let archiveURL = getArchiveURL(progress.v) @@ -2273,11 +2335,262 @@ extension ViewController { } private func makeBackgroundColorTree() -> [SettingsMenuItem] { - return colorDefaultItems { [weak self] color in - self?.readerView.backgroundColor = color - self?.saveLocalState() + return colorDefaultItems { [self] color in + self.readerView.backgroundColor = color + self.saveLocalState() } } + + private func makeComicOptionTree(_ indexPath: IndexPath) -> [ComicOptionItem] { + let viewMetadataOption = ComicOptionItem( + title: "View Metadata", + children: nil, + destructive: false, + available: { true }, + action: { [self] in + let metadataVC = ComicMetadataViewController(comic: self.comics[indexPath.item].metadata) + let nav = UINavigationController(rootViewController: metadataVC) + nav.modalPresentationStyle = .formSheet + self.dismiss(animated: false) + self.present(nav, animated: false) + } + ) + let aniListOptionItems = aniListOptionItems(indexPath) + let aniListOption = ComicOptionItem( + title: "AniList", + children: aniListOptionItems, + destructive: false, + available: { [self] in + return comics[indexPath.item].metadata.anilistId != nil && aniListToken != nil + }, + action: { [weak self] in + self?.dismiss(animated: false) + } + ) + let deleteOption = ComicOptionItem( + title: "Delete", + children: nil, + destructive: true, + available: { true }, + action: { [self] in + do { + try self.fileManager.removeItem(at: self.comics[indexPath.item].path) + self.comics.remove(at: indexPath.item) + self.comicCollectionView.reloadData() + } catch { + let failureAlert = UIAlertController( + title: "Failed to remove directory.", + message: "Failed to recursively remove \(self.comics[indexPath.item].path)", + preferredStyle: .alert + ) + self.present(failureAlert, animated: false) + } + self.dismiss(animated: false) + } + ) + return [viewMetadataOption, aniListOption, deleteOption] + } + + private func aniListOptionItems(_ indexPath: IndexPath) -> [ComicOptionItem] { + let items = AniListAction.allCases + return items.map { action in + var children: [ComicOptionItem]? + var title = action.rawValue + switch action { + case .updateStatus: children = aniListUpdateStatusItems(indexPath) + case .fetchMetadata: _ = 0 + case .track: + if state[currentPath.lastPathComponent]!.aniListTracking { + title += " (Enabled)" + } else { + title += " (Disabled)" + } + } + return ComicOptionItem( + title: title, children: children, + destructive: false, available: { true }, + action: { [self] in + if action == .track { + var v = self.state[currentPath.lastPathComponent]!.aniListTracking + v.toggle() + self.state[self.currentPath.lastPathComponent]!.aniListTracking = v + self.saveLocalState() + } + if action == .fetchMetadata { + if let id = comics[indexPath.item].metadata.anilistId { + AniListClient.fetchMetadata(id: UInt(id)) { result in + switch result { + case let .success(m): self.rebuildMetadata(m, indexPath: indexPath) + case .failure: break + } + } + } + } + self.dismiss(animated: false) + } + ) + } + } + + private func aniListUpdateStatusItems(_ indexPath: IndexPath) -> [ComicOptionItem] { + let items = MediaListStatus.allCases + guard let mediaId = comics[indexPath.item].metadata.anilistId else { return [] } + return items.map { status in + ComicOptionItem( + title: "\(status)", children: nil, + destructive: false, available: { true }, + action: { [weak self] in + self?.dismiss(animated: false) + guard let token = self?.aniListToken else { return } + AniListClient.mutateMediaListStatus( + accessToken: token, mediaId: UInt(mediaId), mediaStatus: status, completion: { _ in } + ) + } + ) + } + } + + private func getAnilistMediaStatus(item _: ComicOptionItem) { + guard let token = aniListToken else { return } + AniListClient.fetchViewer(accessToken: token) { result in + switch result { + case let .success(viewer): + print("Logged in as \(viewer.name) (id: \(viewer.id))") + guard let id = self.metadata.anilistId else { return } + AniListClient.fetchMediaStatus( + accessToken: token, id: UInt(id), + userId: UInt(viewer.id) + ) { result in + print("Tried to fetch for userId: \(viewer.id) and id: \(id)") + switch result { + case let .success(mediaStatus): + print("mediaStatus: \(mediaStatus)") + case let .failure(error): + print("Error: \(error)") + } + } + case let .failure(error): + print("Error: \(error)") + } + } + } + + private func rebuildMetadata(_ m: MediaMetadata, indexPath: IndexPath) { + let alert = UIAlertController( + title: "Failed to rebuild metadata", + message: "This is an alert.", + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + + let endDate: Date! + if let d = m.endDate, let year = d.year, let month = d.month, let day = d.day { + endDate = Date(year: UInt16(year), month: UInt8(month), day: UInt8(day)) + } else { endDate = Date(year: 0, month: 0, day: 0) } + let startDate: Date! + if let d = m.startDate, let year = d.year, let month = d.month, let day = d.day { + startDate = Date(year: UInt16(year), month: UInt8(month), day: UInt8(day)) + } else { startDate = Date(year: 0, month: 0, day: 0) } + + let nSource = Source(from: m.source) + let nFormat = Format(from: m.format) + let nStatus = Status(from: m.status) + let pM = comics[indexPath.item].metadata + var volumes: [Offset] = [] + var currentChapters: [Offset] = [] + var currentImages: [Offset] = [] + var builder = FlatBufferBuilder(initialSize: 1024) + for volume in pM.volumes { + for chapter in volume.chapters { + for image in chapter.images { + let m_filename = builder.create(string: image.filename) + let imageMetadata = ImageMetadata.createImageMetadata( + &builder, + doublePage: image.doublePage, + filenameOffset: m_filename, + firstPage: image.firstPage, + size: image.size + ) + currentImages.append(imageMetadata) + } + let chapterName = builder.create(string: chapter.name) + let imagesOffset = builder.createVector(ofOffsets: currentImages) + let chapterMetadata = ChapterMetadata.createChapterMetadata( + &builder, + chapter: chapter.chapter, + nameOffset: chapterName, + imagesVectorOffset: imagesOffset + ) + currentChapters.append(chapterMetadata) + currentImages = [] + } + let volumeTitle = builder.create(string: volume.title) + let volumeURLOffset = builder.create(string: volume.archive) + let chaptersOffset = builder.createVector(ofOffsets: currentChapters) + let volumeMetadata = VolumeMetadata.createVolumeMetadata( + &builder, + volume: volume.volume, + titleOffset: volumeTitle, + archiveOffset: volumeURLOffset, + chaptersVectorOffset: chaptersOffset + ) + + volumes.append(volumeMetadata) + currentChapters = [] + } + + let title = builder.create(string: m.title?.english) + let originalLanguage = builder.create(string: pM.originalLanguage) + let countryOfOrigin = builder.create(string: pM.countryOfOrigin) + let description = builder.create(string: m.description) + let tags = builder.createVector(ofOffsets: []) + let volumeOffsets = builder.createVector(ofOffsets: volumes) + let metadata = Metadata.createMetadata( + &builder, + titleOffset: title, + format: nFormat, + originalLanguageOffset: originalLanguage, + countryOfOriginOffset: countryOfOrigin, + publicationDemographic: pM.publicationDemographic, + status: nStatus, + contentRating: pM.contentRating, + source: nSource, + startReleaseDate: startDate ?? Date(year: 0, month: 0, day: 0), + endReleaseDate: endDate ?? Date(year: 0, month: 0, day: 0), + tagsVectorOffset: tags, + descriptionOffset: description, + anilistId: pM.anilistId, + volumesVectorOffset: volumeOffsets, + imageCount: pM.imageCount + ) + + builder.finish(offset: metadata) + do { + try builder.data.write(to: currentPath.appendingPathComponent(metadataFilename)) + } catch { + alert.message = String(format: "Failed to write metadata file at: \(metadataFilename)") + DispatchQueue.main.async { + self.present(alert, animated: false) + } + } + let metadataPath = currentPath.appendingPathComponent(metadataFilename) + guard let data = try? Data(contentsOf: metadataPath) else { + alert.message = String(format: "Failed to read metadata file at: \(metadataPath)") + DispatchQueue.main.async { + self.present(alert, animated: false) + } + return + } + var byteBuffer = ByteBuffer(data: data) + guard let newMetadata: Metadata = try? getCheckedRoot(byteBuffer: &byteBuffer) else { + alert.message = String(format: "Failed to load flatbuffer file at: \(metadataPath)") + DispatchQueue.main.async { + self.present(alert, animated: false) + } + return + } + comics[indexPath.item].metadata = newMetadata + } } extension ViewController: UIPopoverPresentationControllerDelegate { @@ -2326,6 +2639,21 @@ extension ViewController: UIPopoverPresentationControllerDelegate { present(nav, animated: false) } + @objc private func comicOptionLongPressed(_ indexPath: IndexPath) { + let root = ComicOptionViewController(title: "Comic", items: makeComicOptionTree(indexPath)) + let nav = UINavigationController(rootViewController: root) + nav.modalPresentationStyle = .popover + + if let popover = nav.popoverPresentationController, let cell = comicCollectionView.cellForItem(at: indexPath) { + popover.sourceView = cell + popover.sourceRect = cell.bounds + popover.permittedArrowDirections = .up + popover.delegate = self + } + + present(nav, animated: false) + } + func adaptivePresentationStyle(for _: UIPresentationController) -> UIModalPresentationStyle { return .none // keeps it a real popover on iPhone instead of a full-screen sheet } @@ -2335,3 +2663,556 @@ extension ViewController: UIPopoverPresentationControllerDelegate { return true } } + +struct AniListResponse: Decodable { + let data: T +} + +struct MediaMetadataData: Decodable { + // swiftlint:disable identifier_name + let Media: MediaMetadata + // swiftlint:enable identifier_name +} + +struct MediaDate: Decodable { + let year: Int? + let month: Int? + let day: Int? +} + +struct MediaMetadata: Decodable { + let description: String? + let endDate: MediaDate? + let startDate: MediaDate? + let format: String? + let title: MediaTitle? + let source: String? + let status: String? +} + +struct MediaTitle: Decodable { + let english: String? +} + +struct ViewerData: Decodable { + // swiftlint:disable identifier_name + let Viewer: Viewer + // swiftlint:enable identifier_name +} + +struct Viewer: Decodable { + let id: Int + let name: String +} + +struct MediaListData: Decodable { + // swiftlint:disable identifier_name + let MediaList: MediaList + // swiftlint:enable identifier_name +} + +struct MediaList: Decodable { + let status: MediaListStatus +} + +struct SaveMediaListEntryData: Decodable { + // swiftlint:disable identifier_name + let SaveMediaListEntry: SaveMediaListEntry + // swiftlint:enable identifier_name +} + +struct SaveMediaListEntry: Decodable { + let id: Int + let status: MediaListStatus +} + +struct SaveMediaListProgressEntryData: Decodable { + // swiftlint:disable identifier_name + let SaveMediaListEntry: SaveMediaListProgressEntry + // swiftlint:enable identifier_name +} + +struct SaveMediaListProgressEntry: Decodable { + let id: Int + let progress: Int + let progressVolumes: Int +} + +enum MediaListStatus: String, Decodable, CaseIterable { + case current = "CURRENT" + case planning = "PLANNING" + case completed = "COMPLETED" + case dropped = "DROPPED" + case paused = "PAUSED" +} + +struct GraphQLBody: Encodable { + let query: String +} + +enum AniListClient { + static let endpoint = URL(string: "https://graphql.aniList.co")! + + enum AniListError: Error { + case badResponse + case requestFailed(status: Int, body: String) + case noData + } + + static func mutateProgress( + accessToken: String, + mediaId: UInt, + progress: UInt, + progressVolumes: UInt, + completion: @escaping (Result) -> Void + ) { + let mutation = """ + mutation ( + $mediaId: Int = \(mediaId), $progress: Int = \(progress), $progressVolumes: Int = \(progressVolumes) + ) { + SaveMediaListEntry(mediaId: $mediaId, progress: $progress, progressVolumes: $progressVolumes) { + id + progress + progressVolumes + } + } + """ + mutate( + accessToken: accessToken, mutation: mutation + ) { (result: Result, Error>) in + switch result { + case let .success(m): + let status = m.data.SaveMediaListEntry + completion(.success(status)) + case let .failure(error): + completion(.failure(error)) + print("Error: \(error)") + } + } + } + + static func mutateMediaListStatus( + accessToken: String, + mediaId: UInt, + mediaStatus: MediaListStatus, + completion: @escaping (Result) -> Void + ) { + let mutation = """ + mutation ($mediaId: Int = \(mediaId), $status: MediaListStatus = \(mediaStatus.rawValue)) { + SaveMediaListEntry(mediaId: $mediaId, status: $status) { + id + status + } + } + """ + mutate( + accessToken: accessToken, mutation: mutation + ) { (result: Result, Error>) in + switch result { + case let .success(m): + let status = m.data.SaveMediaListEntry + completion(.success(status)) + case let .failure(error): + completion(.failure(error)) + print("Error: \(error)") + } + } + } + + static func fetchMetadata( + id: UInt, + completion: @escaping (Result) -> Void + ) { + let query = """ + query($mediaId: Int = \(id)) { + Media(id: $mediaId) { + description + endDate { + day + month + year + } + format + startDate { + day + month + year + } + title { + english + } + source + status + } + } + """ + fetch( + query: query + ) { (result: Result, Error>) in + switch result { + case let .success(m): + let metadata = m.data.Media + completion(.success(metadata)) + case let .failure(error): + completion(.failure(error)) + print("Error: \(error)") + } + } + } + + static func fetch( + query: String, + completion: @escaping (Result, Error>) -> Void + ) { + var request = URLRequest(url: endpoint) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + do { + request.httpBody = try JSONEncoder().encode(GraphQLBody(query: query)) + } catch { + completion(.failure(error)) + return + } + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + completion(.failure(error)) + return + } + + guard let http = response as? HTTPURLResponse else { + completion(.failure(AniListError.badResponse)) + return + } + + guard let data = data else { + completion(.failure(AniListError.noData)) + return + } + + guard (200 ... 299).contains(http.statusCode) else { + let body = String(data: data, encoding: .utf8) ?? "" + completion(.failure(AniListError.requestFailed(status: http.statusCode, body: body))) + return + } + + do { + let decoded = try JSONDecoder().decode(AniListResponse.self, from: data) + completion(.success(decoded)) + } catch { + completion(.failure(error)) + } + } + + task.resume() + } + + static func fetchMediaStatus( + accessToken: String, + id: UInt, + userId: UInt, + completion: @escaping (Result) -> Void + ) { + let query = """ + query ($id: Int = \(id), $userId: Int = \(userId)) { + MediaList(mediaId: $id, userId: $userId) { + status + } + } + """ + fetchAuthenticated( + accessToken: accessToken, query: query + ) { (result: Result, Error>) in + switch result { + case let .success(m): + let status = m.data.MediaList.status + completion(.success(status)) + case let .failure(error): + completion(.failure(error)) + print("Error: \(error)") + } + } + } + + static func fetchViewer( + accessToken: String, + completion: @escaping (Result) -> Void + ) { + let query = """ + query { + Viewer { + id + name + } + } + """ + fetchAuthenticated( + accessToken: accessToken, query: query + ) { (result: Result, Error>) in + switch result { + case let .success(v): + let viewer = v.data.Viewer + completion(.success(viewer)) + case let .failure(error): + print("Error: \(error)") + } + } + } + + static func fetchAuthenticated( + accessToken: String, + query: String, + completion: @escaping (Result, Error>) -> Void + ) { + var request = URLRequest(url: endpoint) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") + + do { + request.httpBody = try JSONEncoder().encode(GraphQLBody(query: query)) + } catch { + completion(.failure(error)) + return + } + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + completion(.failure(error)) + return + } + + guard let http = response as? HTTPURLResponse else { + completion(.failure(AniListError.badResponse)) + return + } + + guard let data = data else { + completion(.failure(AniListError.noData)) + return + } + + guard (200 ... 299).contains(http.statusCode) else { + let body = String(data: data, encoding: .utf8) ?? "" + completion(.failure(AniListError.requestFailed(status: http.statusCode, body: body))) + return + } + + do { + let decoded = try JSONDecoder().decode(AniListResponse.self, from: data) + completion(.success(decoded)) + } catch { + completion(.failure(error)) + } + } + + task.resume() + } + + static func mutate( + accessToken: String, + mutation: String, + completion: @escaping (Result, Error>) -> Void + ) { + var request = URLRequest(url: endpoint) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") + + do { + request.httpBody = try JSONEncoder().encode(GraphQLBody(query: mutation)) + } catch { + completion(.failure(error)) + return + } + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + completion(.failure(error)) + return + } + + guard let http = response as? HTTPURLResponse else { + completion(.failure(AniListError.badResponse)) + return + } + + guard let data = data else { + completion(.failure(AniListError.noData)) + return + } + + guard (200 ... 299).contains(http.statusCode) else { + let body = String(data: data, encoding: .utf8) ?? "" + completion(.failure(AniListError.requestFailed(status: http.statusCode, body: body))) + return + } + + do { + let decoded = try JSONDecoder().decode(AniListResponse.self, from: data) + completion(.success(decoded)) + } catch { + completion(.failure(error)) + } + } + + task.resume() + } +} + +extension Source { + init?(from string: String?) { + guard let string = string else { return nil } + guard let source = Source(from: string) else { return nil } + self = source + } + + init?(from string: String) { + switch string.lowercased() { + case "ORIGINAL": self = .original + case "MANGA": self = .manga + case "LIGHT_NOVEL": self = .lightnovel + case "VISUAL_NOVEL": self = .visualnovel + case "VIDEO_GAME": self = .videogame + case "OTHER": self = .other + case "NOVEL": self = .novel + case "DOUJINSHI": self = .doujinshi + case "ANIME": self = .anime + case "WEB_NOVEL": self = .webnovel + case "LIVE_ACTION": self = .liveaction + case "GAME": self = .game + case "COMIC": self = .comic + case "MULTIMEDIA_PROJECT": self = .multimediaproject + case "PICTUREBOOK": self = .picturebook + default: return nil + } + } +} + +extension Format { + init?(from string: String?) { + guard let string = string else { return nil } + guard let format = Format(from: string) else { return nil } + self = format + } + + init?(from string: String) { + switch string { + case "MANGA": self = .manga + case "NOVEL": self = .novel + case "ONE_SHOT": self = .oneshot + default: return nil + } + } +} + +extension Status { + init?(from string: String?) { + guard let string = string else { return nil } + guard let status = Status(from: string) else { return nil } + self = status + } + + init?(from string: String) { + switch string { + case "FINISHED": self = .finished + case "RELEASING": self = .releasing + case "HIATUS": self = .hiatus + case "NOT_YET_RELEASED": self = .notyetreleased + case "CANCELLED": self = .cancelled + default: return nil + } + } +} + +extension Status: CustomStringConvertible { + public var description: String { + switch self { + case .finished: return "Finished" + case .releasing: return "Releasing" + case .hiatus: return "Hiatus" + case .notyetreleased: return "Not Yet Released" + case .cancelled: return "Cancelled" + } + } +} + +extension Source: CustomStringConvertible { + public var description: String { + switch self { + case .original: return "Original" + case .manga: return "Manga" + case .lightnovel: return "Light Novel" + case .visualnovel: return "Visual Novel" + case .videogame: return "Video Game" + case .other: return "Other" + case .novel: return "Novel" + case .doujinshi: return "Doujinshi" + case .anime: return "Anime" + case .webnovel: return "Web Novel" + case .liveaction: return "Live Action" + case .game: return "Game" + case .comic: return "Comic" + case .multimediaproject: return "Multimedia Project" + case .picturebook: return "Picture Book" + } + } +} + +extension Format: CustomStringConvertible { + public var description: String { + switch self { + case .manga: return "Manga" + case .novel: return "Novel" + case .oneshot: return "One Shot" + } + } +} + +extension PublicationDemographic: CustomStringConvertible { + public var description: String { + switch self { + case .shounen: return "Shounen" + case .shoujou: return "Shoujou" + case .seinen: return "Seinen" + case .josei: return "Josei" + } + } +} + +extension ContentRating: CustomStringConvertible { + public var description: String { + switch self { + case .safe: return "Safe" + case .suggestive: return "Suggestive" + case .erotica: return "Erotica" + case .pornographic: return "Pornographic" + } + } +} + +extension MediaListStatus: CustomStringConvertible { + var description: String { + switch self { + case .current: return "Current" + case .planning: return "Planning" + case .completed: return "Completed" + case .dropped: return "Dropped" + case .paused: return "Paused" + } + } +} + +extension PageTurnMode: CustomStringConvertible { + var description: String { + switch self { + case .leftToRight: return "Left to Right" + case .rightToLeft: return "Right to Left" + case .scroll: return "Scroll" + } + } +} diff --git a/ImageViewer/metadata.fbs b/ImageViewer/metadata.fbs index 15e3aa4..71c3692 100644 --- a/ImageViewer/metadata.fbs +++ b/ImageViewer/metadata.fbs @@ -49,17 +49,18 @@ table VolumeMetadata { table Metadata { title:string (required); - format:Format; + format:Format = null; original_language:string; country_of_origin:string; - publication_demographic:PublicationDemographic; - status:Status; - content_rating:ContentRating; - source:Source; + publication_demographic:PublicationDemographic = null; + status:Status = null; + content_rating:ContentRating = null; + source:Source = null; start_release_date:Date; end_release_date:Date; tags:[Tag]; description:string; + anilist_id:uint = null; volumes:[VolumeMetadata]; image_count:uint; } diff --git a/ImageViewer/metadata_generated.swift b/ImageViewer/metadata_generated.swift index cf529b3..237ebf0 100644 --- a/ImageViewer/metadata_generated.swift +++ b/ImageViewer/metadata_generated.swift @@ -448,21 +448,22 @@ public struct Metadata: FlatBufferTable, FlatbuffersVectorInitializable, Verifia static let endReleaseDate: VOffset = 22 static let tags: VOffset = 24 static let description: VOffset = 26 - static let volumes: VOffset = 28 - static let imageCount: VOffset = 30 + static let anilistId: VOffset = 28 + static let volumes: VOffset = 30 + static let imageCount: VOffset = 32 } public var title: String! { let o = _accessor.offset(VT.title); return _accessor.string(at: o) } public var titleSegmentArray: [UInt8]! { return _accessor.getVector(at: VT.title) } - public var format: Format { let o = _accessor.offset(VT.format); return o == 0 ? .manga : Format(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? .manga } + public var format: Format? { let o = _accessor.offset(VT.format); return o == 0 ? nil : Format(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? nil } public var originalLanguage: String? { let o = _accessor.offset(VT.originalLanguage); return o == 0 ? nil : _accessor.string(at: o) } public var originalLanguageSegmentArray: [UInt8]? { return _accessor.getVector(at: VT.originalLanguage) } public var countryOfOrigin: String? { let o = _accessor.offset(VT.countryOfOrigin); return o == 0 ? nil : _accessor.string(at: o) } public var countryOfOriginSegmentArray: [UInt8]? { return _accessor.getVector(at: VT.countryOfOrigin) } - public var publicationDemographic: PublicationDemographic { let o = _accessor.offset(VT.publicationDemographic); return o == 0 ? .shounen : PublicationDemographic(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? .shounen } - public var status: Status { let o = _accessor.offset(VT.status); return o == 0 ? .finished : Status(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? .finished } - public var contentRating: ContentRating { let o = _accessor.offset(VT.contentRating); return o == 0 ? .safe : ContentRating(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? .safe } - public var source: Source { let o = _accessor.offset(VT.source); return o == 0 ? .original : Source(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? .original } + public var publicationDemographic: PublicationDemographic? { let o = _accessor.offset(VT.publicationDemographic); return o == 0 ? nil : PublicationDemographic(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? nil } + public var status: Status? { let o = _accessor.offset(VT.status); return o == 0 ? nil : Status(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? nil } + public var contentRating: ContentRating? { let o = _accessor.offset(VT.contentRating); return o == 0 ? nil : ContentRating(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? nil } + public var source: Source? { let o = _accessor.offset(VT.source); return o == 0 ? nil : Source(rawValue: _accessor.readBuffer(of: Int8.self, at: o)) ?? nil } public var startReleaseDate: Date? { let o = _accessor.offset(VT.startReleaseDate); return o == 0 ? nil : _accessor.readBuffer(of: Date.self, at: o) } public var mutableStartReleaseDate: Date_Mutable? { let o = _accessor.offset(VT.startReleaseDate); return o == 0 ? nil : Date_Mutable(_accessor.bb, o: o + _accessor.position) } public var endReleaseDate: Date? { let o = _accessor.offset(VT.endReleaseDate); return o == 0 ? nil : _accessor.readBuffer(of: Date.self, at: o) } @@ -470,38 +471,41 @@ public struct Metadata: FlatBufferTable, FlatbuffersVectorInitializable, Verifia public var tags: FlatbufferVector { return _accessor.vector(at: VT.tags, byteSize: 4) } public var description: String? { let o = _accessor.offset(VT.description); return o == 0 ? nil : _accessor.string(at: o) } public var descriptionSegmentArray: [UInt8]? { return _accessor.getVector(at: VT.description) } + public var anilistId: UInt32? { let o = _accessor.offset(VT.anilistId); return o == 0 ? nil : _accessor.readBuffer(of: UInt32.self, at: o) } public var volumes: FlatbufferVector { return _accessor.vector(at: VT.volumes, byteSize: 4) } public var imageCount: UInt32 { let o = _accessor.offset(VT.imageCount); return o == 0 ? 0 : _accessor.readBuffer(of: UInt32.self, at: o) } - public static func startMetadata(_ fbb: inout FlatBufferBuilder) -> UOffset { fbb.startTable(with: 14) } + public static func startMetadata(_ fbb: inout FlatBufferBuilder) -> UOffset { fbb.startTable(with: 15) } public static func add(title: Offset, _ fbb: inout FlatBufferBuilder) { fbb.add(offset: title, at: VT.title) } - public static func add(format: Format, _ fbb: inout FlatBufferBuilder) { fbb.add(element: format.rawValue, def: 0, at: VT.format) } + public static func add(format: Format?, _ fbb: inout FlatBufferBuilder) { fbb.add(element: format?.rawValue, at: VT.format) } public static func add(originalLanguage: Offset, _ fbb: inout FlatBufferBuilder) { fbb.add(offset: originalLanguage, at: VT.originalLanguage) } public static func add(countryOfOrigin: Offset, _ fbb: inout FlatBufferBuilder) { fbb.add(offset: countryOfOrigin, at: VT.countryOfOrigin) } - public static func add(publicationDemographic: PublicationDemographic, _ fbb: inout FlatBufferBuilder) { fbb.add(element: publicationDemographic.rawValue, def: 0, at: VT.publicationDemographic) } - public static func add(status: Status, _ fbb: inout FlatBufferBuilder) { fbb.add(element: status.rawValue, def: 0, at: VT.status) } - public static func add(contentRating: ContentRating, _ fbb: inout FlatBufferBuilder) { fbb.add(element: contentRating.rawValue, def: 0, at: VT.contentRating) } - public static func add(source: Source, _ fbb: inout FlatBufferBuilder) { fbb.add(element: source.rawValue, def: 0, at: VT.source) } + public static func add(publicationDemographic: PublicationDemographic?, _ fbb: inout FlatBufferBuilder) { fbb.add(element: publicationDemographic?.rawValue, at: VT.publicationDemographic) } + public static func add(status: Status?, _ fbb: inout FlatBufferBuilder) { fbb.add(element: status?.rawValue, at: VT.status) } + public static func add(contentRating: ContentRating?, _ fbb: inout FlatBufferBuilder) { fbb.add(element: contentRating?.rawValue, at: VT.contentRating) } + public static func add(source: Source?, _ fbb: inout FlatBufferBuilder) { fbb.add(element: source?.rawValue, at: VT.source) } public static func add(startReleaseDate: Date?, _ fbb: inout FlatBufferBuilder) { guard let startReleaseDate = startReleaseDate else { return }; fbb.create(struct: startReleaseDate, position: VT.startReleaseDate) } public static func add(endReleaseDate: Date?, _ fbb: inout FlatBufferBuilder) { guard let endReleaseDate = endReleaseDate else { return }; fbb.create(struct: endReleaseDate, position: VT.endReleaseDate) } public static func addVectorOf(tags: Offset, _ fbb: inout FlatBufferBuilder) { fbb.add(offset: tags, at: VT.tags) } public static func add(description: Offset, _ fbb: inout FlatBufferBuilder) { fbb.add(offset: description, at: VT.description) } + public static func add(anilistId: UInt32?, _ fbb: inout FlatBufferBuilder) { fbb.add(element: anilistId, at: VT.anilistId) } public static func addVectorOf(volumes: Offset, _ fbb: inout FlatBufferBuilder) { fbb.add(offset: volumes, at: VT.volumes) } public static func add(imageCount: UInt32, _ fbb: inout FlatBufferBuilder) { fbb.add(element: imageCount, def: 0, at: VT.imageCount) } public static func endMetadata(_ fbb: inout FlatBufferBuilder, start: UOffset) -> Offset { let end = Offset(offset: fbb.endTable(at: start)); fbb.require(table: end, fields: [4]); return end } public static func createMetadata( _ fbb: inout FlatBufferBuilder, titleOffset title: Offset, - format: Format = .manga, + format: Format? = nil, originalLanguageOffset originalLanguage: Offset = Offset(), countryOfOriginOffset countryOfOrigin: Offset = Offset(), - publicationDemographic: PublicationDemographic = .shounen, - status: Status = .finished, - contentRating: ContentRating = .safe, - source: Source = .original, + publicationDemographic: PublicationDemographic? = nil, + status: Status? = nil, + contentRating: ContentRating? = nil, + source: Source? = nil, startReleaseDate: Date? = nil, endReleaseDate: Date? = nil, tagsVectorOffset tags: Offset = Offset(), descriptionOffset description: Offset = Offset(), + anilistId: UInt32? = nil, volumesVectorOffset volumes: Offset = Offset(), imageCount: UInt32 = 0 ) -> Offset { @@ -518,6 +522,7 @@ public struct Metadata: FlatBufferTable, FlatbuffersVectorInitializable, Verifia Metadata.add(endReleaseDate: endReleaseDate, &fbb) Metadata.addVectorOf(tags: tags, &fbb) Metadata.add(description: description, &fbb) + Metadata.add(anilistId: anilistId, &fbb) Metadata.addVectorOf(volumes: volumes, &fbb) Metadata.add(imageCount: imageCount, &fbb) return Metadata.endMetadata(&fbb, start: __start) @@ -537,6 +542,7 @@ public struct Metadata: FlatBufferTable, FlatbuffersVectorInitializable, Verifia try _v.visit(field: VT.endReleaseDate, fieldName: "endReleaseDate", required: false, type: Date.self) try _v.visit(field: VT.tags, fieldName: "tags", required: false, type: ForwardOffset, Tag>>.self) try _v.visit(field: VT.description, fieldName: "description", required: false, type: ForwardOffset.self) + try _v.visit(field: VT.anilistId, fieldName: "anilistId", required: false, type: UInt32.self) try _v.visit(field: VT.volumes, fieldName: "volumes", required: false, type: ForwardOffset, VolumeMetadata>>.self) try _v.visit(field: VT.imageCount, fieldName: "imageCount", required: false, type: UInt32.self) _v.finish()