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"?>
<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">
<attribute name="leechers" optional="YES" attributeType="String"/>
<attribute name="magnetHash" optional="YES" attributeType="String"/>

View file

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

View file

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

View file

@ -34,6 +34,10 @@ class NavigationViewModel: ObservableObject {
@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 currentChoiceSheet: ChoiceSheetType?
@ -123,6 +127,7 @@ class NavigationViewModel: ObservableObject {
public func addToHistory(name: String?, source: String?, url: String?, subName: String? = nil) {
let backgroundContext = PersistenceController.shared.backgroundContext
// The timeStamp and date are nil because the create function will make them automatically
PersistenceController.shared.createHistory(
entryJson: HistoryEntryJson(
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)
}

View file

@ -28,6 +28,7 @@ struct BatchChoiceView: View {
if !debridManager.realDebridDownloadUrl.isEmpty {
// The download may complete before this sheet dismisses
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.runDebridAction(urlString: debridManager.realDebridDownloadUrl)
}

View file

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

View file

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

View file

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

View file

@ -23,6 +23,20 @@ struct MagnetChoiceView: View {
var body: some View {
NavView {
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 {
Section(header: "Real Debrid options") {
ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") {

View file

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