221 lines
6.8 KiB
Swift
221 lines
6.8 KiB
Swift
import Foundation
|
|||
|
|
|
||
|
|
/// What a seller fills in to list a copy. The listing's slug and placeholder cover are derived from it on publishing.
|
||
|
|
public struct ListingDraft: Sendable, Equatable {
|
||
|
|
|
||
|
|
// MARK: Properties
|
||
|
|
|
||
|
|
/// The series or book title, e.g. `Nightward`.
|
||
|
|
public var series: String
|
||
|
|
/// The issue number or volume, e.g. `1`; empty for a one-off book.
|
||
|
|
public var issue: String
|
||
|
|
/// The publisher's name.
|
||
|
|
public var publisher: String
|
||
|
|
/// The cover year.
|
||
|
|
public var year: Int
|
||
|
|
/// The format token, e.g. `single-issues`.
|
||
|
|
public var format: String
|
||
|
|
/// The printing, counted from one; zero for later or unknown.
|
||
|
|
public var printing: Int
|
||
|
|
/// The edition, e.g. `Direct`; empty when not applicable.
|
||
|
|
public var edition: String
|
||
|
|
/// The company that graded and slabbed the copy, or `nil` for a raw copy.
|
||
|
|
public var grader: String?
|
||
|
|
/// The numeric grade, or `nil` for formats that are not graded.
|
||
|
|
public var grade: Double?
|
||
|
|
/// The condition as the seller states it, e.g. `NM` or `Like new`.
|
||
|
|
public var condition: String
|
||
|
|
/// The grader's certificate number, or `nil` for a raw copy.
|
||
|
|
public var certNumber: String?
|
||
|
|
/// The page quality, or `nil` when not stated.
|
||
|
|
public var pageQuality: String?
|
||
|
|
/// Why the issue is a key, or `nil` when it is not one.
|
||
|
|
public var keyNotes: String?
|
||
|
|
/// The defects declared, from the grading guide's checklist.
|
||
|
|
public var defects: [String]
|
||
|
|
/// The seller's notes on the copy.
|
||
|
|
public var sellerNotes: String
|
||
|
|
/// The asking price, in euro cents.
|
||
|
|
public var priceCents: Int
|
||
|
|
/// The shipping cost to the Netherlands, in euro cents.
|
||
|
|
public var shippingCents: Int
|
||
|
|
/// Whether the seller takes offers on the copy.
|
||
|
|
public var acceptsOffers: Bool
|
||
|
|
/// The URL paths of the photos of this copy, the front first.
|
||
|
|
public var photos: [String]
|
||
|
|
|
||
|
|
// MARK: Initializers
|
||
|
|
|
||
|
|
public init(
|
||
|
|
series: String,
|
||
|
|
issue: String,
|
||
|
|
publisher: String,
|
||
|
|
year: Int,
|
||
|
|
format: String,
|
||
|
|
printing: Int,
|
||
|
|
edition: String,
|
||
|
|
grader: String?,
|
||
|
|
grade: Double?,
|
||
|
|
condition: String,
|
||
|
|
certNumber: String?,
|
||
|
|
pageQuality: String?,
|
||
|
|
keyNotes: String?,
|
||
|
|
defects: [String],
|
||
|
|
sellerNotes: String,
|
||
|
|
priceCents: Int,
|
||
|
|
shippingCents: Int,
|
||
|
|
acceptsOffers: Bool,
|
||
|
|
photos: [String]
|
||
|
|
) {
|
||
|
|
self.series = series
|
||
|
|
self.issue = issue
|
||
|
|
self.publisher = publisher
|
||
|
|
self.year = year
|
||
|
|
self.format = format
|
||
|
|
self.printing = printing
|
||
|
|
self.edition = edition
|
||
|
|
self.grader = grader
|
||
|
|
self.grade = grade
|
||
|
|
self.condition = condition
|
||
|
|
self.certNumber = certNumber
|
||
|
|
self.pageQuality = pageQuality
|
||
|
|
self.keyNotes = keyNotes
|
||
|
|
self.defects = defects
|
||
|
|
self.sellerNotes = sellerNotes
|
||
|
|
self.priceCents = priceCents
|
||
|
|
self.shippingCents = shippingCents
|
||
|
|
self.acceptsOffers = acceptsOffers
|
||
|
|
self.photos = photos
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Extensions
|
||
|
|
|
||
|
|
extension ListingDraft {
|
||
|
|
|
||
|
|
// MARK: Constants
|
||
|
|
|
||
|
|
/// The tones a placeholder cover is drawn in.
|
||
|
|
static let tones = ["navy", "wine", "forest", "rust", "slate", "plum", "brick", "olive", "ink"]
|
||
|
|
|
||
|
|
// MARK: Computed
|
||
|
|
|
||
|
|
/// The placeholder cover: the series lettered over a few lines, on a tone picked by the series so every copy of it matches.
|
||
|
|
var cover: Listing.Cover {
|
||
|
|
var lines = [String]()
|
||
|
|
|
||
|
|
for word in series.uppercased().split(separator: " ").map(String.init) {
|
||
|
|
if let last = lines.last, last.count + word.count < 10 {
|
||
|
|
lines[lines.count - 1] = "\(last) \(word)"
|
||
|
|
} else {
|
||
|
|
lines.append(word)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let marker = switch format {
|
||
|
|
case "manga": issue.isEmpty ? "MANGA" : "VOL \(issue)"
|
||
|
|
case "trade-paperbacks": "TPB"
|
||
|
|
case "hardcovers": "HC"
|
||
|
|
default: issue
|
||
|
|
}
|
||
|
|
|
||
|
|
return .init(
|
||
|
|
tone: Self.tones[series.unicodeScalars.reduce(0) { ($0 + Int($1.value)) % Self.tones.count }],
|
||
|
|
title: lines.prefix(3).joined(separator: "\n"),
|
||
|
|
imprint: publisher.uppercased(),
|
||
|
|
number: marker
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Whether the copy is a single issue.
|
||
|
|
var isSingleIssue: Bool {
|
||
|
|
format == "single-issues"
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The printing in a few words, e.g. `1st print`.
|
||
|
|
var printingLabel: String {
|
||
|
|
let noun = isSingleIssue ? "print" : "printing"
|
||
|
|
|
||
|
|
switch printing {
|
||
|
|
case 1: return "1st \(noun)"
|
||
|
|
case 2: return "2nd \(noun)"
|
||
|
|
case 3: return "3rd \(noun)"
|
||
|
|
default: return "later \(noun)"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The printing in full, e.g. `First printing, direct edition`.
|
||
|
|
var printingDetail: String {
|
||
|
|
let ordinal = switch printing {
|
||
|
|
case 1: "First"
|
||
|
|
case 2: "Second"
|
||
|
|
case 3: "Third"
|
||
|
|
default: "Later"
|
||
|
|
}
|
||
|
|
|
||
|
|
return edition.isEmpty ? "\(ordinal) printing" : "\(ordinal) printing, \(edition.lowercased()) edition"
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The listing's title, e.g. `Nightward #1`, or `Hollow Comet, Vol. 4`.
|
||
|
|
var title: String {
|
||
|
|
guard !issue.isEmpty else {
|
||
|
|
return series
|
||
|
|
}
|
||
|
|
|
||
|
|
return switch format {
|
||
|
|
case "single-issues": "\(series) #\(issue)"
|
||
|
|
case "manga": "\(series), Vol. \(issue)"
|
||
|
|
default: "\(series) \(issue)"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: Methods
|
||
|
|
|
||
|
|
/// The listing the draft publishes as.
|
||
|
|
func listing(
|
||
|
|
slug: String,
|
||
|
|
shop: Shop,
|
||
|
|
at date: Date
|
||
|
|
) -> Listing {
|
||
|
|
.init(
|
||
|
|
slug: slug,
|
||
|
|
shop: shop,
|
||
|
|
title: title,
|
||
|
|
seriesTitle: isSingleIssue ? "\(series) (\(year) series)" : series,
|
||
|
|
issueLabel: issue.isEmpty ? "—" : (isSingleIssue ? "#\(issue)" : issue),
|
||
|
|
publisher: publisher,
|
||
|
|
year: year,
|
||
|
|
format: format,
|
||
|
|
printing: printingLabel,
|
||
|
|
printingDetail: printingDetail,
|
||
|
|
isFirstPrint: printing == 1,
|
||
|
|
grader: grader,
|
||
|
|
grade: grade,
|
||
|
|
condition: condition,
|
||
|
|
certNumber: certNumber,
|
||
|
|
pageQuality: pageQuality,
|
||
|
|
keyNotes: keyNotes,
|
||
|
|
sellerNotes: sellerNotes,
|
||
|
|
priceCents: priceCents,
|
||
|
|
shippingCents: shippingCents,
|
||
|
|
acceptsOffers: acceptsOffers,
|
||
|
|
cover: cover,
|
||
|
|
listedAt: date,
|
||
|
|
defects: defects,
|
||
|
|
photos: photos
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// A string as a URL slug: lowercased, its letters and digits kept, everything else a single hyphen.
|
||
|
|
static func slug(
|
||
|
|
_ string: String
|
||
|
|
) -> String {
|
||
|
|
string
|
||
|
|
.lowercased()
|
||
|
|
.folding(options: .diacriticInsensitive, locale: nil)
|
||
|
|
.split { !($0.isASCII && ($0.isLetter || $0.isNumber)) }
|
||
|
|
.joined(separator: "-")
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|