Skip to content

Commit

Permalink
Merge pull request #1005 from kiwix/1003-deleting-zim-file-should-sto…
Browse files Browse the repository at this point in the history
…p-playing-pip-video

Close Picture in Picture if the ZIM file is deleted/unlinked
  • Loading branch information
kelson42 authored Nov 1, 2024
2 parents 7a6425c + baada11 commit f76e171
Show file tree
Hide file tree
Showing 31 changed files with 316 additions and 218 deletions.
62 changes: 37 additions & 25 deletions App/App_macOS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,20 +99,24 @@ struct Kiwix: App {

struct RootView: View {
@Environment(\.controlActiveState) var controlActiveState
@StateObject private var browser = BrowserViewModel()
@StateObject private var navigation = NavigationViewModel()
@StateObject private var windowTracker = WindowTracker()

private let primaryItems: [NavigationItem] = [.reading, .bookmarks]
private let primaryItems: [NavigationItem] = [.bookmarks]
private let libraryItems: [NavigationItem] = [.opened, .categories, .downloads, .new]
private let openURL = NotificationCenter.default.publisher(for: .openURL)
private let appTerminates = NotificationCenter.default.publisher(for: NSApplication.willTerminateNotification)
private let tabCloses = NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)
/// Close other tabs then the ones received
private let keepOnlyTabs = NotificationCenter.default.publisher(for: .keepOnlyTabs)

var body: some View {
NavigationSplitView {
List(selection: $navigation.currentItem) {
ForEach(primaryItems, id: \.self) { navigationItem in
ForEach(
[NavigationItem.tab(objectID: navigation.currentTabId)] + primaryItems,
id: \.self
) { navigationItem in
Label(navigationItem.name, systemImage: navigationItem.icon)
}
if FeatureFlags.hasLibrary {
Expand All @@ -128,16 +132,12 @@ struct RootView: View {
switch navigation.currentItem {
case .loading:
LoadingDataView()
case .reading:
case .tab(let tabID):
let browser = BrowserViewModel.getCached(tabID: tabID)
BrowserTab().environmentObject(browser)
.withHostingWindow { window in
if let windowNumber = window?.windowNumber {
browser.restoreByWindowNumber(windowNumber: windowNumber,
urlToTabIdConverter: navigation.tabIDFor(url:))
} else {
if FeatureFlags.hasLibrary == false {
browser.loadMainArticle()
}
.withHostingWindow { [weak browser] _ in
if FeatureFlags.hasLibrary == false {
browser?.loadMainArticle()
}
}
case .bookmarks:
Expand All @@ -156,10 +156,10 @@ struct RootView: View {
}
.frame(minWidth: 650, minHeight: 500)
.focusedSceneValue(\.navigationItem, $navigation.currentItem)
.environmentObject(navigation)
.modifier(AlertHandler())
.modifier(OpenFileHandler())
.modifier(SaveContentHandler())
.environmentObject(navigation)
.onOpenURL { url in
if url.isFileURL {
NotificationCenter.openFiles([url], context: .file)
Expand All @@ -168,7 +168,9 @@ struct RootView: View {
}
}
.onReceive(openURL) { notification in
guard let url = notification.userInfo?["url"] as? URL else { return }
guard let url = notification.userInfo?["url"] as? URL else {
return
}
if notification.userInfo?["isFileContext"] as? Bool == true {
// handle the opened ZIM file from Finder
// for which the system opens a new window,
Expand All @@ -177,16 +179,17 @@ struct RootView: View {
// We need to filter it down the the last window
// (which is usually not the key window yet at this point),
// and load the content only within that
Task {
if windowTracker.isLastWindow() {
browser.load(url: url)
Task { @MainActor [weak navigation] in
if windowTracker.isLastWindow(), let navigation {
BrowserViewModel.getCached(tabID: navigation.currentTabId).load(url: url)
}
}
return
}
guard controlActiveState == .key else { return }
navigation.currentItem = .reading
browser.load(url: url)
let tabID = navigation.currentTabId
navigation.currentItem = .tab(objectID: tabID)
BrowserViewModel.getCached(tabID: tabID).load(url: url)
}
.onReceive(tabCloses) { publisher in
// closing one window either by CMD+W || red(X) close button
Expand All @@ -199,11 +202,20 @@ struct RootView: View {
// tab closed by app termination
return
}
if let tabID = browser.tabID {
// tab closed by user
browser.pauseVideoWhenNotInPIP()
navigation.deleteTab(tabID: tabID)
let tabID = navigation.currentTabId
let browser = BrowserViewModel.getCached(tabID: tabID)
// tab closed by user
browser.pauseVideoWhenNotInPIP()
Task { @MainActor [weak browser] in
await browser?.clear()
}
navigation.deleteTab(tabID: tabID)
}
.onReceive(keepOnlyTabs) { notification in
guard let tabsToKeep = notification.userInfo?["tabIds"] as? Set<NSManagedObjectID> else {
return
}
navigation.keepOnlyTabsBy(tabIds: tabsToKeep)
}
.onReceive(appTerminates) { _ in
// CMD+Q -> Quit Kiwix, this also closes the last window
Expand All @@ -212,14 +224,14 @@ struct RootView: View {
switch AppType.current {
case .kiwix:
await LibraryOperations.reopen()
navigation.currentItem = .reading
navigation.currentItem = .tab(objectID: navigation.currentTabId)
LibraryOperations.scanDirectory(URL.documentDirectory)
LibraryOperations.applyFileBackupSetting()
DownloadService.shared.restartHeartbeatIfNeeded()
case let .custom(zimFileURL):
await LibraryOperations.open(url: zimFileURL)
ZimMigration.forCustomApps()
navigation.currentItem = .reading
navigation.currentItem = .tab(objectID: navigation.currentTabId)
}
// MARK: - migrations
if !ProcessInfo.processInfo.arguments.contains("testing") {
Expand Down
2 changes: 1 addition & 1 deletion App/CompactViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ private struct CompactView: View {
case .library:
Library(dismiss: dismiss)
case .settings:
NavigationView {
NavigationStack {
Settings().toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
Expand Down
58 changes: 31 additions & 27 deletions App/SidebarViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl
collectionView, indexPath, item in
collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
}
dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
dataSource.supplementaryViewProvider = { collectionView, _, indexPath in
collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath)
}
return dataSource
Expand All @@ -45,6 +45,10 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl
cacheName: nil
)

private var navigationViewModel: NavigationViewModel? {
(splitViewController as? SplitViewController)?.navigationViewModel
}

enum Section: String, CaseIterable {
case primary
case tabs
Expand All @@ -62,7 +66,7 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl

init() {
super.init(collectionViewLayout: UICollectionViewLayout())
collectionView.collectionViewLayout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
collectionView.collectionViewLayout = UICollectionViewCompositionalLayout { _, layoutEnvironment in
var config = UICollectionLayoutListConfiguration(appearance: .sidebar)
config.headerMode = .supplementary
config.trailingSwipeActionsConfigurationProvider = { [unowned self] indexPath in
Expand All @@ -78,8 +82,8 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl
}

func updateSelection() {
guard let splitViewController = splitViewController as? SplitViewController,
let currentItem = splitViewController.navigationViewModel.currentItem,
guard let navigationViewModel,
let currentItem = navigationViewModel.currentItem,
let indexPath = dataSource.indexPath(for: currentItem),
collectionView.indexPathsForSelectedItems?.first != indexPath else { return }
collectionView.selectItem(at: indexPath, animated: true, scrollPosition: [])
Expand All @@ -96,26 +100,24 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl
navigationItem.rightBarButtonItem = UIBarButtonItem(
image: UIImage(systemName: "plus.square"),
primaryAction: UIAction { [unowned self] _ in
guard let splitViewController = splitViewController as? SplitViewController else { return }
splitViewController.navigationViewModel.createTab()
navigationViewModel?.createTab()
},
menu: UIMenu(children: [
UIAction(
title: "sidebar_view.navigation.button.close".localized,
image: UIImage(systemName: "xmark.square"),
attributes: .destructive
) { [unowned self] _ in
guard let splitViewController = splitViewController as? SplitViewController,
case let .tab(tabID) = splitViewController.navigationViewModel.currentItem else { return }
splitViewController.navigationViewModel.deleteTab(tabID: tabID)
guard let navigationViewModel,
case let .tab(tabID) = navigationViewModel.currentItem else { return }
navigationViewModel.deleteTab(tabID: tabID)
},
UIAction(
title: "sidebar_view.navigation.button.close_all".localized,
image: UIImage(systemName: "xmark.square.fill"),
attributes: .destructive
) { [unowned self] _ in
guard let splitViewController = splitViewController as? SplitViewController else { return }
splitViewController.navigationViewModel.deleteAllTabs()
navigationViewModel?.deleteAllTabs()
}
])
)
Expand Down Expand Up @@ -148,34 +150,35 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl
_ controller: NSFetchedResultsController<NSFetchRequestResult>,
didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference
) {
let tabs = snapshot.itemIdentifiers
let tabIds = snapshot.itemIdentifiers
.compactMap { $0 as? NSManagedObjectID }
.map { NavigationItem.tab(objectID: $0) }
var snapshot = NSDiffableDataSourceSectionSnapshot<NavigationItem>()
snapshot.append(tabs)
Task { [snapshot] in
await MainActor.run { [snapshot] in
let tabs = tabIds.map { NavigationItem.tab(objectID: $0) }
var tabsSnapshot = NSDiffableDataSourceSectionSnapshot<NavigationItem>()
tabsSnapshot.append(tabs)
Task { [tabsSnapshot] in
await MainActor.run { [tabsSnapshot] in
dataSource.apply(
snapshot,
tabsSnapshot,
to: .tabs,
animatingDifferences: dataSource.snapshot(for: .tabs).items.count > 0
) {
guard let indexPath = self.collectionView.indexPathsForSelectedItems?.first,
let item = self.dataSource.itemIdentifier(for: indexPath),
case .tab = item else { return }
var snapshot = self.dataSource.snapshot()
snapshot.reconfigureItems([item])
self.dataSource.apply(snapshot, animatingDifferences: true)
var sourceSnapshot = self.dataSource.snapshot()
sourceSnapshot.reconfigureItems([item])
self.dataSource.apply(sourceSnapshot, animatingDifferences: true)
}
}
}
}

override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let splitViewController = splitViewController as? SplitViewController,
guard let splitViewController,
let navigationViewModel,
let navigationItem = dataSource.itemIdentifier(for: indexPath) else { return }
if splitViewController.navigationViewModel.currentItem != navigationItem {
splitViewController.navigationViewModel.currentItem = navigationItem
if navigationViewModel.currentItem != navigationItem {
navigationViewModel.currentItem = navigationItem
}
if splitViewController.displayMode == .oneOverSecondary {
splitViewController.hide(.primary)
Expand Down Expand Up @@ -232,12 +235,13 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl
}

private func configureSwipeAction(indexPath: IndexPath) -> UISwipeActionsConfiguration? {
guard let splitViewController = splitViewController as? SplitViewController,
guard let navigationViewModel,
let item = dataSource.itemIdentifier(for: indexPath),
case let .tab(tabID) = item else { return nil }
let title = "sidebar_view.navigation.button.close".localized
let action = UIContextualAction(style: .destructive,
title: "sidebar_view.navigation.button.close".localized) { _, _, _ in
splitViewController.navigationViewModel.deleteTab(tabID: tabID)
title: title) { [weak navigationViewModel] _, _, _ in
navigationViewModel?.deleteTab(tabID: tabID)
}
action.image = UIImage(systemName: "xmark")
return UISwipeActionsConfiguration(actions: [action])
Expand Down
8 changes: 4 additions & 4 deletions App/SplitViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,10 @@ final class SplitViewController: UISplitViewController {
) { [weak self] notification in
guard let url = notification.userInfo?["url"] as? URL else { return }
let inNewTab = notification.userInfo?["inNewTab"] as? Bool ?? false
Task { @MainActor in
Task { @MainActor [weak self] in
if !inNewTab, case let .tab(tabID) = self?.navigationViewModel.currentItem {
BrowserViewModel.getCached(tabID: tabID).load(url: url)
} else {
guard let tabID = self?.navigationViewModel.createTab() else { return }
} else if let tabID = self?.navigationViewModel.createTab() {
BrowserViewModel.getCached(tabID: tabID).load(url: url)
}
}
Expand Down Expand Up @@ -114,7 +113,8 @@ final class SplitViewController: UISplitViewController {
let controller = UIHostingController(rootView: Bookmarks())
setViewController(UINavigationController(rootViewController: controller), for: .secondary)
case .tab(let tabID):
let view = BrowserTab().environmentObject(BrowserViewModel.getCached(tabID: tabID))
let view = BrowserTab()
.environmentObject(BrowserViewModel.getCached(tabID: tabID))
let controller = UIHostingController(rootView: view)
controller.navigationItem.scrollEdgeAppearance = {
let apperance = UINavigationBarAppearance()
Expand Down
10 changes: 5 additions & 5 deletions Model/Entities/Entities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,15 @@ class OutlineItem: ObservableObject, Identifiable {
}
}

class Tab: NSManagedObject, Identifiable {
final class Tab: NSManagedObject, Identifiable {
@NSManaged var created: Date
@NSManaged var interactionState: Data?
@NSManaged var lastOpened: Date
@NSManaged var title: String?

@NSManaged var zimFile: ZimFile?

class func fetchRequest(
static func fetchRequest(
predicate: NSPredicate? = nil, sortDescriptors: [NSSortDescriptor] = []
) -> NSFetchRequest<Tab> {
// swiftlint:disable:next force_cast
Expand All @@ -139,7 +139,7 @@ class Tab: NSManagedObject, Identifiable {
return request
}

class func fetchRequest(id: UUID) -> NSFetchRequest<Tab> {
static func fetchRequest(id: UUID) -> NSFetchRequest<Tab> {
// swiftlint:disable:next force_cast
let request = super.fetchRequest() as! NSFetchRequest<Tab>
request.predicate = NSPredicate(format: "id == %@", id as CVarArg)
Expand Down Expand Up @@ -265,7 +265,7 @@ final class ZimFile: NSManagedObject, Identifiable {
Predicate.notMissing
])

class func fetchRequest(
static func fetchRequest(
predicate: NSPredicate? = nil, sortDescriptors: [NSSortDescriptor] = []
) -> NSFetchRequest<ZimFile> {
// swiftlint:disable:next force_cast
Expand All @@ -275,7 +275,7 @@ final class ZimFile: NSManagedObject, Identifiable {
return request
}

class func fetchRequest(fileID: UUID) -> NSFetchRequest<ZimFile> {
static func fetchRequest(fileID: UUID) -> NSFetchRequest<ZimFile> {
// swiftlint:disable:next force_cast
let request = super.fetchRequest() as! NSFetchRequest<ZimFile>
request.predicate = NSPredicate(format: "fileID == %@", fileID as CVarArg)
Expand Down
4 changes: 4 additions & 0 deletions Model/Utilities/URL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ extension URL {
var isZIMURL: Bool { schemeType == .zim }
var isKiwixURL: Bool { schemeType == .kiwix }
var isGeoURL: Bool { schemeType == .geo }
var zimFileID: UUID? {
guard isZIMURL || isKiwixURL else { return nil }
return UUID(uuidString: host ?? "")
}

/// Returns the path, that should be used to resolve articles in ZIM files.
/// It makes sure that trailing slash is preserved,
Expand Down
Loading

0 comments on commit f76e171

Please sign in to comment.