mirror of
https://github.com/cranci1/Sora.git
synced 2026-01-11 20:10:24 +00:00
Bookmark collection system + migration system (Video in discord) (#207)
This commit is contained in:
parent
14ebc82fc6
commit
82eec0688f
14 changed files with 839 additions and 440 deletions
|
|
@ -246,7 +246,6 @@ struct AllWatchingView: View {
|
|||
.onAppear {
|
||||
loadContinueWatchingItems()
|
||||
|
||||
// Enable swipe back gesture
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first,
|
||||
let navigationController = window.rootViewController?.children.first as? UINavigationController {
|
||||
|
|
|
|||
96
Sora/Views/LibraryView/BookmarkComponents/BookmarkCell.swift
Normal file
96
Sora/Views/LibraryView/BookmarkComponents/BookmarkCell.swift
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
//
|
||||
// BookmarkCell.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by paul on 18/06/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import NukeUI
|
||||
|
||||
struct BookmarkCell: View {
|
||||
let bookmark: LibraryItem
|
||||
@EnvironmentObject private var moduleManager: ModuleManager
|
||||
@EnvironmentObject private var libraryManager: LibraryManager
|
||||
|
||||
var body: some View {
|
||||
if let module = moduleManager.modules.first(where: { $0.id.uuidString == bookmark.moduleId }) {
|
||||
ZStack {
|
||||
LazyImage(url: URL(string: bookmark.imageUrl)) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(0.72, contentMode: .fill)
|
||||
.frame(width: 162, height: 243)
|
||||
.cornerRadius(12)
|
||||
.clipped()
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 162, height: 243)
|
||||
}
|
||||
}
|
||||
.overlay(
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.black.opacity(0.5))
|
||||
.frame(width: 28, height: 28)
|
||||
.overlay(
|
||||
LazyImage(url: URL(string: module.metadata.iconUrl)) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 32, height: 32)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding(8),
|
||||
alignment: .topLeading
|
||||
)
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
Text(bookmark.title)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.lineLimit(2)
|
||||
.foregroundColor(.white)
|
||||
.padding(12)
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
.black.opacity(0.7),
|
||||
.black.opacity(0.0)
|
||||
],
|
||||
startPoint: .bottom,
|
||||
endPoint: .top
|
||||
)
|
||||
.shadow(color: .black, radius: 4, x: 0, y: 2)
|
||||
)
|
||||
}
|
||||
.frame(width: 162)
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.padding(4)
|
||||
.contextMenu {
|
||||
Button(role: .destructive, action: {
|
||||
// Find which collection contains this bookmark
|
||||
for collection in libraryManager.collections {
|
||||
if collection.bookmarks.contains(where: { $0.id == bookmark.id }) {
|
||||
libraryManager.removeBookmarkFromCollection(bookmarkId: bookmark.id, collectionId: collection.id)
|
||||
break
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Label("Remove from Bookmarks", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
//
|
||||
// BookmarkCollectionGridCell.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by paul on 18/06/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import NukeUI
|
||||
|
||||
struct BookmarkCollectionGridCell: View {
|
||||
let collection: BookmarkCollection
|
||||
let width: CGFloat
|
||||
let height: CGFloat
|
||||
|
||||
private var recentBookmarks: [LibraryItem] {
|
||||
Array(collection.bookmarks.prefix(4))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let gap: CGFloat = 2
|
||||
let cellWidth = (width - gap) / 2
|
||||
let cellHeight = (height - gap) / 2
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ZStack {
|
||||
if recentBookmarks.isEmpty {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: width, height: height)
|
||||
.overlay(
|
||||
Image(systemName: "folder.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: width/3)
|
||||
.foregroundColor(.gray.opacity(0.5))
|
||||
)
|
||||
} else {
|
||||
LazyVGrid(
|
||||
columns: [
|
||||
GridItem(.flexible(), spacing: gap),
|
||||
GridItem(.flexible(), spacing: gap)
|
||||
],
|
||||
spacing: gap
|
||||
) {
|
||||
ForEach(0..<4) { index in
|
||||
if index < recentBookmarks.count {
|
||||
LazyImage(url: URL(string: recentBookmarks[index].imageUrl)) { state in
|
||||
if let image = state.imageContainer?.image {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: cellWidth, height: cellHeight)
|
||||
.clipped()
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: cellWidth, height: cellHeight)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: cellWidth, height: cellHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: width, height: height)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(collection.name)
|
||||
.font(.headline)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.layoutPriority(1)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Text("\(collection.bookmarks.count) items")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +1,81 @@
|
|||
//
|
||||
// MediaInfoView.swift
|
||||
// BookmarkGridItemView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by paul on 28/05/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import NukeUI
|
||||
|
||||
struct BookmarkGridItemView: View {
|
||||
let bookmark: LibraryItem
|
||||
let moduleManager: ModuleManager
|
||||
let isSelecting: Bool
|
||||
@Binding var selectedBookmarks: Set<LibraryItem.ID>
|
||||
|
||||
var isSelected: Bool {
|
||||
selectedBookmarks.contains(bookmark.id)
|
||||
}
|
||||
let item: LibraryItem
|
||||
let module: Module
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let module = moduleManager.modules.first(where: { $0.id.uuidString == bookmark.moduleId }) {
|
||||
if isSelecting {
|
||||
Button(action: {
|
||||
if isSelected {
|
||||
selectedBookmarks.remove(bookmark.id)
|
||||
} else {
|
||||
selectedBookmarks.insert(bookmark.id)
|
||||
}
|
||||
}) {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
BookmarkCell(bookmark: bookmark)
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(.accentColor)
|
||||
.background(Color.white.clipShape(Circle()).opacity(0.8))
|
||||
.offset(x: -8, y: 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
ZStack {
|
||||
LazyImage(url: URL(string: item.imageUrl)) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(0.72, contentMode: .fill)
|
||||
.frame(width: 162, height: 243)
|
||||
.cornerRadius(12)
|
||||
.clipped()
|
||||
} else {
|
||||
BookmarkLink(
|
||||
bookmark: bookmark,
|
||||
module: module
|
||||
)
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.aspectRatio(2/3, contentMode: .fit)
|
||||
.redacted(reason: .placeholder)
|
||||
}
|
||||
}
|
||||
.overlay(
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.black.opacity(0.5))
|
||||
.frame(width: 28, height: 28)
|
||||
.overlay(
|
||||
LazyImage(url: URL(string: module.metadata.iconUrl)) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 32, height: 32)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding(8),
|
||||
alignment: .topLeading
|
||||
)
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
Text(item.title)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.lineLimit(2)
|
||||
.foregroundColor(.white)
|
||||
.padding(12)
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
.black.opacity(0.7),
|
||||
.black.opacity(0.0)
|
||||
],
|
||||
startPoint: .bottom,
|
||||
endPoint: .top
|
||||
)
|
||||
.shadow(color: .black, radius: 4, x: 0, y: 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
.frame(width: 162, height: 243)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,29 +8,62 @@
|
|||
import SwiftUI
|
||||
|
||||
struct BookmarkGridView: View {
|
||||
@EnvironmentObject private var libraryManager: LibraryManager
|
||||
@EnvironmentObject private var moduleManager: ModuleManager
|
||||
|
||||
let bookmarks: [LibraryItem]
|
||||
let moduleManager: ModuleManager
|
||||
let isSelecting: Bool
|
||||
@Binding var selectedBookmarks: Set<LibraryItem.ID>
|
||||
|
||||
private let columns = [
|
||||
GridItem(.adaptive(minimum: 150))
|
||||
GridItem(.adaptive(minimum: 150), spacing: 16)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
LazyVGrid(columns: columns, spacing: 16) {
|
||||
ForEach(bookmarks) { bookmark in
|
||||
BookmarkGridItemView(
|
||||
bookmark: bookmark,
|
||||
moduleManager: moduleManager,
|
||||
isSelecting: isSelecting,
|
||||
selectedBookmarks: $selectedBookmarks
|
||||
)
|
||||
LazyVGrid(columns: columns, spacing: 16) {
|
||||
ForEach(bookmarks) { bookmark in
|
||||
if let module = moduleManager.modules.first(where: { $0.id.uuidString == bookmark.moduleId }) {
|
||||
if isSelecting {
|
||||
Button(action: {
|
||||
if selectedBookmarks.contains(bookmark.id) {
|
||||
selectedBookmarks.remove(bookmark.id)
|
||||
} else {
|
||||
selectedBookmarks.insert(bookmark.id)
|
||||
}
|
||||
}) {
|
||||
NavigationLink(destination: MediaInfoView(
|
||||
title: bookmark.title,
|
||||
imageUrl: bookmark.imageUrl,
|
||||
href: bookmark.href,
|
||||
module: module
|
||||
)) {
|
||||
BookmarkGridItemView(item: bookmark, module: module)
|
||||
.overlay(
|
||||
selectedBookmarks.contains(bookmark.id) ?
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(.accentColor)
|
||||
.background(Color.white.clipShape(Circle()))
|
||||
.padding(8)
|
||||
: nil,
|
||||
alignment: .topTrailing
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NavigationLink(destination: MediaInfoView(
|
||||
title: bookmark.title,
|
||||
imageUrl: bookmark.imageUrl,
|
||||
href: bookmark.href,
|
||||
module: module
|
||||
)) {
|
||||
BookmarkGridItemView(item: bookmark, module: module)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.scrollViewBottomPadding()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// MediaInfoView.swift
|
||||
// BookmarkLink.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by paul on 28/05/25.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// MediaInfoView.swift
|
||||
// BookmarksDetailView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by paul on 28/05/25.
|
||||
|
|
@ -13,35 +13,34 @@ struct BookmarksDetailView: View {
|
|||
@EnvironmentObject private var libraryManager: LibraryManager
|
||||
@EnvironmentObject private var moduleManager: ModuleManager
|
||||
|
||||
@Binding var bookmarks: [LibraryItem]
|
||||
@State private var sortOption: SortOption = .dateAdded
|
||||
@State private var sortOption: SortOption = .dateCreated
|
||||
@State private var searchText: String = ""
|
||||
@State private var isSearchActive: Bool = false
|
||||
@State private var isSelecting: Bool = false
|
||||
@State private var selectedBookmarks: Set<LibraryItem.ID> = []
|
||||
@State private var selectedCollections: Set<UUID> = []
|
||||
@State private var isShowingCreateCollection: Bool = false
|
||||
@State private var newCollectionName: String = ""
|
||||
@State private var isShowingRenamePrompt: Bool = false
|
||||
@State private var collectionToRename: BookmarkCollection? = nil
|
||||
@State private var renameCollectionName: String = ""
|
||||
|
||||
enum SortOption: String, CaseIterable {
|
||||
case dateAdded = "Date Added"
|
||||
case title = "Title"
|
||||
case source = "Source"
|
||||
case dateCreated = "Date Created"
|
||||
case name = "Name"
|
||||
case itemCount = "Item Count"
|
||||
}
|
||||
|
||||
var filteredAndSortedBookmarks: [LibraryItem] {
|
||||
let filtered = searchText.isEmpty ? bookmarks : bookmarks.filter { item in
|
||||
item.title.localizedCaseInsensitiveContains(searchText) ||
|
||||
item.moduleName.localizedCaseInsensitiveContains(searchText)
|
||||
var filteredAndSortedCollections: [BookmarkCollection] {
|
||||
let filtered = searchText.isEmpty ? libraryManager.collections : libraryManager.collections.filter { collection in
|
||||
collection.name.localizedCaseInsensitiveContains(searchText)
|
||||
}
|
||||
switch sortOption {
|
||||
case .dateAdded:
|
||||
return filtered
|
||||
case .title:
|
||||
return filtered.sorted { $0.title.lowercased() < $1.title.lowercased() }
|
||||
case .source:
|
||||
return filtered.sorted { item1, item2 in
|
||||
let module1 = moduleManager.modules.first { $0.id.uuidString == item1.moduleId }
|
||||
let module2 = moduleManager.modules.first { $0.id.uuidString == item2.moduleId }
|
||||
return (module1?.metadata.sourceName ?? "") < (module2?.metadata.sourceName ?? "")
|
||||
}
|
||||
case .dateCreated:
|
||||
return filtered.sorted { $0.dateCreated > $1.dateCreated }
|
||||
case .name:
|
||||
return filtered.sorted { $0.name.lowercased() < $1.name.lowercased() }
|
||||
case .itemCount:
|
||||
return filtered.sorted { $0.bookmarks.count > $1.bookmarks.count }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -54,12 +53,15 @@ struct BookmarksDetailView: View {
|
|||
.foregroundColor(.primary)
|
||||
}
|
||||
Button(action: { dismiss() }) {
|
||||
Text("All Bookmarks")
|
||||
Text("Collections")
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.layoutPriority(1)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
Spacer()
|
||||
HStack(spacing: 16) {
|
||||
Button(action: {
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
|
|
@ -112,14 +114,11 @@ struct BookmarksDetailView: View {
|
|||
}
|
||||
Button(action: {
|
||||
if isSelecting {
|
||||
// If trash icon tapped
|
||||
if !selectedBookmarks.isEmpty {
|
||||
for id in selectedBookmarks {
|
||||
if let item = bookmarks.first(where: { $0.id == id }) {
|
||||
libraryManager.removeBookmark(item: item)
|
||||
}
|
||||
if !selectedCollections.isEmpty {
|
||||
for id in selectedCollections {
|
||||
libraryManager.deleteCollection(id: id)
|
||||
}
|
||||
selectedBookmarks.removeAll()
|
||||
selectedCollections.removeAll()
|
||||
}
|
||||
isSelecting = false
|
||||
} else {
|
||||
|
|
@ -139,10 +138,28 @@ struct BookmarksDetailView: View {
|
|||
)
|
||||
.circularGradientOutline()
|
||||
}
|
||||
Button(action: {
|
||||
isShowingCreateCollection = true
|
||||
}) {
|
||||
Image(systemName: "folder.badge.plus")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 18, height: 18)
|
||||
.foregroundColor(.accentColor)
|
||||
.padding(10)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
.shadow(color: .accentColor.opacity(0.2), radius: 2)
|
||||
)
|
||||
.circularGradientOutline()
|
||||
}
|
||||
}
|
||||
.layoutPriority(0)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top)
|
||||
|
||||
if isSearchActive {
|
||||
HStack(spacing: 12) {
|
||||
HStack(spacing: 12) {
|
||||
|
|
@ -151,7 +168,7 @@ struct BookmarksDetailView: View {
|
|||
.scaledToFit()
|
||||
.frame(width: 18, height: 18)
|
||||
.foregroundColor(.secondary)
|
||||
TextField("Search bookmarks...", text: $searchText)
|
||||
TextField("Search collections...", text: $searchText)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
.foregroundColor(.primary)
|
||||
if !searchText.isEmpty {
|
||||
|
|
@ -192,15 +209,115 @@ struct BookmarksDetailView: View {
|
|||
removal: .move(edge: .top).combined(with: .opacity)
|
||||
))
|
||||
}
|
||||
BookmarksDetailGrid(
|
||||
bookmarks: filteredAndSortedBookmarks,
|
||||
moduleManager: moduleManager,
|
||||
isSelecting: isSelecting,
|
||||
selectedBookmarks: $selectedBookmarks
|
||||
)
|
||||
|
||||
if filteredAndSortedCollections.isEmpty {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "folder")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
Text("No Collections")
|
||||
.font(.headline)
|
||||
Text("Create a collection to organize your bookmarks")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
ScrollView(showsIndicators: false) {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 162), spacing: 16)], spacing: 16) {
|
||||
ForEach(filteredAndSortedCollections) { collection in
|
||||
if isSelecting {
|
||||
Button(action: {
|
||||
if selectedCollections.contains(collection.id) {
|
||||
selectedCollections.remove(collection.id)
|
||||
} else {
|
||||
selectedCollections.insert(collection.id)
|
||||
}
|
||||
}) {
|
||||
BookmarkCollectionGridCell(collection: collection, width: 162, height: 162)
|
||||
.overlay(
|
||||
selectedCollections.contains(collection.id) ?
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.white)
|
||||
.frame(width: 32, height: 32)
|
||||
Image(systemName: "checkmark")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 18, height: 18)
|
||||
.foregroundColor(.black)
|
||||
}
|
||||
.padding(8)
|
||||
: nil,
|
||||
alignment: .topTrailing
|
||||
)
|
||||
}
|
||||
.contextMenu {
|
||||
Button("Rename") {
|
||||
collectionToRename = collection
|
||||
renameCollectionName = collection.name
|
||||
isShowingRenamePrompt = true
|
||||
}
|
||||
Button(role: .destructive) {
|
||||
libraryManager.deleteCollection(id: collection.id)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NavigationLink(destination: CollectionDetailView(collection: collection)) {
|
||||
BookmarkCollectionGridCell(collection: collection, width: 162, height: 162)
|
||||
}
|
||||
.contextMenu {
|
||||
Button("Rename") {
|
||||
collectionToRename = collection
|
||||
renameCollectionName = collection.name
|
||||
isShowingRenamePrompt = true
|
||||
}
|
||||
Button(role: .destructive) {
|
||||
libraryManager.deleteCollection(id: collection.id)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top)
|
||||
.scrollViewBottomPadding()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.alert("Create Collection", isPresented: $isShowingCreateCollection) {
|
||||
TextField("Collection Name", text: $newCollectionName)
|
||||
Button("Cancel", role: .cancel) {
|
||||
newCollectionName = ""
|
||||
}
|
||||
Button("Create") {
|
||||
if !newCollectionName.isEmpty {
|
||||
libraryManager.createCollection(name: newCollectionName)
|
||||
newCollectionName = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert("Rename Collection", isPresented: $isShowingRenamePrompt, presenting: collectionToRename) { collection in
|
||||
TextField("Collection Name", text: $renameCollectionName)
|
||||
Button("Cancel", role: .cancel) {
|
||||
collectionToRename = nil
|
||||
renameCollectionName = ""
|
||||
}
|
||||
Button("Rename") {
|
||||
if !renameCollectionName.isEmpty {
|
||||
libraryManager.renameCollection(id: collection.id, newName: renameCollectionName)
|
||||
}
|
||||
collectionToRename = nil
|
||||
renameCollectionName = ""
|
||||
}
|
||||
} message: { _ in EmptyView() }
|
||||
.onAppear {
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first,
|
||||
|
|
@ -285,12 +402,17 @@ private struct BookmarksDetailGridCell: View {
|
|||
ZStack(alignment: .topTrailing) {
|
||||
BookmarkCell(bookmark: bookmark)
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(.black)
|
||||
.background(Color.white.clipShape(Circle()).opacity(0.8))
|
||||
.offset(x: -8, y: 8)
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.white)
|
||||
.frame(width: 32, height: 32)
|
||||
Image(systemName: "checkmark")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 18, height: 18)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.offset(x: -8, y: 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,78 +1,68 @@
|
|||
//
|
||||
// AllBookmarks.swift
|
||||
// Sulfur
|
||||
// CollectionDetailView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by paul on 29/04/2025.
|
||||
// Created by paul on 18/06/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
import NukeUI
|
||||
|
||||
extension View {
|
||||
func circularGradientOutlineTwo() -> some View {
|
||||
self.background(
|
||||
Circle()
|
||||
.stroke(
|
||||
LinearGradient(
|
||||
gradient: Gradient(stops: [
|
||||
.init(color: Color.accentColor.opacity(0.25), location: 0),
|
||||
.init(color: Color.accentColor.opacity(0), location: 1)
|
||||
]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
),
|
||||
lineWidth: 0.5
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct AllBookmarks: View {
|
||||
@EnvironmentObject var libraryManager: LibraryManager
|
||||
@EnvironmentObject var moduleManager: ModuleManager
|
||||
struct CollectionDetailView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@EnvironmentObject private var libraryManager: LibraryManager
|
||||
@EnvironmentObject private var moduleManager: ModuleManager
|
||||
|
||||
let collection: BookmarkCollection
|
||||
@State private var sortOption: SortOption = .dateAdded
|
||||
@State private var searchText: String = ""
|
||||
@State private var isSearchActive: Bool = false
|
||||
@State private var sortOption: SortOption = .title
|
||||
@State private var isSelecting: Bool = false
|
||||
@State private var selectedBookmarks: Set<LibraryItem.ID> = []
|
||||
|
||||
enum SortOption: String, CaseIterable {
|
||||
case title = "Title"
|
||||
case dateAdded = "Date Added"
|
||||
case title = "Title"
|
||||
case source = "Source"
|
||||
}
|
||||
|
||||
var filteredAndSortedBookmarks: [LibraryItem] {
|
||||
let filtered = searchText.isEmpty ? libraryManager.bookmarks : libraryManager.bookmarks.filter { item in
|
||||
private var filteredAndSortedBookmarks: [LibraryItem] {
|
||||
let filtered = searchText.isEmpty ? collection.bookmarks : collection.bookmarks.filter { item in
|
||||
item.title.localizedCaseInsensitiveContains(searchText) ||
|
||||
item.moduleName.localizedCaseInsensitiveContains(searchText)
|
||||
}
|
||||
switch sortOption {
|
||||
case .title:
|
||||
return filtered.sorted { $0.title.lowercased() < $1.title.lowercased() }
|
||||
case .dateAdded:
|
||||
return filtered
|
||||
case .title:
|
||||
return filtered.sorted { $0.title.lowercased() < $1.title.lowercased() }
|
||||
case .source:
|
||||
return filtered.sorted { $0.moduleName < $1.moduleName }
|
||||
return filtered.sorted { item1, item2 in
|
||||
let module1 = moduleManager.modules.first { $0.id.uuidString == item1.moduleId }
|
||||
let module2 = moduleManager.modules.first { $0.id.uuidString == item2.moduleId }
|
||||
return (module1?.metadata.sourceName ?? "") < (module2?.metadata.sourceName ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Button(action: { }) {
|
||||
HStack(spacing: 8) {
|
||||
Button(action: { dismiss() }) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 24))
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
Button(action: { }) {
|
||||
Text("All Bookmarks")
|
||||
Button(action: { dismiss() }) {
|
||||
Text(collection.name)
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.layoutPriority(1)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
Spacer()
|
||||
HStack(spacing: 16) {
|
||||
Button(action: {
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
|
|
@ -93,7 +83,7 @@ struct AllBookmarks: View {
|
|||
.fill(Color.gray.opacity(0.2))
|
||||
.shadow(color: .accentColor.opacity(0.2), radius: 2)
|
||||
)
|
||||
.circularGradientOutlineTwo()
|
||||
.circularGradientOutline()
|
||||
}
|
||||
Menu {
|
||||
ForEach(SortOption.allCases, id: \.self) { option in
|
||||
|
|
@ -121,14 +111,14 @@ struct AllBookmarks: View {
|
|||
.fill(Color.gray.opacity(0.2))
|
||||
.shadow(color: .accentColor.opacity(0.2), radius: 2)
|
||||
)
|
||||
.circularGradientOutlineTwo()
|
||||
.circularGradientOutline()
|
||||
}
|
||||
Button(action: {
|
||||
if isSelecting {
|
||||
if !selectedBookmarks.isEmpty {
|
||||
for id in selectedBookmarks {
|
||||
if let item = libraryManager.bookmarks.first(where: { $0.id == id }) {
|
||||
libraryManager.removeBookmark(item: item)
|
||||
if let item = collection.bookmarks.first(where: { $0.id == id }) {
|
||||
libraryManager.removeBookmarkFromCollection(bookmarkId: id, collectionId: collection.id)
|
||||
}
|
||||
}
|
||||
selectedBookmarks.removeAll()
|
||||
|
|
@ -149,12 +139,14 @@ struct AllBookmarks: View {
|
|||
.fill(Color.gray.opacity(0.2))
|
||||
.shadow(color: .accentColor.opacity(0.2), radius: 2)
|
||||
)
|
||||
.circularGradientOutlineTwo()
|
||||
.circularGradientOutline()
|
||||
}
|
||||
}
|
||||
.layoutPriority(0)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top)
|
||||
|
||||
if isSearchActive {
|
||||
HStack(spacing: 12) {
|
||||
HStack(spacing: 12) {
|
||||
|
|
@ -204,122 +196,92 @@ struct AllBookmarks: View {
|
|||
removal: .move(edge: .top).combined(with: .opacity)
|
||||
))
|
||||
}
|
||||
BookmarkGridView(
|
||||
bookmarks: filteredAndSortedBookmarks,
|
||||
moduleManager: moduleManager,
|
||||
isSelecting: isSelecting,
|
||||
selectedBookmarks: $selectedBookmarks
|
||||
)
|
||||
.withGridPadding()
|
||||
Spacer()
|
||||
|
||||
if filteredAndSortedBookmarks.isEmpty {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "bookmark")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
Text("No Bookmarks")
|
||||
.font(.headline)
|
||||
Text("Add bookmarks to this collection")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
ScrollView(showsIndicators: false) {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))], spacing: 16) {
|
||||
ForEach(filteredAndSortedBookmarks) { bookmark in
|
||||
if let module = moduleManager.modules.first(where: { $0.id.uuidString == bookmark.moduleId }) {
|
||||
if isSelecting {
|
||||
Button(action: {
|
||||
if selectedBookmarks.contains(bookmark.id) {
|
||||
selectedBookmarks.remove(bookmark.id)
|
||||
} else {
|
||||
selectedBookmarks.insert(bookmark.id)
|
||||
}
|
||||
}) {
|
||||
BookmarkGridItemView(item: bookmark, module: module)
|
||||
.overlay(
|
||||
selectedBookmarks.contains(bookmark.id) ?
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.white)
|
||||
.frame(width: 32, height: 32)
|
||||
Image(systemName: "checkmark")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 18, height: 18)
|
||||
.foregroundColor(.black)
|
||||
}
|
||||
.padding(8)
|
||||
: nil,
|
||||
alignment: .topTrailing
|
||||
)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(role: .destructive) {
|
||||
libraryManager.removeBookmarkFromCollection(bookmarkId: bookmark.id, collectionId: collection.id)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NavigationLink(destination: MediaInfoView(
|
||||
title: bookmark.title,
|
||||
imageUrl: bookmark.imageUrl,
|
||||
href: bookmark.href,
|
||||
module: module
|
||||
)) {
|
||||
BookmarkGridItemView(item: bookmark, module: module)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(role: .destructive) {
|
||||
libraryManager.removeBookmarkFromCollection(bookmarkId: bookmark.id, collectionId: collection.id)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.scrollViewBottomPadding()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.onAppear(perform: setupNavigationController)
|
||||
}
|
||||
|
||||
private func setupNavigationController() {
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first,
|
||||
let navigationController = window.rootViewController?.children.first as? UINavigationController {
|
||||
navigationController.interactivePopGestureRecognizer?.isEnabled = true
|
||||
navigationController.interactivePopGestureRecognizer?.delegate = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BookmarkCell: View {
|
||||
let bookmark: LibraryItem
|
||||
@EnvironmentObject private var moduleManager: ModuleManager
|
||||
@EnvironmentObject private var libraryManager: LibraryManager
|
||||
|
||||
var body: some View {
|
||||
if let module = moduleManager.modules.first(where: { $0.id.uuidString == bookmark.moduleId }) {
|
||||
ZStack {
|
||||
LazyImage(url: URL(string: bookmark.imageUrl)) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(0.72, contentMode: .fill)
|
||||
.frame(width: 162, height: 243)
|
||||
.cornerRadius(12)
|
||||
.clipped()
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 162, height: 243)
|
||||
}
|
||||
}
|
||||
.overlay(
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.black.opacity(0.5))
|
||||
.frame(width: 28, height: 28)
|
||||
.overlay(
|
||||
LazyImage(url: URL(string: module.metadata.iconUrl)) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 32, height: 32)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding(8),
|
||||
alignment: .topLeading
|
||||
)
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
Text(bookmark.title)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.lineLimit(2)
|
||||
.foregroundColor(.white)
|
||||
.padding(12)
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
.black.opacity(0.7),
|
||||
.black.opacity(0.0)
|
||||
],
|
||||
startPoint: .bottom,
|
||||
endPoint: .top
|
||||
)
|
||||
.shadow(color: .black, radius: 4, x: 0, y: 2)
|
||||
)
|
||||
}
|
||||
.frame(width: 162)
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.padding(4)
|
||||
.contextMenu {
|
||||
Button(role: .destructive, action: {
|
||||
libraryManager.removeBookmark(item: bookmark)
|
||||
}) {
|
||||
Label("Remove from Bookmarks", systemImage: "trash")
|
||||
}
|
||||
.onAppear {
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first,
|
||||
let navigationController = window.rootViewController?.children.first as? UINavigationController {
|
||||
navigationController.interactivePopGestureRecognizer?.isEnabled = true
|
||||
navigationController.interactivePopGestureRecognizer?.delegate = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension View {
|
||||
func withNavigationBarModifiers() -> some View {
|
||||
self
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
func withGridPadding() -> some View {
|
||||
self
|
||||
.padding(.top)
|
||||
.padding()
|
||||
.scrollViewBottomPadding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
//
|
||||
// LibraryManager.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by paul on 18/06/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CollectionPickerView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@EnvironmentObject private var libraryManager: LibraryManager
|
||||
let bookmark: LibraryItem
|
||||
@State private var newCollectionName: String = ""
|
||||
@State private var isShowingNewCollectionField: Bool = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
if isShowingNewCollectionField {
|
||||
Section {
|
||||
HStack {
|
||||
TextField("Collection name", text: $newCollectionName)
|
||||
Button("Create") {
|
||||
if !newCollectionName.isEmpty {
|
||||
libraryManager.createCollection(name: newCollectionName)
|
||||
if let newCollection = libraryManager.collections.first(where: { $0.name == newCollectionName }) {
|
||||
libraryManager.addBookmarkToCollection(bookmark: bookmark, collectionId: newCollection.id)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
.disabled(newCollectionName.isEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
ForEach(libraryManager.collections) { collection in
|
||||
Button(action: {
|
||||
libraryManager.addBookmarkToCollection(bookmark: bookmark, collectionId: collection.id)
|
||||
dismiss()
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "folder")
|
||||
Text(collection.name)
|
||||
Spacer()
|
||||
Text("\(collection.bookmarks.count)")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Add to Collection")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(action: {
|
||||
isShowingNewCollectionField.toggle()
|
||||
}) {
|
||||
Image(systemName: "folder.badge.plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,21 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct BookmarkCollection: Codable, Identifiable {
|
||||
let id: UUID
|
||||
let name: String
|
||||
var bookmarks: [LibraryItem]
|
||||
let dateCreated: Date
|
||||
|
||||
init(name: String, bookmarks: [LibraryItem] = []) {
|
||||
self.id = UUID()
|
||||
self.name = name
|
||||
self.bookmarks = bookmarks
|
||||
self.dateCreated = Date()
|
||||
}
|
||||
}
|
||||
|
||||
struct LibraryItem: Codable, Identifiable {
|
||||
let id: UUID
|
||||
|
|
@ -28,62 +43,134 @@ struct LibraryItem: Codable, Identifiable {
|
|||
}
|
||||
|
||||
class LibraryManager: ObservableObject {
|
||||
@Published var bookmarks: [LibraryItem] = []
|
||||
private let bookmarksKey = "bookmarkedItems"
|
||||
@Published var collections: [BookmarkCollection] = []
|
||||
@Published var isShowingCollectionPicker: Bool = false
|
||||
@Published var bookmarkToAdd: LibraryItem?
|
||||
|
||||
private let collectionsKey = "bookmarkCollections"
|
||||
private let oldBookmarksKey = "bookmarkedItems"
|
||||
|
||||
init() {
|
||||
loadBookmarks()
|
||||
migrateOldBookmarks()
|
||||
loadCollections()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleiCloudSync), name: .iCloudSyncDidComplete, object: nil)
|
||||
}
|
||||
|
||||
@objc private func handleiCloudSync() {
|
||||
DispatchQueue.main.async {
|
||||
self.loadBookmarks()
|
||||
self.loadCollections()
|
||||
}
|
||||
}
|
||||
|
||||
func removeBookmark(item: LibraryItem) {
|
||||
if let index = bookmarks.firstIndex(where: { $0.id == item.id }) {
|
||||
bookmarks.remove(at: index)
|
||||
Logger.shared.log("Removed series \(item.id) from bookmarks.",type: "Debug")
|
||||
saveBookmarks()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadBookmarks() {
|
||||
guard let data = UserDefaults.standard.data(forKey: bookmarksKey) else {
|
||||
Logger.shared.log("No bookmarks data found in UserDefaults.", type: "Debug")
|
||||
private func migrateOldBookmarks() {
|
||||
guard let data = UserDefaults.standard.data(forKey: oldBookmarksKey) else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
bookmarks = try JSONDecoder().decode([LibraryItem].self, from: data)
|
||||
let oldBookmarks = try JSONDecoder().decode([LibraryItem].self, from: data)
|
||||
if !oldBookmarks.isEmpty {
|
||||
// Check if "Old Bookmarks" collection already exists
|
||||
if let existingIndex = collections.firstIndex(where: { $0.name == "Old Bookmarks" }) {
|
||||
// Add new bookmarks to existing collection, avoiding duplicates
|
||||
for bookmark in oldBookmarks {
|
||||
if !collections[existingIndex].bookmarks.contains(where: { $0.href == bookmark.href }) {
|
||||
collections[existingIndex].bookmarks.insert(bookmark, at: 0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Create new "Old Bookmarks" collection
|
||||
let oldCollection = BookmarkCollection(name: "Old Bookmarks", bookmarks: oldBookmarks)
|
||||
collections.append(oldCollection)
|
||||
}
|
||||
saveCollections()
|
||||
}
|
||||
|
||||
UserDefaults.standard.removeObject(forKey: oldBookmarksKey)
|
||||
} catch {
|
||||
Logger.shared.log("Failed to decode bookmarks: \(error.localizedDescription)", type: "Error")
|
||||
Logger.shared.log("Failed to migrate old bookmarks: \(error)", type: "Error")
|
||||
}
|
||||
}
|
||||
|
||||
private func saveBookmarks() {
|
||||
private func loadCollections() {
|
||||
guard let data = UserDefaults.standard.data(forKey: collectionsKey) else {
|
||||
Logger.shared.log("No collections data found in UserDefaults.", type: "Debug")
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let encoded = try JSONEncoder().encode(bookmarks)
|
||||
UserDefaults.standard.set(encoded, forKey: bookmarksKey)
|
||||
collections = try JSONDecoder().decode([BookmarkCollection].self, from: data)
|
||||
} catch {
|
||||
Logger.shared.log("Failed to save bookmarks: \(error)", type: "Error")
|
||||
Logger.shared.log("Failed to decode collections: \(error.localizedDescription)", type: "Error")
|
||||
}
|
||||
}
|
||||
|
||||
private func saveCollections() {
|
||||
do {
|
||||
let encoded = try JSONEncoder().encode(collections)
|
||||
UserDefaults.standard.set(encoded, forKey: collectionsKey)
|
||||
} catch {
|
||||
Logger.shared.log("Failed to save collections: \(error)", type: "Error")
|
||||
}
|
||||
}
|
||||
|
||||
func createCollection(name: String) {
|
||||
let newCollection = BookmarkCollection(name: name)
|
||||
collections.append(newCollection)
|
||||
saveCollections()
|
||||
}
|
||||
|
||||
func deleteCollection(id: UUID) {
|
||||
collections.removeAll { $0.id == id }
|
||||
saveCollections()
|
||||
}
|
||||
|
||||
func addBookmarkToCollection(bookmark: LibraryItem, collectionId: UUID) {
|
||||
if let index = collections.firstIndex(where: { $0.id == collectionId }) {
|
||||
if !collections[index].bookmarks.contains(where: { $0.href == bookmark.href }) {
|
||||
collections[index].bookmarks.insert(bookmark, at: 0)
|
||||
saveCollections()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeBookmarkFromCollection(bookmarkId: UUID, collectionId: UUID) {
|
||||
if let collectionIndex = collections.firstIndex(where: { $0.id == collectionId }) {
|
||||
collections[collectionIndex].bookmarks.removeAll { $0.id == bookmarkId }
|
||||
saveCollections()
|
||||
}
|
||||
}
|
||||
|
||||
func isBookmarked(href: String, moduleName: String) -> Bool {
|
||||
bookmarks.contains { $0.href == href }
|
||||
for collection in collections {
|
||||
if collection.bookmarks.contains(where: { $0.href == href }) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func toggleBookmark(title: String, imageUrl: String, href: String, moduleId: String, moduleName: String) {
|
||||
if let index = bookmarks.firstIndex(where: { $0.href == href }) {
|
||||
bookmarks.remove(at: index)
|
||||
} else {
|
||||
let bookmark = LibraryItem(title: title, imageUrl: imageUrl, href: href, moduleId: moduleId, moduleName: moduleName)
|
||||
bookmarks.insert(bookmark, at: 0)
|
||||
for (collectionIndex, collection) in collections.enumerated() {
|
||||
if let bookmarkIndex = collection.bookmarks.firstIndex(where: { $0.href == href }) {
|
||||
collections[collectionIndex].bookmarks.remove(at: bookmarkIndex)
|
||||
saveCollections()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let bookmark = LibraryItem(title: title, imageUrl: imageUrl, href: href, moduleId: moduleId, moduleName: moduleName)
|
||||
bookmarkToAdd = bookmark
|
||||
isShowingCollectionPicker = true
|
||||
}
|
||||
|
||||
func renameCollection(id: UUID, newName: String) {
|
||||
if let index = collections.firstIndex(where: { $0.id == id }) {
|
||||
var updated = collections[index]
|
||||
updated = BookmarkCollection(name: newName, bookmarks: updated.bookmarks)
|
||||
collections[index] = BookmarkCollection(name: newName, bookmarks: updated.bookmarks)
|
||||
saveCollections()
|
||||
}
|
||||
saveBookmarks()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,9 +20,6 @@ struct LibraryView: View {
|
|||
@Environment(\.verticalSizeClass) var verticalSizeClass
|
||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||
|
||||
@State private var selectedBookmark: LibraryItem? = nil
|
||||
@State private var isDetailActive: Bool = false
|
||||
|
||||
@State private var continueWatchingItems: [ContinueWatchingItem] = []
|
||||
@State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape
|
||||
@State private var selectedTab: Int = 0
|
||||
|
|
@ -111,16 +108,16 @@ struct LibraryView: View {
|
|||
|
||||
HStack {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "bookmark.fill")
|
||||
Image(systemName: "folder.fill")
|
||||
.font(.subheadline)
|
||||
Text("Bookmarks")
|
||||
Text("Collections")
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
NavigationLink(destination: BookmarksDetailView(bookmarks: $libraryManager.bookmarks)) {
|
||||
NavigationLink(destination: BookmarksDetailView()) {
|
||||
Text("View All")
|
||||
.font(.subheadline)
|
||||
.padding(.horizontal, 12)
|
||||
|
|
@ -132,32 +129,9 @@ struct LibraryView: View {
|
|||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
|
||||
BookmarksSection(
|
||||
selectedBookmark: $selectedBookmark,
|
||||
isDetailActive: $isDetailActive
|
||||
)
|
||||
BookmarksSection()
|
||||
|
||||
Spacer().frame(height: 100)
|
||||
|
||||
NavigationLink(
|
||||
destination: Group {
|
||||
if let bookmark = selectedBookmark,
|
||||
let module = moduleManager.modules.first(where: {
|
||||
$0.id.uuidString == bookmark.moduleId
|
||||
}) {
|
||||
MediaInfoView(title: bookmark.title,
|
||||
imageUrl: bookmark.imageUrl,
|
||||
href: bookmark.href,
|
||||
module: module)
|
||||
} else {
|
||||
Text("No Data Available")
|
||||
}
|
||||
},
|
||||
isActive: $isDetailActive
|
||||
) {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
|
|
@ -462,33 +436,66 @@ extension View {
|
|||
struct BookmarksSection: View {
|
||||
@EnvironmentObject private var libraryManager: LibraryManager
|
||||
@EnvironmentObject private var moduleManager: ModuleManager
|
||||
|
||||
@Binding var selectedBookmark: LibraryItem?
|
||||
@Binding var isDetailActive: Bool
|
||||
@State private var isShowingRenamePrompt: Bool = false
|
||||
@State private var collectionToRename: BookmarkCollection? = nil
|
||||
@State private var renameCollectionName: String = ""
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
if libraryManager.bookmarks.isEmpty {
|
||||
if libraryManager.collections.isEmpty {
|
||||
EmptyBookmarksView()
|
||||
} else {
|
||||
BookmarksGridView(
|
||||
selectedBookmark: $selectedBookmark,
|
||||
isDetailActive: $isDetailActive
|
||||
)
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 12) {
|
||||
ForEach(Array(libraryManager.collections.prefix(5))) { collection in
|
||||
NavigationLink(destination: CollectionDetailView(collection: collection)) {
|
||||
BookmarkCollectionGridCell(collection: collection, width: 162, height: 162)
|
||||
}
|
||||
.contextMenu {
|
||||
Button("Rename") {
|
||||
collectionToRename = collection
|
||||
renameCollectionName = collection.name
|
||||
isShowingRenamePrompt = true
|
||||
}
|
||||
Button(role: .destructive) {
|
||||
libraryManager.deleteCollection(id: collection.id)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.frame(height: 220)
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert("Rename Collection", isPresented: $isShowingRenamePrompt, presenting: collectionToRename) { collection in
|
||||
TextField("Collection Name", text: $renameCollectionName)
|
||||
Button("Cancel", role: .cancel) {
|
||||
collectionToRename = nil
|
||||
renameCollectionName = ""
|
||||
}
|
||||
Button("Rename") {
|
||||
if !renameCollectionName.isEmpty {
|
||||
libraryManager.renameCollection(id: collection.id, newName: renameCollectionName)
|
||||
}
|
||||
collectionToRename = nil
|
||||
renameCollectionName = ""
|
||||
}
|
||||
} message: { _ in EmptyView() }
|
||||
}
|
||||
}
|
||||
|
||||
struct EmptyBookmarksView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "magazine")
|
||||
Image(systemName: "folder")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
Text("You have no items saved.")
|
||||
Text("No Collections")
|
||||
.font(.headline)
|
||||
Text("Bookmark items for an easier access later.")
|
||||
Text("Create a collection to organize your bookmarks")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
|
@ -496,120 +503,3 @@ struct EmptyBookmarksView: View {
|
|||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
struct BookmarksGridView: View {
|
||||
@EnvironmentObject private var libraryManager: LibraryManager
|
||||
@EnvironmentObject private var moduleManager: ModuleManager
|
||||
|
||||
@Binding var selectedBookmark: LibraryItem?
|
||||
@Binding var isDetailActive: Bool
|
||||
|
||||
private var recentBookmarks: [LibraryItem] {
|
||||
Array(libraryManager.bookmarks.prefix(5))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 12) {
|
||||
ForEach(recentBookmarks) { item in
|
||||
BookmarkItemView(
|
||||
item: item,
|
||||
selectedBookmark: $selectedBookmark,
|
||||
isDetailActive: $isDetailActive
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.frame(height: 243)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BookmarkItemView: View {
|
||||
@EnvironmentObject private var libraryManager: LibraryManager
|
||||
@EnvironmentObject private var moduleManager: ModuleManager
|
||||
|
||||
let item: LibraryItem
|
||||
@Binding var selectedBookmark: LibraryItem?
|
||||
@Binding var isDetailActive: Bool
|
||||
|
||||
var body: some View {
|
||||
if let module = moduleManager.modules.first(where: { $0.id.uuidString == item.moduleId }) {
|
||||
Button(action: {
|
||||
selectedBookmark = item
|
||||
isDetailActive = true
|
||||
}) {
|
||||
ZStack {
|
||||
LazyImage(url: URL(string: item.imageUrl)) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(0.72, contentMode: .fill)
|
||||
.frame(width: 162, height: 243)
|
||||
.cornerRadius(12)
|
||||
.clipped()
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.aspectRatio(2/3, contentMode: .fit)
|
||||
.redacted(reason: .placeholder)
|
||||
}
|
||||
}
|
||||
.overlay(
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.black.opacity(0.5))
|
||||
.frame(width: 28, height: 28)
|
||||
.overlay(
|
||||
LazyImage(url: URL(string: module.metadata.iconUrl)) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 32, height: 32)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding(8),
|
||||
alignment: .topLeading
|
||||
)
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
Text(item.title)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.lineLimit(2)
|
||||
.foregroundColor(.white)
|
||||
.padding(12)
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
.black.opacity(0.7),
|
||||
.black.opacity(0.0)
|
||||
],
|
||||
startPoint: .bottom,
|
||||
endPoint: .top
|
||||
)
|
||||
.shadow(color: .black, radius: 4, x: 0, y: 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
.frame(width: 162, height: 243)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
.contextMenu {
|
||||
Button(role: .destructive, action: {
|
||||
libraryManager.removeBookmark(item: item)
|
||||
}) {
|
||||
Label("Remove from Bookmarks", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -197,6 +197,11 @@ struct MediaInfoView: View {
|
|||
|
||||
navigationOverlay
|
||||
}
|
||||
.sheet(isPresented: $libraryManager.isShowingCollectionPicker) {
|
||||
if let bookmark = libraryManager.bookmarkToAdd {
|
||||
CollectionPickerView(bookmark: bookmark)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
|
|||
|
|
@ -127,5 +127,10 @@ struct SearchResultsGrid: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $libraryManager.isShowingCollectionPicker) {
|
||||
if let bookmark = libraryManager.bookmarkToAdd {
|
||||
CollectionPickerView(bookmark: bookmark)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,12 +29,15 @@
|
|||
0488FA9A2DFDF380007575E1 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0488FA982DFDF380007575E1 /* Localizable.strings */; };
|
||||
0488FA9E2DFDF3BB007575E1 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0488FA9C2DFDF3BB007575E1 /* Localizable.strings */; };
|
||||
04A1B73C2DFF39EB0064688A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 04A1B73A2DFF39EB0064688A /* Localizable.strings */; };
|
||||
04AD070F2E035D6E00EB74C1 /* BookmarkCollectionGridCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AD070D2E035D6E00EB74C1 /* BookmarkCollectionGridCell.swift */; };
|
||||
04AD07102E035D6E00EB74C1 /* CollectionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AD070E2E035D6E00EB74C1 /* CollectionDetailView.swift */; };
|
||||
04AD07122E0360CD00EB74C1 /* CollectionPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AD07112E0360CD00EB74C1 /* CollectionPickerView.swift */; };
|
||||
04AD07162E03704700EB74C1 /* BookmarkCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AD07152E03704700EB74C1 /* BookmarkCell.swift */; };
|
||||
04CD76DB2DE20F2200733536 /* AllWatching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04CD76DA2DE20F2200733536 /* AllWatching.swift */; };
|
||||
04EAC39A2DF9E0DB00BBD483 /* SplashScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04EAC3992DF9E0DB00BBD483 /* SplashScreenView.swift */; };
|
||||
04F08EDC2DE10BF3006B29D9 /* ProgressiveBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F08EDB2DE10BEC006B29D9 /* ProgressiveBlurView.swift */; };
|
||||
04F08EDF2DE10C1D006B29D9 /* TabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F08EDE2DE10C1A006B29D9 /* TabBar.swift */; };
|
||||
04F08EE22DE10C40006B29D9 /* TabItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F08EE12DE10C27006B29D9 /* TabItem.swift */; };
|
||||
04F08EE42DE10D6F006B29D9 /* AllBookmarks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F08EE32DE10D6B006B29D9 /* AllBookmarks.swift */; };
|
||||
130326B62DF979AC00AEF610 /* WebAuthenticationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130326B52DF979AC00AEF610 /* WebAuthenticationManager.swift */; };
|
||||
130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */; };
|
||||
13103E8B2D58E028000F0673 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E8A2D58E028000F0673 /* View.swift */; };
|
||||
|
|
@ -137,12 +140,15 @@
|
|||
0488FA992DFDF380007575E1 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
0488FA9D2DFDF3BB007575E1 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
04A1B7392DFF39EB0064688A /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nn; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
04AD070D2E035D6E00EB74C1 /* BookmarkCollectionGridCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkCollectionGridCell.swift; sourceTree = "<group>"; };
|
||||
04AD070E2E035D6E00EB74C1 /* CollectionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionDetailView.swift; sourceTree = "<group>"; };
|
||||
04AD07112E0360CD00EB74C1 /* CollectionPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionPickerView.swift; sourceTree = "<group>"; };
|
||||
04AD07152E03704700EB74C1 /* BookmarkCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkCell.swift; sourceTree = "<group>"; };
|
||||
04CD76DA2DE20F2200733536 /* AllWatching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllWatching.swift; sourceTree = "<group>"; };
|
||||
04EAC3992DF9E0DB00BBD483 /* SplashScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenView.swift; sourceTree = "<group>"; };
|
||||
04F08EDB2DE10BEC006B29D9 /* ProgressiveBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressiveBlurView.swift; sourceTree = "<group>"; };
|
||||
04F08EDE2DE10C1A006B29D9 /* TabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBar.swift; sourceTree = "<group>"; };
|
||||
04F08EE12DE10C27006B29D9 /* TabItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabItem.swift; sourceTree = "<group>"; };
|
||||
04F08EE32DE10D6B006B29D9 /* AllBookmarks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllBookmarks.swift; sourceTree = "<group>"; };
|
||||
130326B52DF979AC00AEF610 /* WebAuthenticationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebAuthenticationManager.swift; sourceTree = "<group>"; };
|
||||
130C6BF82D53A4C200DC1432 /* Sora.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Sora.entitlements; sourceTree = "<group>"; };
|
||||
130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewData.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -322,6 +328,10 @@
|
|||
0457C59C2DE78267000AFBD9 /* BookmarkComponents */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
04AD07152E03704700EB74C1 /* BookmarkCell.swift */,
|
||||
04AD07112E0360CD00EB74C1 /* CollectionPickerView.swift */,
|
||||
04AD070D2E035D6E00EB74C1 /* BookmarkCollectionGridCell.swift */,
|
||||
04AD070E2E035D6E00EB74C1 /* CollectionDetailView.swift */,
|
||||
0457C5A02DE78385000AFBD9 /* BookmarksDetailView.swift */,
|
||||
0457C5992DE78267000AFBD9 /* BookmarkGridItemView.swift */,
|
||||
0457C59A2DE78267000AFBD9 /* BookmarkGridView.swift */,
|
||||
|
|
@ -604,7 +614,6 @@
|
|||
children = (
|
||||
0457C59C2DE78267000AFBD9 /* BookmarkComponents */,
|
||||
04CD76DA2DE20F2200733536 /* AllWatching.swift */,
|
||||
04F08EE32DE10D6B006B29D9 /* AllBookmarks.swift */,
|
||||
133F55BA2D33B55100E08EEA /* LibraryManager.swift */,
|
||||
133D7C7E2D2BE2630075467E /* LibraryView.swift */,
|
||||
);
|
||||
|
|
@ -855,6 +864,7 @@
|
|||
sv,
|
||||
bos,
|
||||
bs,
|
||||
cs,
|
||||
);
|
||||
mainGroup = 133D7C612D2BE2500075467E;
|
||||
packageReferences = (
|
||||
|
|
@ -919,8 +929,8 @@
|
|||
136BBE802DB1038000906B5E /* Notification+Name.swift in Sources */,
|
||||
13DB46902D900A38008CBC03 /* URL.swift in Sources */,
|
||||
130326B62DF979AC00AEF610 /* WebAuthenticationManager.swift in Sources */,
|
||||
04F08EE42DE10D6F006B29D9 /* AllBookmarks.swift in Sources */,
|
||||
138FE1D02DECA00D00936D81 /* TMDB-FetchID.swift in Sources */,
|
||||
04AD07162E03704700EB74C1 /* BookmarkCell.swift in Sources */,
|
||||
130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */,
|
||||
0402DA172DE7B7B8003BB42C /* SearchViewComponents.swift in Sources */,
|
||||
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */,
|
||||
|
|
@ -934,6 +944,8 @@
|
|||
13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */,
|
||||
13C0E5EC2D5F85F800E7F619 /* ContinueWatchingItem.swift in Sources */,
|
||||
13CBA0882D60F19C00EFE70A /* VTTSubtitlesLoader.swift in Sources */,
|
||||
04AD070F2E035D6E00EB74C1 /* BookmarkCollectionGridCell.swift in Sources */,
|
||||
04AD07102E035D6E00EB74C1 /* CollectionDetailView.swift in Sources */,
|
||||
13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */,
|
||||
133D7C8F2D2BE2640075467E /* MediaInfoView.swift in Sources */,
|
||||
04F08EE22DE10C40006B29D9 /* TabItem.swift in Sources */,
|
||||
|
|
@ -949,6 +961,7 @@
|
|||
138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */,
|
||||
722248662DCBC13E00CABE2D /* JSController-Downloads.swift in Sources */,
|
||||
133D7C8C2D2BE2640075467E /* SearchView.swift in Sources */,
|
||||
04AD07122E0360CD00EB74C1 /* CollectionPickerView.swift in Sources */,
|
||||
133CF6A62DFEBE9000BD13F9 /* VideoWatchingActivity.swift in Sources */,
|
||||
13DB468E2D90093A008CBC03 /* Anilist-Token.swift in Sources */,
|
||||
1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */,
|
||||
|
|
|
|||
Loading…
Reference in a new issue