Skip to content

Commit

Permalink
Feat: SourceFolders (PlayCover#1592)
Browse files Browse the repository at this point in the history
* Initial Creation

* Added Search Back + Bug & UI Fixes

* Sort Alphabetically Toggle Option

* Fix Sort Issues + Save to Defaults + `.help`

* Backport Compatibility

* Add `Semaphore` to `README.md`

* SwiftLint

* Various Fixes & Improvements

- Search Field Fix
- Local IPA Source Fix
- Name Local Legacy IPA Sources `localhost`
- Bug where if a source load fails could't load other ones fix
- Added Source List Search

* SwiftLint

* Apply recent UI change

* iTunes Playlist Style Sidebar for Sources

* Collapsible Source Folders list is Sidebar

* Making Collapse Button Smaller

* Reviews

* Update project.pbxproj

* some cleanup and bug fixes

* fix the folders duplicate bug

* rebase with develop branch

* dumbest fix ever

* Update and Fixes

StoreVM SourceResolve Improvement
Switch to NavigationStack package (not mine, should be updated after my PR gets merged)

* fix unwanted comment

* remove redundant strings

* remove searchable from sources list

* Last Commit (Hopefully)

* `try! beLastCommit`

* Icon Unifications + Fix `var` Warn

* Fix + Improvements

1. Makes sure Sidebar width shows IPALibrary completely
2. Marks `appendSourceData()` as a private function
  • Loading branch information
amirsaam authored Aug 27, 2024
1 parent 58e948f commit 079bd57
Show file tree
Hide file tree
Showing 14 changed files with 518 additions and 307 deletions.
2 changes: 1 addition & 1 deletion Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1 +1 @@
github "PlayCover/PlayTools" "v3.0.0"
github "PlayCover/PlayTools" "v3.0.0"
4 changes: 4 additions & 0 deletions PlayCover.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
53F3802826EB6F6B00D6B525 /* NotifyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F3802726EB6F6B00D6B525 /* NotifyService.swift */; };
53F4D2A026C43C690020167C /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F4D29F26C43C690020167C /* Log.swift */; };
53F50C4926E3CA42007AD2D3 /* AppLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F50C4826E3CA42007AD2D3 /* AppLibraryView.swift */; };
6888981729F9158700105D9C /* IPASourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6888981629F9158700105D9C /* IPASourceView.swift */; };
68E48B952957046D00C39879 /* DownloadManager in Frameworks */ = {isa = PBXBuildFile; productRef = 68E48B942957046D00C39879 /* DownloadManager */; };
68E48B97295704A600C39879 /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E48B96295704A600C39879 /* Downloader.swift */; };
68E48B9A295708C800C39879 /* Injection in Frameworks */ = {isa = PBXBuildFile; productRef = 68E48B99295708C800C39879 /* Injection */; };
Expand Down Expand Up @@ -111,6 +112,7 @@
53F4D29F26C43C690020167C /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = "<group>"; };
53F50C4826E3CA42007AD2D3 /* AppLibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLibraryView.swift; sourceTree = "<group>"; };
6854C5E528D53C9500CE28A0 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = "<group>"; };
6888981629F9158700105D9C /* IPASourceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IPASourceView.swift; sourceTree = "<group>"; };
68C79E67296741580041DBC9 /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = "<group>"; };
68E48B96295704A600C39879 /* Downloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Downloader.swift; path = PlayCover/AppInstaller/Downloader.swift; sourceTree = SOURCE_ROOT; };
68E48B9B295709BF00C39879 /* Cacher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cacher.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -360,6 +362,7 @@
children = (
53F50C4826E3CA42007AD2D3 /* AppLibraryView.swift */,
6E66B0C8289F52800099B907 /* IPALibraryView.swift */,
6888981629F9158700105D9C /* IPASourceView.swift */,
);
path = "Sidebar Views";
sourceTree = "<group>";
Expand Down Expand Up @@ -675,6 +678,7 @@
B67C1A112AE8091F00F396CC /* StoreInfoAppView.swift in Sources */,
B17FD04728C7B0D900B1D4CA /* AssetsExtractor.swift in Sources */,
ABDAD80629893CF900DC164F /* KeyCoverSetupViews.swift in Sources */,
6888981729F9158700105D9C /* IPASourceView.swift in Sources */,
6EE8265028E8AB2B003935BC /* DownloadVM.swift in Sources */,
AB00EB5229A7BF17006F3225 /* KeyCover.swift in Sources */,
68E48B9C295709C000C39879 /* Cacher.swift in Sources */,
Expand Down
4 changes: 2 additions & 2 deletions PlayCover/AppInstaller/Downloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ import DownloadManager

class DownloadApp {
let url: URL?
let app: StoreAppData?
let app: SourceAppsData?
let warning: String?

init(url: URL?, app: StoreAppData?, warning: String?) {
init(url: URL?, app: SourceAppsData?, warning: String?) {
self.url = url
self.app = app
self.warning = warning
Expand Down
2 changes: 1 addition & 1 deletion PlayCover/ViewModel/DownloadVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ enum DownloadStepsNative: String {
}

class DownloadVM: ProgressVM<DownloadStepsNative> {
@Published var storeAppData: StoreAppData?
@Published var storeAppData: SourceAppsData?

static let shared = DownloadVM()

Expand Down
4 changes: 2 additions & 2 deletions PlayCover/ViewModel/StoreAppVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
import Foundation

class StoreAppVM: ObservableObject {
@Published var data: StoreAppData
@Published var data: SourceAppsData

init(data: StoreAppData) {
init(data: SourceAppsData) {
self.data = data
}
}
269 changes: 140 additions & 129 deletions PlayCover/ViewModel/StoreVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,182 +8,193 @@
import Foundation

class StoreVM: ObservableObject, @unchecked Sendable {

static let shared = StoreVM()
public static let shared = StoreVM()
private let plistSource: URL

private init() {
sourcesUrl = PlayTools.playCoverContainer
plistSource = PlayTools.playCoverContainer
.appendingPathComponent("Sources")
.appendingPathExtension("plist")
sources = []
if !decode() {
encode()
}
sourcesList = []
if !decode() { encode() }
resolveSources()
}

@Published var apps: [StoreAppData] = []
@Published var searchText: String = ""
@Published var filteredApps: [StoreAppData] = []
@Published var sources: [SourceData] {
@Published var sourcesList: [SourceData] {
didSet {
encode()
}
}

let sourcesUrl: URL

@discardableResult
public func decode() -> Bool {
do {
let data = try Data(contentsOf: sourcesUrl)
sources = try PropertyListDecoder().decode([SourceData].self, from: data)
return true
} catch {
print(error)
return false
@Published var sourcesData: [SourceJSON] = [] {
didSet {
sourcesApps.removeAll()
for source in sourcesData {
appendSourceData(source)
}
}
}
@Published var sourcesApps: [SourceAppsData] = []

@discardableResult
public func encode() -> Bool {
let encoder = PropertyListEncoder()
encoder.outputFormat = .xml
private var resolveTask: Task<Void, Never>?

do {
let data = try encoder.encode(sources)
try data.write(to: sourcesUrl)
return true
} catch {
print(error)
return false
//
func addSource(_ source: SourceData) {
sourcesList.append(source)
resolveSources()
}

//
func deleteSource(_ selectedSource: inout Set<UUID>) {
sourcesList.removeAll {
selectedSource.contains($0.id)
}
resolveSources()
}

func appendAppData(_ data: [StoreAppData]) {
for element in data {
if let index = apps.firstIndex(where: {$0.bundleID == element.bundleID}) {
if apps[index].version < element.version {
apps[index] = element
continue
//
func moveSourceUp(_ selectedSource: inout Set<UUID>) {
let selected = sourcesList.filter {
selectedSource.contains($0.id)
}
if let first = sourcesList.first,
let data = selected.first {
if data != first {
if var index = sourcesList.firstIndex(of: data) {
index -= 1
sourcesList.removeAll {
selectedSource.contains($0.id)
}
sourcesList.insert(contentsOf: selected, at: index)
}
} else {
apps.append(element)
resolveSources()
}
}
fetchApps()
}

func fetchApps() {
filteredApps.removeAll()
filteredApps = apps
if !searchText.isEmpty {
filteredApps = filteredApps.filter({
$0.name.lowercased().contains(searchText.lowercased())
})
//
func moveSourceDown(_ selectedSource: inout Set<UUID>) {
let selected = sourcesList.filter {
selectedSource.contains($0.id)
}

if let last = sourcesList.last,
let data = selected.first {
if data != last {
if var index = sourcesList.firstIndex(of: data) {
index += 1
sourcesList.removeAll {
selectedSource.contains($0.id)
}
sourcesList.insert(contentsOf: selected, at: index)
}
resolveSources()
}
}
}

//
func resolveSources() {
guard NetworkVM.isConnectedToNetwork() else {
return
}
resolveTask?.cancel()
resolveTask = Task { @MainActor in

apps.removeAll()
for index in 0..<sources.endIndex {
sources[index].status = .empty
Task {
if let url = URL(string: self.sources[index].source) {
URLSession.shared.dataTask(with: URLRequest(url: url)) { jsonData, response, error in
guard error == nil,
((response as? HTTPURLResponse)?.statusCode ?? 200) == 200,
let jsonData = jsonData else {
Task { @MainActor in
self.sources[index].status = .badurl
}

return
}

do {
let data: [StoreAppData] = try JSONDecoder().decode([StoreAppData].self,
from: jsonData)
if data.count > 0 {
Task { @MainActor in
self.sources[index].status = self.sources[0..<index].filter({
$0.source == self.sources[index].source && $0.id != self.sources[index].id
}).isEmpty ? .valid : .duplicate

self.appendAppData(data)
}
}
} catch {
Task { @MainActor in
self.sources[index].status = .badjson
}
}
}.resume()

Task { @MainActor in
self.sources[index].status = .checking
}
guard NetworkVM.isConnectedToNetwork() && !sourcesList.isEmpty else { return }

return
}
let sourcesCount = sourcesList.count
sourcesData.removeAll()

Task { @MainActor in
sources[index].status = .badurl
for index in sourcesList.indices {
sourcesList[index].status = .checking
let (sourceJson, sourceState) = await getSourceData(sourceLink: sourcesList[index].source)
guard sourcesCount == sourcesList.count else { return }
sourcesList[index].status = sourceState
if sourceState == .valid, let sourceJson {
sourcesData.append(sourceJson)
}
}
}

fetchApps()
}

func deleteSource(_ selected: inout Set<UUID>) {
self.sources.removeAll(where: { selected.contains($0.id) })
selected.removeAll()
resolveSources()
}
}

func moveSourceUp(_ selected: inout Set<UUID>) {
let selectedData = self.sources.filter({ selected.contains($0.id) })
//
@discardableResult private func encode() -> Bool {
let encoder = PropertyListEncoder()
encoder.outputFormat = .xml

if let first = selectedData.first {
if var index = self.sources.firstIndex(of: first) {
index -= 1
self.sources.removeAll(where: { selected.contains($0.id) })
if index < 0 {
index = 0
}
self.sources.insert(contentsOf: selectedData, at: index)
}
do {
let data = try encoder.encode(sourcesList)
try data.write(to: plistSource)
return true
} catch {
print("StoreVM: Failed to encode Sources.plist! ", error)
return false
}
}

func moveSourceDown(_ selected: inout Set<UUID>) {
let selectedData = self.sources.filter({ selected.contains($0.id) })
//
@discardableResult private func decode() -> Bool {
do {
let data = try Data(contentsOf: plistSource)
sourcesList = try PropertyListDecoder().decode([SourceData].self, from: data)
return true
} catch {
print("StoreVM: Failed to decode Sources.plist! ", error)
return false
}
}

if let first = selectedData.first {
if var index = self.sources.firstIndex(of: first) {
index += 1
self.sources.removeAll(where: { selected.contains($0.id) })
if index > self.sources.endIndex {
index = self.sources.endIndex
}
self.sources.insert(contentsOf: selectedData, at: index)
//
private func getSourceData(sourceLink: String) async -> (SourceJSON?, SourceValidation) {
guard let url = URL(string: sourceLink) else { return (nil, .badurl) }
var dataToDecode: Data?
do {
let (data, response) = try await URLSession.shared.data(
for: URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData)
)
if !url.isFileURL {
guard (response as? HTTPURLResponse)?.statusCode == 200 else { return (nil, .badurl) }
}
dataToDecode = data
} catch {
debugPrint("Error decoding data from URL: \(url): \(error)")
return (nil, .badjson)
}
guard let unwrappedData = dataToDecode else { return (nil, .badurl) }
var decodedData: SourceJSON?
do {
decodedData = try JSONDecoder().decode(SourceJSON.self, from: unwrappedData)
return (decodedData, .valid)
} catch {
do {
let sourceName = url.isFileURL
? (url.absoluteString as NSString).lastPathComponent.replacingOccurrences(of: ".json", with: "")
: url.host ?? url.absoluteString
let oldTypeJson: [SourceAppsData] = try JSONDecoder().decode([SourceAppsData].self, from: unwrappedData)
decodedData = SourceJSON(name: sourceName, data: oldTypeJson)
return (decodedData, .valid)
} catch {
debugPrint("Error decoding data from URL: \(url): \(error)")
return (nil, .badjson)
}
}
}

func appendSourceData(_ data: SourceData) {
self.sources.append(data)
self.resolveSources()
//
private func appendSourceData(_ source: SourceJSON) {
for app in source.data where !sourcesApps.contains(app) {
sourcesApps.append(app)
}
}

}

// Source Data Structure
struct SourceJSON: Codable, Equatable, Hashable {
let name: String
let data: [SourceAppsData]
}

struct StoreAppData: Codable, Equatable {
var bundleID: String
struct SourceAppsData: Codable, Equatable, Hashable {
let bundleID: String
let name: String
let version: String
let itunesLookup: String
Expand Down
Loading

0 comments on commit 079bd57

Please sign in to comment.