Add import on tvOS, other export/import improvements

This commit is contained in:
Arkadiusz Fal 2024-02-03 21:04:37 +01:00
parent 0216c17b95
commit e6deb9ef26
8 changed files with 230 additions and 159 deletions

View File

@ -2,15 +2,11 @@ import Defaults
import Foundation import Foundation
import SwiftyJSON import SwiftyJSON
struct ImportSettingsFileModel { final class ImportSettingsFileModel: ObservableObject {
let url: URL static let shared = ImportSettingsFileModel()
var filename: String {
String(url.lastPathComponent.dropLast(ImportExportSettingsModel.settingsExtension.count + 1))
}
var locationsSettingsGroupImporter: LocationsSettingsGroupImporter? { var locationsSettingsGroupImporter: LocationsSettingsGroupImporter? {
if let locationsSettings = json?.dictionaryValue["locationsSettings"] { if let locationsSettings = json.dictionaryValue["locationsSettings"] {
return LocationsSettingsGroupImporter( return LocationsSettingsGroupImporter(
json: locationsSettings, json: locationsSettings,
includePublicLocations: importExportModel.isGroupEnabled(.locationsSettings), includePublicLocations: importExportModel.isGroupEnabled(.locationsSettings),
@ -25,6 +21,8 @@ struct ImportSettingsFileModel {
var importExportModel = ImportExportSettingsModel.shared var importExportModel = ImportExportSettingsModel.shared
var sheetViewModel = ImportSettingsSheetViewModel.shared var sheetViewModel = ImportSettingsSheetViewModel.shared
var loadTask: URLSessionTask?
func isGroupIncludedInFile(_ group: ImportExportSettingsModel.ExportGroup) -> Bool { func isGroupIncludedInFile(_ group: ImportExportSettingsModel.ExportGroup) -> Bool {
switch group { switch group {
case .locationsSettings: case .locationsSettings:
@ -48,7 +46,7 @@ struct ImportSettingsFileModel {
} }
func groupJSON(_ group: ImportExportSettingsModel.ExportGroup) -> JSON { func groupJSON(_ group: ImportExportSettingsModel.ExportGroup) -> JSON {
json?.dictionaryValue[group.rawValue] ?? .init() json.dictionaryValue[group.rawValue] ?? .init()
} }
func performImport() { func performImport() {
@ -91,17 +89,34 @@ struct ImportSettingsFileModel {
} }
} }
var json: JSON? { @Published var json = JSON()
if let fileContents = try? Data(contentsOf: url),
let json = try? JSON(data: fileContents) func loadData(_ url: URL) {
{ json = JSON()
return json loadTask?.cancel()
loadTask = URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
guard let data else { return }
if let json = try? JSON(data: data) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.json = json
self.sheetViewModel.reset(locationsSettingsGroupImporter)
self.importExportModel.reset(self)
} }
return nil }
}
loadTask?.resume()
}
func filename(_ url: URL) -> String {
String(url.lastPathComponent.dropLast(ImportExportSettingsModel.settingsExtension.count + 1))
} }
var metadataBuild: String? { var metadataBuild: String? {
if let build = json?.dictionaryValue["metadata"]?.dictionaryValue["build"]?.string { if let build = json.dictionaryValue["metadata"]?.dictionaryValue["build"]?.string {
return build return build
} }
@ -109,7 +124,7 @@ struct ImportSettingsFileModel {
} }
var metadataPlatform: String? { var metadataPlatform: String? {
if let platform = json?.dictionaryValue["metadata"]?.dictionaryValue["platform"]?.string { if let platform = json.dictionaryValue["metadata"]?.dictionaryValue["platform"]?.string {
return platform return platform
} }
@ -117,7 +132,7 @@ struct ImportSettingsFileModel {
} }
var metadataDate: String? { var metadataDate: String? {
if let timestamp = json?.dictionaryValue["metadata"]?.dictionaryValue["timestamp"]?.doubleValue { if let timestamp = json.dictionaryValue["metadata"]?.dictionaryValue["timestamp"]?.doubleValue {
let date = Date(timeIntervalSince1970: timestamp) let date = Date(timeIntervalSince1970: timestamp)
return dateFormatter.string(from: date) return dateFormatter.string(from: date)
} }

View File

@ -14,11 +14,6 @@ struct OpenURLHandler {
var navigationStyle: NavigationStyle var navigationStyle: NavigationStyle
func handle(_ url: URL) { func handle(_ url: URL) {
if url.isFileURL, url.standardizedFileURL.absoluteString.hasSuffix(".\(ImportExportSettingsModel.settingsExtension)") {
navigation.presentSettingsImportSheet(url)
return
}
if Self.firstHandle { if Self.firstHandle {
Self.firstHandle = false Self.firstHandle = false
@ -26,6 +21,11 @@ struct OpenURLHandler {
return return
} }
if url.isFileURL, url.standardizedFileURL.absoluteString.hasSuffix(".\(ImportExportSettingsModel.settingsExtension)") {
navigation.presentSettingsImportSheet(url)
return
}
if accounts.current.isNil { if accounts.current.isNil {
accounts.setCurrent(accounts.any) accounts.setCurrent(accounts.any)
} }

View File

@ -30,6 +30,13 @@ struct ExportSettings: View {
#endif #endif
#endif #endif
} }
#if os(iOS)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
exportButton
}
}
#endif
.navigationTitle("Export Settings") .navigationTitle("Export Settings")
} }
@ -106,12 +113,6 @@ struct ExportSettings: View {
ExportGroupRow(group: group) ExportGroupRow(group: group)
} }
} }
#if !os(macOS)
Section {
exportButton
}
#endif
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.disabled(model.isExportInProgress) .disabled(model.isExportInProgress)
@ -119,7 +120,7 @@ struct ExportSettings: View {
var exportButton: some View { var exportButton: some View {
Button(action: exportSettings) { Button(action: exportSettings) {
Label(model.isExportInProgress ? "Export in progress..." : "Export...", systemImage: model.isExportInProgress ? "fireworks" : "square.and.arrow.up") Text(model.isExportInProgress ? "In progress..." : "Export")
.animation(nil, value: model.isExportInProgress) .animation(nil, value: model.isExportInProgress)
#if !os(macOS) #if !os(macOS)
.foregroundColor(.accentColor) .foregroundColor(.accentColor)

View File

@ -0,0 +1,34 @@
import SwiftUI
struct ImportSettings: View {
@State private var fileURL = ""
var body: some View {
VStack(spacing: 100) {
VStack(alignment: .leading, spacing: 20) {
Text("1. Export settings from Yattee for iOS or macOS")
Text("2. Upload it to a file hosting (e. g. Pastebin or GitHub Gist)")
Text("3. Enter file URL in the field below. You can use iOS remote to paste.")
}
TextField("URL", text: $fileURL)
Button {
if let url = URL(string: fileURL) {
NavigationModel.shared.presentSettingsImportSheet(url)
}
} label: {
Text("Import")
}
}
.padding(20)
.navigationTitle("Import Settings")
}
}
struct ImportSettings_Previews: PreviewProvider {
static var previews: some View {
ImportSettings()
}
}

View File

@ -27,10 +27,20 @@ struct ImportSettingsAccountRow: View {
} }
var body: some View { var body: some View {
#if os(tvOS)
row
#else
Button(action: { model.toggleAccount(account, accounts: accounts) }) { Button(action: { model.toggleAccount(account, accounts: accounts) }) {
row
}
.buttonStyle(.plain)
#endif
}
var row: some View {
let accountExists = AccountsModel.shared.find(account.id) != nil let accountExists = AccountsModel.shared.find(account.id) != nil
VStack(alignment: .leading) { return VStack(alignment: .leading) {
HStack { HStack {
Text(account.username) Text(account.username)
Spacer() Spacer()
@ -122,8 +132,6 @@ struct ImportSettingsAccountRow: View {
.onChange(of: isValid) { _ in afterValidation() } .onChange(of: isValid) { _ in afterValidation() }
.animation(nil, value: isChecked) .animation(nil, value: isChecked)
} }
.buttonStyle(.plain)
}
var isChecked: Bool { var isChecked: Bool {
model.isSelectedForImport(account) model.isSelectedForImport(account)
@ -173,7 +181,8 @@ struct ImportSettingsAccountRow: View {
struct ImportSettingsAccountRow_Previews: PreviewProvider { struct ImportSettingsAccountRow_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let fileModel = ImportSettingsFileModel(url: URL(string: "https://gist.githubusercontent.com/arekf/578668969c9fdef1b3828bea864c3956/raw/f794a95a20261bcb1145e656c8dda00bea339e2a/yattee-recents.yatteesettings")!) let fileModel = ImportSettingsFileModel()
fileModel.loadData(URL(string: "https://gist.githubusercontent.com/arekf/578668969c9fdef1b3828bea864c3956/raw/f794a95a20261bcb1145e656c8dda00bea339e2a/yattee-recents.yatteesettings")!)
return List { return List {
ImportSettingsAccountRow( ImportSettingsAccountRow(

View File

@ -4,6 +4,7 @@ struct ImportSettingsSheetView: View {
@Binding var settingsFile: URL? @Binding var settingsFile: URL?
@StateObject private var model = ImportSettingsSheetViewModel.shared @StateObject private var model = ImportSettingsSheetViewModel.shared
@StateObject private var importExportModel = ImportExportSettingsModel.shared @StateObject private var importExportModel = ImportExportSettingsModel.shared
@StateObject private var fileModel = ImportSettingsFileModel.shared
@Environment(\.presentationMode) private var presentationMode @Environment(\.presentationMode) private var presentationMode
@ -23,12 +24,12 @@ struct ImportSettingsSheetView: View {
#endif #endif
} }
.onAppear { .onAppear {
guard let fileModel else { return } guard let settingsFile else { return }
model.reset(fileModel.locationsSettingsGroupImporter) fileModel.loadData(settingsFile)
importExportModel.reset(fileModel)
} }
.onChange(of: settingsFile) { _ in .onChange(of: settingsFile) { _ in
importExportModel.reset(fileModel) guard let settingsFile else { return }
fileModel.loadData(settingsFile)
} }
} }
@ -56,7 +57,7 @@ struct ImportSettingsSheetView: View {
} }
ToolbarItem(placement: .confirmationAction) { ToolbarItem(placement: .confirmationAction) {
Button(action: { Button(action: {
fileModel?.performImport() fileModel.performImport()
presentingCompletedAlert = true presentingCompletedAlert = true
ImportExportSettingsModel.shared.reset() ImportExportSettingsModel.shared.reset()
}) { }) {
@ -85,16 +86,8 @@ struct ImportSettingsSheetView: View {
return !model.selectedAccounts.isEmpty || !model.selectedInstances.isEmpty || !importExportModel.selectedExportGroups.isEmpty return !model.selectedAccounts.isEmpty || !model.selectedInstances.isEmpty || !importExportModel.selectedExportGroups.isEmpty
} }
var fileModel: ImportSettingsFileModel? {
guard let settingsFile else { return nil }
return ImportSettingsFileModel(url: settingsFile)
}
var locationsSettingsGroupImporter: LocationsSettingsGroupImporter? { var locationsSettingsGroupImporter: LocationsSettingsGroupImporter? {
guard let fileModel else { return nil } fileModel.locationsSettingsGroupImporter
return fileModel.locationsSettingsGroupImporter
} }
struct ExportGroupRow: View { struct ExportGroupRow: View {
@ -128,34 +121,43 @@ struct ImportSettingsSheetView: View {
Section(header: Text("Settings")) { Section(header: Text("Settings")) {
ForEach(ImportExportSettingsModel.ExportGroup.settingsGroups) { group in ForEach(ImportExportSettingsModel.ExportGroup.settingsGroups) { group in
ExportGroupRow(group: group) ExportGroupRow(group: group)
.disabled(!fileModel!.isGroupIncludedInFile(group)) .disabled(!fileModel.isGroupIncludedInFile(group))
} }
} }
Section(header: Text("Other")) { Section(header: Text("Other")) {
ForEach(ImportExportSettingsModel.ExportGroup.otherGroups) { group in ForEach(ImportExportSettingsModel.ExportGroup.otherGroups) { group in
ExportGroupRow(group: group) ExportGroupRow(group: group)
.disabled(!fileModel!.isGroupIncludedInFile(group)) .disabled(!fileModel.isGroupIncludedInFile(group))
} }
} }
} }
} }
@ViewBuilder var metadata: some View { @ViewBuilder var metadata: some View {
if let fileModel { if let settingsFile {
Section(header: Text("File information")) { Section(header: Text("File information")) {
MetadataRow(name: Text("Name"), value: Text(fileModel.filename)) MetadataRow(name: Text("Name"), value: Text(fileModel.filename(settingsFile)))
if let date = fileModel.metadataDate { if let date = fileModel.metadataDate {
MetadataRow(name: Text("Date"), value: Text(date)) MetadataRow(name: Text("Date"), value: Text(date))
#if os(tvOS)
.focusable()
#endif
} }
if let build = fileModel.metadataBuild { if let build = fileModel.metadataBuild {
MetadataRow(name: Text("Build"), value: Text(build)) MetadataRow(name: Text("Build"), value: Text(build))
#if os(tvOS)
.focusable()
#endif
} }
if let platform = fileModel.metadataPlatform { if let platform = fileModel.metadataPlatform {
MetadataRow(name: Text("Platform"), value: Text(platform)) MetadataRow(name: Text("Platform"), value: Text(platform))
#if os(tvOS)
.focusable()
#endif
} }
} }
} }
@ -231,7 +233,6 @@ struct ImportSettingsSheetView: View {
} }
@ViewBuilder var importOptions: some View { @ViewBuilder var importOptions: some View {
if let fileModel {
if fileModel.isPublicInstancesSettingsGroupInFile || !instances.isEmpty { if fileModel.isPublicInstancesSettingsGroupInFile || !instances.isEmpty {
Section(header: Text("Locations")) { Section(header: Text("Locations")) {
if fileModel.isPublicInstancesSettingsGroupInFile { if fileModel.isPublicInstancesSettingsGroupInFile {
@ -252,7 +253,6 @@ struct ImportSettingsSheetView: View {
} }
} }
} }
}
} }
struct ImportSettingsSheetView_Previews: PreviewProvider { struct ImportSettingsSheetView_Previews: PreviewProvider {

View File

@ -31,9 +31,9 @@ struct SettingsView: View {
var body: some View { var body: some View {
settings settings
.modifier(ImportSettingsSheetViewModifier(isPresented: $settingsModel.presentingSettingsImportSheet, settingsFile: $settingsModel.settingsImportURL))
#if !os(tvOS) #if !os(tvOS)
.modifier(ImportSettingsFileImporterViewModifier(isPresented: $navigation.presentingSettingsFileImporter)) .modifier(ImportSettingsFileImporterViewModifier(isPresented: $navigation.presentingSettingsFileImporter))
.modifier(ImportSettingsSheetViewModifier(isPresented: $settingsModel.presentingSettingsImportSheet, settingsFile: $settingsModel.settingsImportURL))
#endif #endif
#if os(iOS) #if os(iOS)
.backport .backport
@ -281,6 +281,13 @@ struct SettingsView: View {
var importView: some View { var importView: some View {
Section { Section {
#if os(tvOS)
NavigationLink(destination: LazyView(ImportSettings())) {
Label("Import Settings", systemImage: "square.and.arrow.down")
.labelStyle(SettingsLabel())
}
.padding(.horizontal, 20)
#else
Button(action: importSettings) { Button(action: importSettings) {
Label("Import Settings...", systemImage: "square.and.arrow.down") Label("Import Settings...", systemImage: "square.and.arrow.down")
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
@ -294,6 +301,7 @@ struct SettingsView: View {
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
#endif
} }
} }

View File

@ -662,6 +662,7 @@
37A5DBC8285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */; }; 37A5DBC8285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */; };
37A5DBC9285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */; }; 37A5DBC9285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */; };
37A5DBCA285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */; }; 37A5DBCA285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */; };
37A6D4ED2B6E372700B26299 /* ImportSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A6D4EC2B6E372700B26299 /* ImportSettings.swift */; };
37A7D6E32B67E303009CB1ED /* ImportSettingsFileModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372C74692B67098A00BE179B /* ImportSettingsFileModel.swift */; }; 37A7D6E32B67E303009CB1ED /* ImportSettingsFileModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372C74692B67098A00BE179B /* ImportSettingsFileModel.swift */; };
37A7D6E52B67E315009CB1ED /* SettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6E42B67E315009CB1ED /* SettingsGroupExporter.swift */; }; 37A7D6E52B67E315009CB1ED /* SettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6E42B67E315009CB1ED /* SettingsGroupExporter.swift */; };
37A7D6E62B67E315009CB1ED /* SettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6E42B67E315009CB1ED /* SettingsGroupExporter.swift */; }; 37A7D6E62B67E315009CB1ED /* SettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6E42B67E315009CB1ED /* SettingsGroupExporter.swift */; };
@ -1360,6 +1361,7 @@
37A362BD29537AAA00BDF328 /* PlaybackSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackSettings.swift; sourceTree = "<group>"; }; 37A362BD29537AAA00BDF328 /* PlaybackSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackSettings.swift; sourceTree = "<group>"; };
37A362C129537FED00BDF328 /* PlaybackSettingsPresentationDetents+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlaybackSettingsPresentationDetents+Backport.swift"; sourceTree = "<group>"; }; 37A362C129537FED00BDF328 /* PlaybackSettingsPresentationDetents+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlaybackSettingsPresentationDetents+Backport.swift"; sourceTree = "<group>"; };
37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlBackgroundModifier.swift; sourceTree = "<group>"; }; 37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlBackgroundModifier.swift; sourceTree = "<group>"; };
37A6D4EC2B6E372700B26299 /* ImportSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportSettings.swift; sourceTree = "<group>"; };
37A7D6E42B67E315009CB1ED /* SettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsGroupExporter.swift; sourceTree = "<group>"; }; 37A7D6E42B67E315009CB1ED /* SettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsGroupExporter.swift; sourceTree = "<group>"; };
37A7D6E82B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingSettingsGroupExporter.swift; sourceTree = "<group>"; }; 37A7D6E82B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingSettingsGroupExporter.swift; sourceTree = "<group>"; };
37A7D6EC2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingSettingsGroupImporter.swift; sourceTree = "<group>"; }; 37A7D6EC2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingSettingsGroupImporter.swift; sourceTree = "<group>"; };
@ -2162,6 +2164,7 @@
37BBB33D2B6B9C80001C4845 /* Import */ = { 37BBB33D2B6B9C80001C4845 /* Import */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
37A6D4EC2B6E372700B26299 /* ImportSettings.swift */,
37BBB3422B6BB88F001C4845 /* ImportSettingsAccountRow.swift */, 37BBB3422B6BB88F001C4845 /* ImportSettingsAccountRow.swift */,
372C74622B66FFFC00BE179B /* ImportSettingsFileImporterViewModifier.swift */, 372C74622B66FFFC00BE179B /* ImportSettingsFileImporterViewModifier.swift */,
37BBB33E2B6B9D52001C4845 /* ImportSettingsSheetView.swift */, 37BBB33E2B6B9D52001C4845 /* ImportSettingsSheetView.swift */,
@ -3840,6 +3843,7 @@
37E80F45287B7AC000561799 /* ControlsBar.swift in Sources */, 37E80F45287B7AC000561799 /* ControlsBar.swift in Sources */,
3743CA50270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */, 3743CA50270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
376BE50827347B57009AD608 /* SettingsHeader.swift in Sources */, 376BE50827347B57009AD608 /* SettingsHeader.swift in Sources */,
37A6D4ED2B6E372700B26299 /* ImportSettings.swift in Sources */,
37A9966026D6F9B9006E3224 /* HomeView.swift in Sources */, 37A9966026D6F9B9006E3224 /* HomeView.swift in Sources */,
372820402945E4A8009A0E2D /* SubscriptionsPageButton.swift in Sources */, 372820402945E4A8009A0E2D /* SubscriptionsPageButton.swift in Sources */,
37001565271B1F250049C794 /* AccountsModel.swift in Sources */, 37001565271B1F250049C794 /* AccountsModel.swift in Sources */,