mirror of
https://github.com/kiwix/kiwix-apple.git
synced 2025-08-03 04:27:00 -04:00
Refactor
This commit is contained in:
parent
6af7f7d568
commit
f58c2007a8
@ -16,8 +16,7 @@
|
||||
import Foundation
|
||||
import Defaults
|
||||
|
||||
@MainActor
|
||||
final class Hotspot: ObservableObject {
|
||||
final class Hotspot {
|
||||
|
||||
@MainActor
|
||||
static let shared = Hotspot()
|
||||
@ -26,36 +25,36 @@ final class Hotspot: ObservableObject {
|
||||
nonisolated static let defaultPort = 8080
|
||||
private static let maxPort = 9999
|
||||
|
||||
@ZimActor
|
||||
private var hotspot: KiwixHotspot?
|
||||
|
||||
@Published private(set) var isStarted: Bool = false
|
||||
@Published var selection = MultiSelectedZimFilesViewModel()
|
||||
@MainActor
|
||||
@Published var isStarted: Bool = false
|
||||
|
||||
@ZimActor
|
||||
func toggle() async {
|
||||
if let hotspot {
|
||||
hotspot.__stop()
|
||||
self.hotspot = nil
|
||||
await MainActor.run { self.isStarted = false }
|
||||
return
|
||||
} else {
|
||||
let zimFileIds: Set<UUID> = await MainActor.run(
|
||||
resultType: Set<UUID>.self,
|
||||
body: {
|
||||
Set(selection.selectedZimFiles.map { $0.fileID })
|
||||
})
|
||||
guard !zimFileIds.isEmpty else {
|
||||
debugPrint("no zim files were set for Hotspot to start")
|
||||
return
|
||||
}
|
||||
let portNumber = Int32(Defaults[.hotspotPortNumber])
|
||||
self.hotspot = KiwixHotspot(__zimFileIds: zimFileIds, onPort: portNumber)
|
||||
await MainActor.run {
|
||||
isStarted = true
|
||||
private var hotspot: KiwixHotspot? {
|
||||
didSet {
|
||||
let started = hotspot != nil
|
||||
Task { @MainActor [weak self] in
|
||||
self?.isStarted = started
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ZimActor
|
||||
func startWith(zimFileIds: Set<UUID>) async {
|
||||
guard hotspot == nil else { return }
|
||||
guard !zimFileIds.isEmpty else {
|
||||
debugPrint("no zim files were set for Hotspot to start")
|
||||
return
|
||||
}
|
||||
let portNumber = Int32(Defaults[.hotspotPortNumber])
|
||||
hotspot = KiwixHotspot(__zimFileIds: zimFileIds, onPort: portNumber)
|
||||
}
|
||||
|
||||
@ZimActor
|
||||
func stop() async {
|
||||
guard let hotspot else { return }
|
||||
hotspot.__stop()
|
||||
self.hotspot = nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func serverAddress() async -> URL? {
|
||||
@ -65,14 +64,14 @@ final class Hotspot: ObservableObject {
|
||||
return URL(string: address)
|
||||
}
|
||||
|
||||
static func isValid(port: Int) -> Bool {
|
||||
nonisolated static func isValid(port: Int) -> Bool {
|
||||
switch port {
|
||||
case minPort...maxPort: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
static var invalidPortMessage: String {
|
||||
nonisolated static var invalidPortMessage: String {
|
||||
LocalString.hotspot_settings_invalid_port_message(withArgs: "\(minPort)", "\(maxPort)")
|
||||
}
|
||||
}
|
||||
|
40
Views/Hotspot/HotspotAddress.swift
Normal file
40
Views/Hotspot/HotspotAddress.swift
Normal file
@ -0,0 +1,40 @@
|
||||
// This file is part of Kiwix for iOS & macOS.
|
||||
//
|
||||
// Kiwix is free software; you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kiwix is distributed in the hope that it will be useful, but
|
||||
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kiwix; If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct HotspotAddress: View {
|
||||
let serverAddress: URL
|
||||
let qrCodeImage: Image?
|
||||
|
||||
var body: some View {
|
||||
Section(LocalString.hotspot_server_running_title) {
|
||||
AttributeLink(title: LocalString.hotspot_server_running_address,
|
||||
destination: serverAddress)
|
||||
if let qrCodeImage {
|
||||
qrCodeImage
|
||||
.resizable()
|
||||
.frame(width: 250, height: 250)
|
||||
} else {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.frame(width: 250, height: 250)
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.collapsible(false)
|
||||
#endif
|
||||
}
|
||||
}
|
@ -18,17 +18,11 @@ import SwiftUI
|
||||
#if os(macOS)
|
||||
/// Hotspot multi ZIM files side panel
|
||||
struct HotspotDetails: View {
|
||||
let zimFiles: Set<ZimFile>
|
||||
@State private var serverAddress: URL?
|
||||
@State private var qrCodeImage: Image?
|
||||
@ObservedObject private var hotspot = Hotspot.shared
|
||||
|
||||
private var buttonTitle: String {
|
||||
hotspot.isStarted ? LocalString.hotspot_action_stop_server_title : LocalString.hotspot_action_start_server_title
|
||||
}
|
||||
let zimFileIds: Set<UUID>
|
||||
@ObservedObject var hotspot: HotspotObservable
|
||||
|
||||
private func zimFilesCount() -> String {
|
||||
Formatter.number.string(from: NSNumber(value: zimFiles.count)) ?? ""
|
||||
Formatter.number.string(from: NSNumber(value: zimFileIds.count)) ?? ""
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@ -41,52 +35,19 @@ struct HotspotDetails: View {
|
||||
}
|
||||
.collapsible(false)
|
||||
Section(LocalString.zim_file_list_actions_text) {
|
||||
Action(title: buttonTitle) {
|
||||
await hotspot.toggle()
|
||||
Action(title: hotspot.buttonTitle) {
|
||||
await hotspot.toggleWith(zimFileIds: zimFileIds)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
.collapsible(false)
|
||||
|
||||
if let serverAddress {
|
||||
Section(LocalString.hotspot_server_running_title) {
|
||||
AttributeLink(title: LocalString.hotspot_server_running_address,
|
||||
destination: serverAddress)
|
||||
if let qrCodeImage {
|
||||
qrCodeImage
|
||||
.resizable()
|
||||
.frame(width: 250, height: 250)
|
||||
} else {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.frame(width: 250, height: 250)
|
||||
}
|
||||
}
|
||||
.collapsible(false)
|
||||
if case .started(let address, let qrCodeImage) = hotspot.state {
|
||||
HotspotAddress(serverAddress: address, qrCodeImage: qrCodeImage)
|
||||
}
|
||||
Section {
|
||||
Text(LocalString.hotspot_server_explanation)
|
||||
.font(.subheadline)
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(nil)
|
||||
}.collapsible(false)
|
||||
HotspotExplanation()
|
||||
}
|
||||
.listStyle(.sidebar)
|
||||
.onReceive(hotspot.$isStarted) { isStarted in
|
||||
if isStarted {
|
||||
Task {
|
||||
serverAddress = await hotspot.serverAddress()
|
||||
if let serverAddress {
|
||||
qrCodeImage = await QRCode.image(from: serverAddress.absoluteString)
|
||||
} else {
|
||||
qrCodeImage = nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
serverAddress = nil
|
||||
qrCodeImage = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
30
Views/Hotspot/HotspotExplanation.swift
Normal file
30
Views/Hotspot/HotspotExplanation.swift
Normal file
@ -0,0 +1,30 @@
|
||||
// This file is part of Kiwix for iOS & macOS.
|
||||
//
|
||||
// Kiwix is free software; you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kiwix is distributed in the hope that it will be useful, but
|
||||
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kiwix; If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct HotspotExplanation: View {
|
||||
var body: some View {
|
||||
Section {
|
||||
Text(LocalString.hotspot_server_explanation)
|
||||
.font(.subheadline)
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(nil)
|
||||
}
|
||||
#if os(macOS)
|
||||
.collapsible(false)
|
||||
#endif
|
||||
}
|
||||
}
|
73
Views/Hotspot/HotspotObservable.swift
Normal file
73
Views/Hotspot/HotspotObservable.swift
Normal file
@ -0,0 +1,73 @@
|
||||
// This file is part of Kiwix for iOS & macOS.
|
||||
//
|
||||
// Kiwix is free software; you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kiwix is distributed in the hope that it will be useful, but
|
||||
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kiwix; If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
enum HotspotState {
|
||||
@MainActor static let selection = MultiSelectedZimFilesViewModel()
|
||||
|
||||
case stopped
|
||||
case started(URL, Image?)
|
||||
|
||||
var isStarted: Bool {
|
||||
switch self {
|
||||
case .stopped: return false
|
||||
case .started: return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class HotspotObservable: ObservableObject {
|
||||
|
||||
@Published var buttonTitle: String = LocalString.hotspot_action_start_server_title
|
||||
@Published var state: HotspotState = .stopped
|
||||
private var hotspot = Hotspot.shared
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init() {
|
||||
hotspot.$isStarted.sink { [weak self] isStarted in
|
||||
Task { [weak self] in
|
||||
await self?.update(isStarted: isStarted)
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func toggleWith(zimFileIds: Set<UUID>) async {
|
||||
if hotspot.isStarted {
|
||||
await hotspot.stop()
|
||||
} else {
|
||||
await hotspot.startWith(zimFileIds: zimFileIds)
|
||||
}
|
||||
}
|
||||
|
||||
private func update(isStarted: Bool) async {
|
||||
if isStarted {
|
||||
buttonTitle = LocalString.hotspot_action_stop_server_title
|
||||
let address = await hotspot.serverAddress()
|
||||
if let address {
|
||||
state = .started(address, nil)
|
||||
let qrCodeImage = await QRCode.image(from: address.absoluteString)
|
||||
state = .started(address, qrCodeImage)
|
||||
} else {
|
||||
state = .stopped
|
||||
}
|
||||
} else {
|
||||
buttonTitle = LocalString.hotspot_action_start_server_title
|
||||
state = .stopped
|
||||
}
|
||||
}
|
||||
}
|
@ -24,18 +24,14 @@ struct HotspotZimFilesSelection: View {
|
||||
predicate: ZimFile.openedPredicate,
|
||||
animation: .easeInOut
|
||||
) private var zimFiles: FetchedResults<ZimFile>
|
||||
@State private var isFileImporterPresented = false
|
||||
@ObservedObject private var hotspot: Hotspot
|
||||
@StateObject private var selection: MultiSelectedZimFilesViewModel
|
||||
#if os(iOS)
|
||||
@State private var serverAddress: URL?
|
||||
@State private var qrCodeImage: Image?
|
||||
#endif
|
||||
@ObservedObject private var hotspot = HotspotObservable()
|
||||
|
||||
init(hotspotProvider: @MainActor () -> Hotspot = { @MainActor in Hotspot.shared }) {
|
||||
let hotspotInstance = hotspotProvider()
|
||||
self.hotspot = hotspotInstance
|
||||
_selection = StateObject(wrappedValue: hotspotInstance.selection)
|
||||
init(
|
||||
selectionProvider: @MainActor () -> MultiSelectedZimFilesViewModel = { @MainActor in HotspotState.selection }
|
||||
) {
|
||||
let selectionInstance = selectionProvider()
|
||||
_selection = StateObject(wrappedValue: selectionInstance)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@ -61,7 +57,7 @@ struct HotspotZimFilesSelection: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
.disabled(hotspot.isStarted)
|
||||
.disabled(hotspot.state.isStarted)
|
||||
.modifier(GridCommon(edges: .all))
|
||||
.modifier(ToolbarRoleBrowser())
|
||||
.navigationTitle(MenuItem.hotspot.name)
|
||||
@ -74,61 +70,24 @@ struct HotspotZimFilesSelection: View {
|
||||
if zimFiles.isEmpty {
|
||||
Message(text: LocalString.zim_file_opened_overlay_no_opened_message)
|
||||
}
|
||||
if let serverAddress {
|
||||
if case .started(let address, let qrCodeImage) = hotspot.state {
|
||||
List {
|
||||
Section(LocalString.hotspot_server_running_title) {
|
||||
AttributeLink(title: LocalString.hotspot_server_running_address,
|
||||
destination: serverAddress)
|
||||
Section {
|
||||
if let qrCodeImage {
|
||||
qrCodeImage
|
||||
.resizable()
|
||||
.frame(width: 250, height: 250)
|
||||
} else {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.frame(width: 250, height: 250)
|
||||
}
|
||||
}
|
||||
}
|
||||
Section {
|
||||
Text(LocalString.hotspot_server_explanation)
|
||||
.font(.subheadline)
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(nil)
|
||||
}
|
||||
HotspotAddress(serverAddress: address, qrCodeImage: qrCodeImage)
|
||||
HotspotExplanation()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(hotspot.$isStarted) { isStarted in
|
||||
if isStarted {
|
||||
Task {
|
||||
serverAddress = await hotspot.serverAddress()
|
||||
if let serverAddress {
|
||||
qrCodeImage = await QRCode.image(from: serverAddress.absoluteString)
|
||||
} else {
|
||||
qrCodeImage = nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
serverAddress = nil
|
||||
qrCodeImage = nil
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
AsyncButton {
|
||||
await hotspot.toggle()
|
||||
await hotspot.toggleWith(
|
||||
zimFileIds: Set(selection.selectedZimFiles.map { $0.fileID })
|
||||
)
|
||||
} label: {
|
||||
let text = if hotspot.isStarted {
|
||||
LocalString.hotspot_action_stop_server_title
|
||||
} else {
|
||||
LocalString.hotspot_action_start_server_title
|
||||
}
|
||||
Text(text)
|
||||
Text(hotspot.buttonTitle)
|
||||
.bold()
|
||||
}
|
||||
.disabled(selection.selectedZimFiles.isEmpty && !hotspot.isStarted)
|
||||
.disabled(selection.selectedZimFiles.isEmpty && !hotspot.state.isStarted)
|
||||
.padding(.leading, 32)
|
||||
.modifier(BadgeModifier(count: selection.selectedZimFiles.count))
|
||||
}
|
||||
@ -148,7 +107,8 @@ struct HotspotZimFilesSelection: View {
|
||||
Message(text: LocalString.hotspot_zim_file_selection_message)
|
||||
.background(.thickMaterial)
|
||||
default:
|
||||
HotspotDetails(zimFiles: selection.selectedZimFiles)
|
||||
HotspotDetails(zimFileIds: Set(selection.selectedZimFiles.map { $0.fileID }),
|
||||
hotspot: hotspot)
|
||||
}
|
||||
}
|
||||
.frame(width: 275)
|
||||
|
Loading…
x
Reference in New Issue
Block a user