Ferrite: Improve overall UI

- Make history buttons have 2 lines of text to give more context.
If a batch is given, the full episode name is shown
- Add a "now playing" section to player choices to show what the user
will play in the event of a misclick
- Make the maximum line limit in search results 4 lines to prevent
long title results from taking up the entire cell
- Fix light theme appearance with library since the picker and list
weren't aligned right

Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
kingbri 2022-11-18 17:10:42 -05:00
parent e3e8924547
commit a774564212
11 changed files with 142 additions and 116 deletions

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21279" systemVersion="21G83" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier=""> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21512" systemVersion="21G83" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Bookmark" representedClassName="Bookmark" syncable="YES"> <entity name="Bookmark" representedClassName="Bookmark" syncable="YES">
<attribute name="leechers" optional="YES" attributeType="String"/> <attribute name="leechers" optional="YES" attributeType="String"/>
<attribute name="magnetHash" optional="YES" attributeType="String"/> <attribute name="magnetHash" optional="YES" attributeType="String"/>

View file

@ -114,7 +114,6 @@ struct PersistenceController {
newBookmark.leechers = bookmarkJson.leechers newBookmark.leechers = bookmarkJson.leechers
} }
// TODO: Change timestamp to use a date instead of a double
func createHistory(entryJson: HistoryEntryJson, date: Double?) { func createHistory(entryJson: HistoryEntryJson, date: Double?) {
let historyDate = date.map { Date(timeIntervalSince1970: $0) } ?? Date() let historyDate = date.map { Date(timeIntervalSince1970: $0) } ?? Date()
let historyDateString = DateFormatter.historyDateFormatter.string(from: historyDate) let historyDateString = DateFormatter.historyDateFormatter.string(from: historyDate)
@ -124,7 +123,8 @@ struct PersistenceController {
newHistoryEntry.source = entryJson.source newHistoryEntry.source = entryJson.source
newHistoryEntry.name = entryJson.name newHistoryEntry.name = entryJson.name
newHistoryEntry.url = entryJson.url newHistoryEntry.url = entryJson.url
newHistoryEntry.subName = entryJson.source newHistoryEntry.subName = entryJson.subName
newHistoryEntry.timeStamp = entryJson.timeStamp ?? Date().timeIntervalSince1970
let historyRequest = History.fetchRequest() let historyRequest = History.fetchRequest()
historyRequest.predicate = NSPredicate(format: "dateString = %@", historyDateString) historyRequest.predicate = NSPredicate(format: "dateString = %@", historyDateString)

View file

@ -11,182 +11,184 @@ import Foundation
// MARK: - device code endpoint // MARK: - device code endpoint
public struct DeviceCodeResponse: Codable, Sendable { public struct DeviceCodeResponse: Codable, Sendable {
let deviceCode, userCode: String let deviceCode, userCode: String
let interval, expiresIn: Int let interval, expiresIn: Int
let verificationURL, directVerificationURL: String let verificationURL, directVerificationURL: String
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case deviceCode = "device_code" case deviceCode = "device_code"
case userCode = "user_code" case userCode = "user_code"
case interval case interval
case expiresIn = "expires_in" case expiresIn = "expires_in"
case verificationURL = "verification_url" case verificationURL = "verification_url"
case directVerificationURL = "direct_verification_url" case directVerificationURL = "direct_verification_url"
} }
} }
// MARK: - device credentials endpoint // MARK: - device credentials endpoint
public struct DeviceCredentialsResponse: Codable, Sendable { public struct DeviceCredentialsResponse: Codable, Sendable {
let clientID, clientSecret: String? let clientID, clientSecret: String?
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case clientID = "client_id" case clientID = "client_id"
case clientSecret = "client_secret" case clientSecret = "client_secret"
} }
} }
// MARK: - token endpoint // MARK: - token endpoint
public struct TokenResponse: Codable, Sendable { public struct TokenResponse: Codable, Sendable {
let accessToken: String let accessToken: String
let expiresIn: Int let expiresIn: Int
let refreshToken, tokenType: String let refreshToken, tokenType: String
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case accessToken = "access_token" case accessToken = "access_token"
case expiresIn = "expires_in" case expiresIn = "expires_in"
case refreshToken = "refresh_token" case refreshToken = "refresh_token"
case tokenType = "token_type" case tokenType = "token_type"
} }
} }
// MARK: - instantAvailability endpoint // MARK: - instantAvailability endpoint
// Thanks Skitty! // Thanks Skitty!
public struct InstantAvailabilityResponse: Codable, Sendable { public struct InstantAvailabilityResponse: Codable, Sendable {
var data: InstantAvailabilityData? var data: InstantAvailabilityData?
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer() let container = try decoder.singleValueContainer()
if let data = try? container.decode(InstantAvailabilityData.self) { if let data = try? container.decode(InstantAvailabilityData.self) {
self.data = data self.data = data
} }
} }
} }
struct InstantAvailabilityData: Codable, Sendable { struct InstantAvailabilityData: Codable, Sendable {
var rd: [[String: InstantAvailabilityInfo]] var rd: [[String: InstantAvailabilityInfo]]
} }
struct InstantAvailabilityInfo: Codable, Sendable { struct InstantAvailabilityInfo: Codable, Sendable {
var filename: String var filename: String
var filesize: Int var filesize: Int
} }
// MARK: - Instant Availability client side structures // MARK: - Instant Availability client side structures
public struct RealDebridIA: Codable, Hashable, Sendable { public struct RealDebridIA: Codable, Hashable, Sendable {
let hash: String let hash: String
let expiryTimeStamp: Double let expiryTimeStamp: Double
var files: [RealDebridIAFile] = [] var files: [RealDebridIAFile] = []
var batches: [RealDebridIABatch] = [] var batches: [RealDebridIABatch] = []
} }
public struct RealDebridIABatch: Codable, Hashable, Sendable { public struct RealDebridIABatch: Codable, Hashable, Sendable {
let files: [RealDebridIABatchFile] let files: [RealDebridIABatchFile]
} }
public struct RealDebridIABatchFile: Codable, Hashable, Sendable { public struct RealDebridIABatchFile: Codable, Hashable, Sendable {
let id: Int let id: Int
let fileName: String let fileName: String
} }
public struct RealDebridIAFile: Codable, Hashable, Sendable { public struct RealDebridIAFile: Codable, Hashable, Sendable {
let name: String let name: String
let batchIndex: Int let batchIndex: Int
let batchFileIndex: Int let batchFileIndex: Int
} }
public enum RealDebridIAStatus: Codable, Hashable, Sendable { public enum RealDebridIAStatus: Codable, Hashable, Sendable {
case full case full
case partial case partial
case none case none
} }
// MARK: - addMagnet endpoint // MARK: - addMagnet endpoint
public struct AddMagnetResponse: Codable, Sendable { public struct AddMagnetResponse: Codable, Sendable {
let id: String let id: String
let uri: String let uri: String
} }
// MARK: - torrentInfo endpoint // MARK: - torrentInfo endpoint
struct TorrentInfoResponse: Codable, Sendable { struct TorrentInfoResponse: Codable, Sendable {
let id, filename, originalFilename, hash: String let id, filename, originalFilename, hash: String
let bytes, originalBytes: Int let bytes, originalBytes: Int
let host: String let host: String
let split, progress: Int let split, progress: Int
let status, added: String let status, added: String
let files: [TorrentInfoFile] let files: [TorrentInfoFile]
let links: [String] let links: [String]
let ended: String? let ended: String?
let speed: Int? let speed: Int?
let seeders: Int? let seeders: Int?
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case id, filename case id, filename
case originalFilename = "original_filename" case originalFilename = "original_filename"
case hash, bytes case hash, bytes
case originalBytes = "original_bytes" case originalBytes = "original_bytes"
case host, split, progress, status, added, files, links, ended, speed, seeders case host, split, progress, status, added, files, links, ended, speed, seeders
} }
} }
struct TorrentInfoFile: Codable, Sendable { struct TorrentInfoFile: Codable, Sendable {
let id: Int let id: Int
let path: String let path: String
let bytes, selected: Int let bytes, selected: Int
} }
public struct UserTorrentsResponse: Codable, Sendable { public struct UserTorrentsResponse: Codable, Sendable {
let id, filename, hash: String let id, filename, hash: String
let bytes: Int let bytes: Int
let host: String let host: String
let split, progress: Int let split, progress: Int
let status, added: String let status, added: String
let links: [String] let links: [String]
let speed, seeders: Int? let speed, seeders: Int?
let ended: String? let ended: String?
} }
// MARK: - unrestrictLink endpoint // MARK: - unrestrictLink endpoint
struct UnrestrictLinkResponse: Codable, Sendable { struct UnrestrictLinkResponse: Codable, Sendable {
let id, filename, mimeType: String let id, filename: String
let filesize: Int let mimeType: String?
let link: String let filesize: Int
let host: String let link: String
let hostIcon: String let host: String
let chunks, crc: Int let hostIcon: String
let download: String let chunks, crc: Int
let streamable: Int let download: String
let streamable: Int
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case id, filename, mimeType, filesize, link, host case id, filename, mimeType, filesize, link, host
case hostIcon = "host_icon" case hostIcon = "host_icon"
case chunks, crc, download, streamable case chunks, crc, download, streamable
} }
} }
// MARK: - User downloads list // MARK: - User downloads list
public struct UserDownloadsResponse: Codable, Sendable { public struct UserDownloadsResponse: Codable, Sendable {
let id, filename, mimeType: String let id, filename: String
let filesize: Int let mimeType: String?
let link: String let filesize: Int
let host: String let link: String
let hostIcon: String let host: String
let chunks: Int let hostIcon: String
let download: String let chunks: Int
let streamable: Int let download: String
let generated: String let streamable: Int
let generated: String
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case id, filename, mimeType, filesize, link, host case id, filename, mimeType, filesize, link, host
case hostIcon = "host_icon" case hostIcon = "host_icon"
case chunks, download, streamable, generated case chunks, download, streamable, generated
} }
} }

View file

@ -34,6 +34,10 @@ class NavigationViewModel: ObservableObject {
@Published var selectedSearchResult: SearchResult? @Published var selectedSearchResult: SearchResult?
// For giving information in magnet choice sheet
@Published var selectedTitle: String?
@Published var selectedBatchTitle: String?
@Published var hideNavigationBar = false @Published var hideNavigationBar = false
@Published var currentChoiceSheet: ChoiceSheetType? @Published var currentChoiceSheet: ChoiceSheetType?
@ -123,6 +127,7 @@ class NavigationViewModel: ObservableObject {
public func addToHistory(name: String?, source: String?, url: String?, subName: String? = nil) { public func addToHistory(name: String?, source: String?, url: String?, subName: String? = nil) {
let backgroundContext = PersistenceController.shared.backgroundContext let backgroundContext = PersistenceController.shared.backgroundContext
// The timeStamp and date are nil because the create function will make them automatically
PersistenceController.shared.createHistory( PersistenceController.shared.createHistory(
entryJson: HistoryEntryJson( entryJson: HistoryEntryJson(
name: name ?? "", name: name ?? "",

View file

@ -901,7 +901,7 @@ class ScrapingViewModel: ObservableObject {
} }
} }
await toastModel?.updateToastDescription(responseArray.joined()) await toastModel?.updateToastDescription(responseArray.joined(separator: " "))
PersistenceController.shared.save(backgroundContext) PersistenceController.shared.save(backgroundContext)
} }

View file

@ -28,6 +28,7 @@ struct BatchChoiceView: View {
if !debridManager.realDebridDownloadUrl.isEmpty { if !debridManager.realDebridDownloadUrl.isEmpty {
// The download may complete before this sheet dismisses // The download may complete before this sheet dismisses
try? await Task.sleep(seconds: 1) try? await Task.sleep(seconds: 1)
navModel.selectedBatchTitle = file.name
navModel.addToHistory(name: searchResult.title, source: searchResult.source, url: debridManager.realDebridDownloadUrl, subName: file.name) navModel.addToHistory(name: searchResult.title, source: searchResult.source, url: debridManager.realDebridDownloadUrl, subName: file.name)
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl) navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl)
} }

View file

@ -36,14 +36,13 @@ struct LibraryView: View {
var body: some View { var body: some View {
NavView { NavView {
VStack(spacing: 0) { VStack {
Picker("Segments", selection: $selectedSegment) { Picker("Segments", selection: $selectedSegment) {
Text("Bookmarks").tag(LibraryPickerSegment.bookmarks) Text("Bookmarks").tag(LibraryPickerSegment.bookmarks)
Text("History").tag(LibraryPickerSegment.history) Text("History").tag(LibraryPickerSegment.history)
} }
.pickerStyle(.segmented) .pickerStyle(.segmented)
.padding(.horizontal) .padding()
.padding(.top)
switch selectedSegment { switch selectedSegment {
case .bookmarks: case .bookmarks:

View file

@ -49,7 +49,7 @@ struct BookmarksView: View {
PersistenceController.shared.save() PersistenceController.shared.save()
} }
} }
.id(UUID()) .inlinedList()
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
} }
} }

View file

@ -16,6 +16,9 @@ struct HistoryButtonView: View {
var body: some View { var body: some View {
Button { Button {
navModel.selectedTitle = entry.name
navModel.selectedBatchTitle = entry.subName
if let url = entry.url { if let url = entry.url {
if url.starts(with: "https://") { if url.starts(with: "https://") {
Task { Task {
@ -37,11 +40,13 @@ struct HistoryButtonView: View {
VStack(alignment: .leading, spacing: 3) { VStack(alignment: .leading, spacing: 3) {
Text(entry.name ?? "Unknown title") Text(entry.name ?? "Unknown title")
.font(entry.subName == nil ? .body : .subheadline) .font(entry.subName == nil ? .body : .subheadline)
.lineLimit(entry.subName == nil ? 2 : 1)
if let subName = entry.subName { if let subName = entry.subName {
Text(subName) Text(subName)
.foregroundColor(.gray) .foregroundColor(.gray)
.font(.subheadline) .font(.subheadline)
.lineLimit(2)
} }
} }
@ -67,10 +72,9 @@ struct HistoryButtonView: View {
} }
.font(.caption) .font(.caption)
} }
.lineLimit(1)
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2)) .disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
} }
.backport.tint(.white) .backport.tint(.primary)
.disableInteraction(navModel.currentChoiceSheet != nil) .disableInteraction(navModel.currentChoiceSheet != nil)
} }
} }

View file

@ -23,6 +23,20 @@ struct MagnetChoiceView: View {
var body: some View { var body: some View {
NavView { NavView {
Form { Form {
Section(header: "Now Playing") {
VStack(alignment: .leading, spacing: 5) {
Text(navModel.selectedTitle ?? "No title")
.font(.callout)
.lineLimit(navModel.selectedBatchTitle == nil ? .max : 1)
if let batchTitle = navModel.selectedBatchTitle {
Text(batchTitle)
.foregroundColor(.gray)
.font(.subheadline)
}
}
}
if !debridManager.realDebridDownloadUrl.isEmpty { if !debridManager.realDebridDownloadUrl.isEmpty {
Section(header: "Real Debrid options") { Section(header: "Real Debrid options") {
ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") { ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") {

View file

@ -7,7 +7,6 @@
import SwiftUI import SwiftUI
// BUG: iOS 15 cannot refresh the context menu. Debating using swipe actions or adopting a workaround.
struct SearchResultButtonView: View { struct SearchResultButtonView: View {
let backgroundContext = PersistenceController.shared.backgroundContext let backgroundContext = PersistenceController.shared.backgroundContext
@ -25,6 +24,7 @@ struct SearchResultButtonView: View {
Button { Button {
if debridManager.currentDebridTask == nil { if debridManager.currentDebridTask == nil {
navModel.selectedSearchResult = result navModel.selectedSearchResult = result
navModel.selectedTitle = result.title
switch debridManager.matchSearchResult(result: result) { switch debridManager.matchSearchResult(result: result) {
case .full: case .full:
@ -56,6 +56,7 @@ struct SearchResultButtonView: View {
Text(result.title ?? "No title") Text(result.title ?? "No title")
.font(.callout) .font(.callout)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.lineLimit(4)
SearchResultRDView(result: result) SearchResultRDView(result: result)
} }