Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions DJDX/Games/beatmania IIDX/Data Types/IIDXSongRecord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ final class IIDXSongRecord: Equatable, Hashable, @unchecked Sendable {

var importGroup: ImportGroup?

// Row id of the backing SQLite record, when fetched from PlayData.db.
// Used to target manual (INFINITAS) entries for edit/delete. Not persisted
// by SwiftData since storage is handled by the custom SQLite layer.
@Transient var databaseID: Int64?

init() {
// Empty default initializer required by SwiftData
}
Expand Down
15 changes: 14 additions & 1 deletion DJDX/Games/beatmania IIDX/Data Types/IIDXVersion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,19 @@ enum IIDXVersion: Int, Codable, CaseIterable {
case epolis = 31
case pinkyCrush = 32
case sparkleShower = 33
// Home (PC) version. Has no e-amusement export, so scores are entered manually.
// Sentinel raw value kept clearly outside the arcade numbering range.
case infinitas = 1000

static var supportedVersions: [IIDXVersion] {
[.epolis, .pinkyCrush, .sparkleShower]
// INFINITAS is placed first so that, after the picker reverses the list,
// it appears as the last (bottom) entry.
[.infinitas, .epolis, .pinkyCrush, .sparkleShower]
}

// Whether this version is the manually-tracked home version (no import path).
var isManualEntry: Bool {
self == .infinitas
}

var marketingName: String {
Expand Down Expand Up @@ -75,6 +85,7 @@ enum IIDXVersion: Int, Codable, CaseIterable {
case .epolis: return "EPOLIS"
case .pinkyCrush: return "Pinky Crush"
case .sparkleShower: return "Sparkle Shower"
case .infinitas: return "INFINITAS"
}
}

Expand Down Expand Up @@ -119,6 +130,7 @@ enum IIDXVersion: Int, Codable, CaseIterable {
case .epolis: return UIColor(red: 50 / 255, green: 50 / 255, blue: 50 / 255, alpha: 1.0)
case .pinkyCrush: return UIColor(red: 249 / 255, green: 87 / 255, blue: 142 / 255, alpha: 1.0)
case .sparkleShower: return UIColor(red: 67 / 255, green: 143 / 255, blue: 82 / 255, alpha: 1.0)
case .infinitas: return UIColor(red: 88 / 255, green: 86 / 255, blue: 214 / 255, alpha: 1.0)
}
}

Expand Down Expand Up @@ -157,6 +169,7 @@ enum IIDXVersion: Int, Codable, CaseIterable {
case .epolis: return UIColor(red: 240 / 255, green: 254 / 255, blue: 0 / 255, alpha: 1.0)
case .pinkyCrush: return UIColor(red: 1.0, green: 97 / 255, blue: 178 / 255, alpha: 1.0)
case .sparkleShower: return UIColor(red: 173 / 255, green: 227 / 255, blue: 77 / 255, alpha: 1.0)
case .infinitas: return UIColor(red: 153 / 255, green: 151 / 255, blue: 1.0, alpha: 1.0)
}
}

Expand Down
100 changes: 100 additions & 0 deletions DJDX/Games/beatmania IIDX/IIDXImporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,46 @@ actor IIDXImporter {
return newID
}

// MARK: Manual Entry (INFINITAS)

// Resolves (creating if needed) the single, date-agnostic import group that
// holds all manually-entered INFINITAS records.
func infinitasImportGroupID(database: Connection) -> String {
let col = IIDXPlayDataDatabase.self
let query = col.importGroupTable
.filter(col.igIIDXVersion == IIDXVersion.infinitas.rawValue)
.limit(1)

if let row = try? database.pluck(query) {
return row[col.igID]
}

let newID = UUID().uuidString
_ = try? database.run(col.importGroupTable.insert(
col.igID <- newID,
col.igImportDate <- Date.now.timeIntervalSince1970,
col.igIIDXVersion <- IIDXVersion.infinitas.rawValue
))
return newID
}

func addManualSongRecord(_ record: IIDXSongRecord) {
guard let database = try? IIDXPlayDataDatabase.shared.getWriteConnection() else { return }
let importGroupID = infinitasImportGroupID(database: database)
Self.insertSongRecord(database: database, record: record, importGroupID: importGroupID)
}

func updateSongRecord(id: Int64, _ record: IIDXSongRecord) {
guard let database = try? IIDXPlayDataDatabase.shared.getWriteConnection() else { return }
Self.updateSongRecord(database: database, id: id, record: record)
}

func deleteSongRecord(id: Int64) {
guard let database = try? IIDXPlayDataDatabase.shared.getWriteConnection() else { return }
let col = IIDXPlayDataDatabase.self
_ = try? database.run(col.songRecordTable.filter(col.srID == id).delete())
}

// MARK: Insert Helpers

static func insertSongRecord(database: Connection, record: IIDXSongRecord, importGroupID: String) {
Expand Down Expand Up @@ -266,6 +306,66 @@ actor IIDXImporter {
))
}

// swiftlint:disable:next function_body_length
static func updateSongRecord(database: Connection, id: Int64, record: IIDXSongRecord) {
let col = IIDXPlayDataDatabase.self
let row = col.songRecordTable.filter(col.srID == id)
_ = try? database.run(row.update(
col.srVersion <- record.version,
col.srTitle <- record.title,
col.srGenre <- record.genre,
col.srArtist <- record.artist,
col.srPlayCount <- record.playCount,
col.srPlayType <- record.playType.rawValue,
col.srLastPlayDate <- record.lastPlayDate.timeIntervalSince1970,
// Beginner
col.srBeginnerLevel <- record.beginnerScore.level.code(),
col.srBeginnerDifficulty <- record.beginnerScore.difficulty,
col.srBeginnerScore <- record.beginnerScore.score,
col.srBeginnerPerfectGreatCount <- record.beginnerScore.perfectGreatCount,
col.srBeginnerGreatCount <- record.beginnerScore.greatCount,
col.srBeginnerMissCount <- record.beginnerScore.missCount,
col.srBeginnerClearType <- record.beginnerScore.clearType,
col.srBeginnerDJLevel <- record.beginnerScore.djLevel,
// Normal
col.srNormalLevel <- record.normalScore.level.code(),
col.srNormalDifficulty <- record.normalScore.difficulty,
col.srNormalScore <- record.normalScore.score,
col.srNormalPerfectGreatCount <- record.normalScore.perfectGreatCount,
col.srNormalGreatCount <- record.normalScore.greatCount,
col.srNormalMissCount <- record.normalScore.missCount,
col.srNormalClearType <- record.normalScore.clearType,
col.srNormalDJLevel <- record.normalScore.djLevel,
// Hyper
col.srHyperLevel <- record.hyperScore.level.code(),
col.srHyperDifficulty <- record.hyperScore.difficulty,
col.srHyperScore <- record.hyperScore.score,
col.srHyperPerfectGreatCount <- record.hyperScore.perfectGreatCount,
col.srHyperGreatCount <- record.hyperScore.greatCount,
col.srHyperMissCount <- record.hyperScore.missCount,
col.srHyperClearType <- record.hyperScore.clearType,
col.srHyperDJLevel <- record.hyperScore.djLevel,
// Another
col.srAnotherLevel <- record.anotherScore.level.code(),
col.srAnotherDifficulty <- record.anotherScore.difficulty,
col.srAnotherScore <- record.anotherScore.score,
col.srAnotherPerfectGreatCount <- record.anotherScore.perfectGreatCount,
col.srAnotherGreatCount <- record.anotherScore.greatCount,
col.srAnotherMissCount <- record.anotherScore.missCount,
col.srAnotherClearType <- record.anotherScore.clearType,
col.srAnotherDJLevel <- record.anotherScore.djLevel,
// Leggendaria
col.srLeggendariaLevel <- record.leggendariaScore.level.code(),
col.srLeggendariaDifficulty <- record.leggendariaScore.difficulty,
col.srLeggendariaScore <- record.leggendariaScore.score,
col.srLeggendariaPerfectGreatCount <- record.leggendariaScore.perfectGreatCount,
col.srLeggendariaGreatCount <- record.leggendariaScore.greatCount,
col.srLeggendariaMissCount <- record.leggendariaScore.missCount,
col.srLeggendariaClearType <- record.leggendariaScore.clearType,
col.srLeggendariaDJLevel <- record.leggendariaScore.djLevel
))
}

static func insertSongToBEMANIWiki(database: Connection, song: IIDXSong) {
let col = BEMANIWikiDatabase.self
_ = try? database.run(col.songTable.insert(
Expand Down
10 changes: 9 additions & 1 deletion DJDX/Games/beatmania IIDX/IIDXReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ actor IIDXReader {
// displayed data follows the selected version (matching the trends queries).
func importGroup(for selectedDate: Date, version: IIDXVersion?) -> ImportGroup? {
guard let version else { return importGroup(for: selectedDate) }
// INFINITAS is a manually-tracked persistent collection: always return its
// single import group regardless of the selected date.
if version == .infinitas {
return importGroups(for: .infinitas).first
}
let groups = importGroups(for: version)
guard !groups.isEmpty else { return nil }

Expand Down Expand Up @@ -133,7 +138,9 @@ actor IIDXReader {
return nil
}

if importGroupID != importGroup.id {
// For INFINITAS the group id never changes across manual add/edit/delete,
// so always re-query to reflect the latest entries.
if importGroupID != importGroup.id || version == .infinitas {
importGroupID = importGroup.id
previousFilters = nil
previousSortOptions = nil
Expand Down Expand Up @@ -567,6 +574,7 @@ actor IIDXReader {

static func songRecord(from row: Row) -> IIDXSongRecord {
let record = IIDXSongRecord()
record.databaseID = row[IIDXPlayDataDatabase.srID]
record.version = row[IIDXPlayDataDatabase.srVersion]
record.title = row[IIDXPlayDataDatabase.srTitle]
record.genre = row[IIDXPlayDataDatabase.srGenre]
Expand Down
26 changes: 22 additions & 4 deletions DJDX/Views/UnifiedView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ struct UnifiedView: View {
@AppStorage(wrappedValue: 0, "Review.LaunchCount", store: .standard) var launchCount: Int

@State var isPresentingImport: Bool = false
@State var isPresentingScoreEditor: Bool = false
@State var isFirstStartCleanupComplete: Bool = false
@State var isEditingAnalytics: Bool = false

Expand All @@ -34,6 +35,11 @@ struct UnifiedView: View {
@Namespace var towerNamespace
@Namespace var importNamespace

// INFINITAS has no import path; scores are added manually via a `+` button.
var isManualEntryMode: Bool {
selectedGame == .iidxArcade && iidxVersion.isManualEntry
}

var body: some View {
@Bindable var progressAlertManager = progressAlertManager
NavigationStack(path: $navigationManager.path) {
Expand All @@ -59,11 +65,18 @@ struct UnifiedView: View {
gameMenu
}
ToolbarItemGroup(placement: .topBarLeading) {
Button("Shared.Import", systemImage: "arrow.down.circle.dotted") {
isPresentingImport = true
if isManualEntryMode {
Button("Scores.ManualEntry.Add.Title", systemImage: "plus") {
isPresentingScoreEditor = true
}
.automaticSheetMatchedTransitionSource(id: "Import", in: importNamespace)
} else {
Button("Shared.Import", systemImage: "arrow.down.circle.dotted") {
isPresentingImport = true
}
.popoverTip(ImportMovedTip(), arrowEdge: .top)
.automaticSheetMatchedTransitionSource(id: "Import", in: importNamespace)
}
.popoverTip(ImportMovedTip(), arrowEdge: .top)
.automaticSheetMatchedTransitionSource(id: "Import", in: importNamespace)
}
ToolbarItemGroup(placement: .topBarTrailing) {
Button {
Expand Down Expand Up @@ -118,6 +131,11 @@ struct UnifiedView: View {
.presentationDetents([.large])
.interactiveDismissDisabled()
}
.sheet(isPresented: $isPresentingScoreEditor) {
IIDXInfinitasScoreEditor()
.automaticSheetNavigationTransition(id: "Import", in: importNamespace)
.presentationDetents([.large])
}
.overlay {
if progressAlertManager.isShowing {
ProgressAlert(
Expand Down
Loading