Skip to content
This repository has been archived by the owner on Nov 17, 2024. It is now read-only.

Repo // Add UIGFv4 support. #203

Merged
merged 6 commits into from
Jul 13, 2024
Merged
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
47 changes: 47 additions & 0 deletions Features/Gacha/Impl/UIGFImpl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// (c) 2023 and onwards Pizza Studio (GPL v3.0 License).
// ====================
// This code is released under the GPL v3.0 License (SPDX-License-Identifier: GPL-3.0)

import CoreData
import Defaults
import EnkaKitHSR
import Foundation
import GachaKit
import HBMihoyoAPI
import SwiftUI

extension GachaItemMO {
public func toUIGFEntry(
langOverride: GachaLanguageCode? = nil,
timeZoneDeltaOverride: Int? = nil
)
-> UIGFv4.DataEntry {
toEntry().toUIGFEntry(
langOverride: langOverride,
timeZoneDeltaOverride: timeZoneDeltaOverride
)
}
}

extension UIGFv4.DataEntry {
public func toManagedModel(
uid: String,
lang: GachaLanguageCode?,
timeZoneDelta: Int
)
-> GachaItemMO {
let rawResult = toGachaEntry(uid: uid, lang: lang, timeZoneDelta: timeZoneDelta)
return rawResult.toManagedModel()
}

public func toManagedModel(
uid: String,
lang: GachaLanguageCode?,
timeZoneDelta: Int,
context: NSManagedObjectContext
)
-> GachaItemMO {
let rawResult = toGachaEntry(uid: uid, lang: lang, timeZoneDelta: timeZoneDelta)
return rawResult.toManagedModel(context: context)
}
}
4 changes: 2 additions & 2 deletions Features/Gacha/View/GachaView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ struct GachaView: View {
NavigationLink("gacha.home.manage_gacha_record") {
ManageGachaRecordView()
}
NavigationLink("gacha.manage.srgf.import") {
NavigationLink("gacha.manage.uigf.import") {
ImportGachaView()
}
NavigationLink("gacha.manage.srgf.export") {
NavigationLink("gacha.manage.uigf.export") {
ExportGachaView()
}.disabled(noDataAvailable)
}
Expand Down
212 changes: 147 additions & 65 deletions Features/Gacha/View/SRGF/ExportGachaView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,7 @@ struct ExportGachaView: View {
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("app.gacha.data.export.button") {
exportButtonClicked()
}
.disabled(params.uid == nil)
exportButton()
}
}
}
Expand All @@ -45,49 +42,38 @@ struct ExportGachaView: View {
isPresented: $isSucceedAlertShown,
presenting: alert,
actions: { _ in
Button("button.okay") {
isSucceedAlertShown = false
}
Button("button.okay") { isSucceedAlertShown = false }
},
message: { thisAlert in
switch thisAlert {
case let .succeed(url):
Text("gacha.export.fileSavedTo:\(url)")
default:
EmptyView()
}
message: { _ in
postAlertMessage()
}
)
.alert(
"gacha.export.failedInSavingToFile",
isPresented: $isFailureAlertShown,
presenting: alert,
actions: { _ in
Button("button.okay") {
isFailureAlertShown = false
}
Button("button.okay") { isSucceedAlertShown = false }
},
message: { thisAlert in
switch thisAlert {
case let .failure(error):
Text("错误信息:\(error)")
default:
EmptyView()
}
message: { _ in
postAlertMessage()
}
)
.fileExporter(
isPresented: $isExporterPresented,
document: file,
isPresented: $isSRGFExporterPresented,
document: srgfJson?.asDocument,
contentType: .json,
defaultFilename: defaultFileName
defaultFilename: fileNameStem
) { result in
switch result {
case let .success(url):
alert = .succeed(url: url.absoluteString)
case let .failure(failure):
alert = .failure(message: failure.localizedDescription)
}
handleFileExporterResult(result)
}
.fileExporter(
isPresented: $isUIGFExporterPresented,
document: uigfJson?.asDocument,
contentType: .json,
defaultFilename: fileNameStem
) { result in
handleFileExporterResult(result)
}
}

Expand All @@ -103,6 +89,10 @@ struct ExportGachaView: View {
Text(code.localized).tag(code)
}
}
Picker("gacha.export.chooseFormat", selection: $currentFormat) {
Text(verbatim: "UIGFv4").tag(UIGFFormat.uigfv4)
Text(verbatim: "SRGFv1").tag(UIGFFormat.srgfv1)
}
} footer: {
Text("app.gacha.srgf.affLink.[SRGF](https://uigf.org/)")
}
Expand All @@ -111,31 +101,30 @@ struct ExportGachaView: View {

@ViewBuilder
func compactMain() -> some View {
Menu("gacha.manage.srgf.export.toolbarTitle") {
ForEach(GachaLanguageCode.allCases, id: \.rawValue) { code in
Button(code.localized) {
params.lang = code
exportButtonClicked()
Menu("gacha.manage.uigf.export.toolbarTitle") {
Menu {
ForEach(GachaLanguageCode.allCases, id: \.rawValue) { code in
Button(code.localized) {
params.lang = code
exportButtonClicked(format: .uigfv4)
}
}
} label: {
Text(verbatim: "UIGFv4")
}
Menu {
ForEach(GachaLanguageCode.allCases, id: \.rawValue) { code in
Button(code.localized) {
params.lang = code
exportButtonClicked(format: .srgfv1)
}
}
} label: {
Text(verbatim: "SRGFv1")
}
}
}

func exportButtonClicked() {
let uid = params.uid!
let items = fetchAllMO(uid: uid).map {
$0.toSRGFEntry(
langOverride: params.lang,
timeZoneDeltaOverride: nil
)
}
srgfJson = .init(
info: .init(uid: uid, lang: params.lang),
list: items
)
isExporterPresented.toggle()
}

// MARK: Private

@State private var isSucceedAlertShown: Bool = false
Expand All @@ -151,14 +140,30 @@ struct ExportGachaView: View {

@ObservedObject private var params: ExportGachaParams = .init()

@State private var isExporterPresented: Bool = false
@State private var isSRGFExporterPresented: Bool = false
@State private var isUIGFExporterPresented: Bool = false

@State private var srgfJson: SRGFv1?
@State private var uigfJson: UIGFv4?
@State private var currentFormat: UIGFFormat = .uigfv4

private var fileNameStem: String {
switch currentFormat {
case .uigfv4:
return uigfJson?.getFileNameStem(uid: params.uid, for: .starRail) ?? "Untitled"
case .srgfv1:
return srgfJson?.defaultFileNameStem ?? "Untitled"
}
}

private var accountPickerPairs: [(value: String, tag: String?)] {
var result = [(value: String, tag: String?)]()
if params.uid == nil {
let i18nStr = String(localized: .init(stringLiteral: "app.gacha.account.select.notSelected"))
var i18nKey = "app.gacha.account.select.selectAll"
if currentFormat == .srgfv1 {
i18nKey = "app.gacha.account.select.notSelected"
}
let i18nStr = String(localized: .init(stringLiteral: i18nKey))
result.append((i18nStr, nil))
}
result.append(contentsOf: allAvaliableAccountUID.map { uid in
Expand Down Expand Up @@ -187,12 +192,45 @@ struct ExportGachaView: View {
}
}

private var defaultFileName: String {
srgfJson?.defaultFileNameStem ?? "Untitled"
}

private var file: JsonFile? {
srgfJson?.asDocument
private func exportButtonClicked(format: UIGFFormat) {
switch format {
case .uigfv4:
currentFormat = format
srgfJson = nil
if let uid = params.uid {
let itemsUIGF = fetchAllMO(uid: uid).map {
$0.toUIGFEntry(
langOverride: params.lang,
timeZoneDeltaOverride: nil
)
}
let hsrProfile = UIGFv4.ProfileHSR(
lang: params.lang,
list: itemsUIGF,
timezone: nil,
uid: uid
)
uigfJson = .init(info: .init(), hsrProfiles: [hsrProfile])
} else {
uigfJson = exportAllAccountDataIntoSingleUIGFv4()
}
isUIGFExporterPresented.toggle()
case .srgfv1:
guard let uid = params.uid else { return }
currentFormat = format
uigfJson = nil
let itemsSRGF = fetchAllMO(uid: uid).map {
$0.toSRGFEntry(
langOverride: params.lang,
timeZoneDeltaOverride: nil
)
}
srgfJson = .init(
info: .init(uid: uid, lang: params.lang),
list: itemsSRGF
)
isSRGFExporterPresented.toggle()
}
}

@ViewBuilder
Expand All @@ -209,6 +247,32 @@ struct ExportGachaView: View {
private func firstAccount(uid: String) -> Account? {
accounts.first(where: { $0.uid! == uid })
}

private func handleFileExporterResult(_ result: Result<URL, any Error>) {
switch result {
case let .success(url):
alert = .succeed(url: url.absoluteString)
case let .failure(failure):
alert = .failure(message: failure.localizedDescription)
}
}

@ViewBuilder
private func postAlertMessage() -> some View {
switch alert {
case let .succeed(url): Text("gacha.export.fileSavedTo:\(url)")
case let .failure(message): Text(verbatim: "⚠︎ \(message)")
case nil: EmptyView()
}
}

@ViewBuilder
private func exportButton() -> some View {
Button("app.gacha.data.export.button") {
exportButtonClicked(format: currentFormat)
}
.disabled(params.uid == nil && currentFormat == .srgfv1)
}
}

extension ExportGachaView {
Expand Down Expand Up @@ -250,17 +314,35 @@ extension ExportGachaView {
}
}

// MARK: - Batch Export Support (UIGFv4 Only).

extension ExportGachaView {
public func exportAllAccountDataIntoSingleUIGFv4() -> UIGFv4 {
let profiles: [UIGFv4.ProfileHSR] = allAvaliableAccountUID.compactMap { uid in
let itemsUIGF = fetchAllMO(uid: uid).map {
$0.toUIGFEntry(
langOverride: params.lang,
timeZoneDeltaOverride: nil
)
}
return !itemsUIGF.isEmpty ? UIGFv4.ProfileHSR(
lang: params.lang,
list: itemsUIGF,
timezone: nil,
uid: uid
) : nil
}
return .init(info: .init(), hsrProfiles: profiles)
}
}

// MARK: - ExportGachaParams

private class ExportGachaParams: ObservableObject {
@Published var uid: String?
@Published var lang: GachaLanguageCode = .zhHans
}

// MARK: - JsonFile

typealias JsonFile = SRGFv1.Document

// MARK: - AlertType

private enum AlertType: Identifiable {
Expand Down
Loading
Loading