Add upload, preview sheet
This commit is contained in:
parent
0753d91f29
commit
2cad27b504
@ -172,9 +172,10 @@
|
|||||||
E2B482002D5D1136005C309D /* Vapor in Frameworks */ = {isa = PBXBuildFile; productRef = E2B481FF2D5D1136005C309D /* Vapor */; };
|
E2B482002D5D1136005C309D /* Vapor in Frameworks */ = {isa = PBXBuildFile; productRef = E2B481FF2D5D1136005C309D /* Vapor */; };
|
||||||
E2B482032D5D1331005C309D /* WebServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B482022D5D132D005C309D /* WebServer.swift */; };
|
E2B482032D5D1331005C309D /* WebServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B482022D5D132D005C309D /* WebServer.swift */; };
|
||||||
E2B482052D5E7D4A005C309D /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B482042D5E7D4A005C309D /* WebView.swift */; };
|
E2B482052D5E7D4A005C309D /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B482042D5E7D4A005C309D /* WebView.swift */; };
|
||||||
E2B482072D5E7DF4005C309D /* WebDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B482062D5E7DF0005C309D /* WebDetailView.swift */; };
|
E2B482092D5E7F4F005C309D /* WebsitePreviewSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B482082D5E7F4C005C309D /* WebsitePreviewSheet.swift */; };
|
||||||
E2B482092D5E7F4F005C309D /* WebContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B482082D5E7F4C005C309D /* WebContentView.swift */; };
|
|
||||||
E2B4820D2D5E811E005C309D /* TryFilesMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B4820C2D5E811E005C309D /* TryFilesMiddleware.swift */; };
|
E2B4820D2D5E811E005C309D /* TryFilesMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B4820C2D5E811E005C309D /* TryFilesMiddleware.swift */; };
|
||||||
|
E2B482102D5E9FF9005C309D /* RemotePush.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B4820F2D5E9FF5005C309D /* RemotePush.swift */; };
|
||||||
|
E2B482122D600AE0005C309D /* UploadSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B482112D600AD1005C309D /* UploadSheet.swift */; };
|
||||||
E2B85F362C426BEE0047CD0C /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E2B85F352C426BEE0047CD0C /* SFSafeSymbols */; };
|
E2B85F362C426BEE0047CD0C /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E2B85F352C426BEE0047CD0C /* SFSafeSymbols */; };
|
||||||
E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3A2C428F0D0047CD0C /* Post.swift */; };
|
E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3A2C428F0D0047CD0C /* Post.swift */; };
|
||||||
E2B85F3D2C4293F80047CD0C /* FeedPageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */; };
|
E2B85F3D2C4293F80047CD0C /* FeedPageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */; };
|
||||||
@ -433,9 +434,10 @@
|
|||||||
E2A9CB7D2C7BCF2A005C89CC /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = "<group>"; };
|
E2A9CB7D2C7BCF2A005C89CC /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = "<group>"; };
|
||||||
E2B482022D5D132D005C309D /* WebServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebServer.swift; sourceTree = "<group>"; };
|
E2B482022D5D132D005C309D /* WebServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebServer.swift; sourceTree = "<group>"; };
|
||||||
E2B482042D5E7D4A005C309D /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = "<group>"; };
|
E2B482042D5E7D4A005C309D /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = "<group>"; };
|
||||||
E2B482062D5E7DF0005C309D /* WebDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebDetailView.swift; sourceTree = "<group>"; };
|
E2B482082D5E7F4C005C309D /* WebsitePreviewSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsitePreviewSheet.swift; sourceTree = "<group>"; };
|
||||||
E2B482082D5E7F4C005C309D /* WebContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebContentView.swift; sourceTree = "<group>"; };
|
|
||||||
E2B4820C2D5E811E005C309D /* TryFilesMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TryFilesMiddleware.swift; sourceTree = "<group>"; };
|
E2B4820C2D5E811E005C309D /* TryFilesMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TryFilesMiddleware.swift; sourceTree = "<group>"; };
|
||||||
|
E2B4820F2D5E9FF5005C309D /* RemotePush.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemotePush.swift; sourceTree = "<group>"; };
|
||||||
|
E2B482112D600AD1005C309D /* UploadSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadSheet.swift; sourceTree = "<group>"; };
|
||||||
E2B85F3A2C428F0D0047CD0C /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = "<group>"; };
|
E2B85F3A2C428F0D0047CD0C /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = "<group>"; };
|
||||||
E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedPageGenerator.swift; sourceTree = "<group>"; };
|
E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedPageGenerator.swift; sourceTree = "<group>"; };
|
||||||
E2B85F402C4294790047CD0C /* PageHead.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageHead.swift; sourceTree = "<group>"; };
|
E2B85F402C4294790047CD0C /* PageHead.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageHead.swift; sourceTree = "<group>"; };
|
||||||
@ -853,14 +855,22 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E2B4820C2D5E811E005C309D /* TryFilesMiddleware.swift */,
|
E2B4820C2D5E811E005C309D /* TryFilesMiddleware.swift */,
|
||||||
E2B482082D5E7F4C005C309D /* WebContentView.swift */,
|
E2B482082D5E7F4C005C309D /* WebsitePreviewSheet.swift */,
|
||||||
E2B482062D5E7DF0005C309D /* WebDetailView.swift */,
|
|
||||||
E2B482042D5E7D4A005C309D /* WebView.swift */,
|
E2B482042D5E7D4A005C309D /* WebView.swift */,
|
||||||
E2B482022D5D132D005C309D /* WebServer.swift */,
|
E2B482022D5D132D005C309D /* WebServer.swift */,
|
||||||
);
|
);
|
||||||
path = Server;
|
path = Server;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
E2B4820E2D5E9FF0005C309D /* Push */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E2B482112D600AD1005C309D /* UploadSheet.swift */,
|
||||||
|
E2B4820F2D5E9FF5005C309D /* RemotePush.swift */,
|
||||||
|
);
|
||||||
|
path = Push;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
E2B85F392C428F020047CD0C /* Model */ = {
|
E2B85F392C428F020047CD0C /* Model */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -981,6 +991,7 @@
|
|||||||
E2DD04722C276F31003BFF1F /* CHDataManagement */ = {
|
E2DD04722C276F31003BFF1F /* CHDataManagement */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E2B4820E2D5E9FF0005C309D /* Push */,
|
||||||
E2B482012D5D1325005C309D /* Server */,
|
E2B482012D5D1325005C309D /* Server */,
|
||||||
E29D31372D043EB80051B7F4 /* Main */,
|
E29D31372D043EB80051B7F4 /* Main */,
|
||||||
E25DA5782D01C56200AEF16D /* Generator */,
|
E25DA5782D01C56200AEF16D /* Generator */,
|
||||||
@ -1245,7 +1256,6 @@
|
|||||||
E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */,
|
E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */,
|
||||||
E229904C2D10BE5D009F8D77 /* InitialSetupView.swift in Sources */,
|
E229904C2D10BE5D009F8D77 /* InitialSetupView.swift in Sources */,
|
||||||
E218502B2CF790B30090B18B /* PostContentView.swift in Sources */,
|
E218502B2CF790B30090B18B /* PostContentView.swift in Sources */,
|
||||||
E2B482072D5E7DF4005C309D /* WebDetailView.swift in Sources */,
|
|
||||||
E29D317D2D086AB00051B7F4 /* Int+Random.swift in Sources */,
|
E29D317D2D086AB00051B7F4 /* Int+Random.swift in Sources */,
|
||||||
E25DA56F2D00F9A100AEF16D /* PostFeedSettingsView.swift in Sources */,
|
E25DA56F2D00F9A100AEF16D /* PostFeedSettingsView.swift in Sources */,
|
||||||
E2521E042D51796000C56662 /* StorageErrorView.swift in Sources */,
|
E2521E042D51796000C56662 /* StorageErrorView.swift in Sources */,
|
||||||
@ -1333,7 +1343,7 @@
|
|||||||
E2B4820D2D5E811E005C309D /* TryFilesMiddleware.swift in Sources */,
|
E2B4820D2D5E811E005C309D /* TryFilesMiddleware.swift in Sources */,
|
||||||
E20BCC9F2D53851400B8DBEB /* SelectableListItem.swift in Sources */,
|
E20BCC9F2D53851400B8DBEB /* SelectableListItem.swift in Sources */,
|
||||||
E20BCCAB2D53B86900B8DBEB /* GenerationResultsIssueView.swift in Sources */,
|
E20BCCAB2D53B86900B8DBEB /* GenerationResultsIssueView.swift in Sources */,
|
||||||
E2B482092D5E7F4F005C309D /* WebContentView.swift in Sources */,
|
E2B482092D5E7F4F005C309D /* WebsitePreviewSheet.swift in Sources */,
|
||||||
E22990262D0F582B009F8D77 /* FilePropertyView.swift in Sources */,
|
E22990262D0F582B009F8D77 /* FilePropertyView.swift in Sources */,
|
||||||
E2FD1D462D46428100B48627 /* PageIconView.swift in Sources */,
|
E2FD1D462D46428100B48627 /* PageIconView.swift in Sources */,
|
||||||
E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */,
|
E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */,
|
||||||
@ -1428,6 +1438,7 @@
|
|||||||
E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */,
|
E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */,
|
||||||
E2FE0F2C2D2B119A002963B7 /* ImageCommand.swift in Sources */,
|
E2FE0F2C2D2B119A002963B7 /* ImageCommand.swift in Sources */,
|
||||||
E2FE0F112D268E7E002963B7 /* MarkdownCodeProcessor.swift in Sources */,
|
E2FE0F112D268E7E002963B7 /* MarkdownCodeProcessor.swift in Sources */,
|
||||||
|
E2B482102D5E9FF9005C309D /* RemotePush.swift in Sources */,
|
||||||
E22990202D0ECBE5009F8D77 /* TagOverviewDetailView.swift in Sources */,
|
E22990202D0ECBE5009F8D77 /* TagOverviewDetailView.swift in Sources */,
|
||||||
E29D31C02D0DB9F20051B7F4 /* AudioPlayerContent.swift in Sources */,
|
E29D31C02D0DB9F20051B7F4 /* AudioPlayerContent.swift in Sources */,
|
||||||
E22990192D0E3546009F8D77 /* ItemReference.swift in Sources */,
|
E22990192D0E3546009F8D77 /* ItemReference.swift in Sources */,
|
||||||
@ -1442,6 +1453,7 @@
|
|||||||
E2A37D0E2CE527070000979F /* Storage.swift in Sources */,
|
E2A37D0E2CE527070000979F /* Storage.swift in Sources */,
|
||||||
E2E06E002CA4A8F00019C2AF /* Page+Mock.swift in Sources */,
|
E2E06E002CA4A8F00019C2AF /* Page+Mock.swift in Sources */,
|
||||||
E29D314D2D04FCBF0051B7F4 /* FileToAddView.swift in Sources */,
|
E29D314D2D04FCBF0051B7F4 /* FileToAddView.swift in Sources */,
|
||||||
|
E2B482122D600AE0005C309D /* UploadSheet.swift in Sources */,
|
||||||
E2FE0F6C2D2D335E002963B7 /* LocalizedPageSettingsView.swift in Sources */,
|
E2FE0F6C2D2D335E002963B7 /* LocalizedPageSettingsView.swift in Sources */,
|
||||||
E2B85F572C4BD0BB0047CD0C /* Binding+Extension.swift in Sources */,
|
E2B85F572C4BD0BB0047CD0C /* Binding+Extension.swift in Sources */,
|
||||||
E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */,
|
E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */,
|
||||||
|
@ -17,10 +17,29 @@ extension String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var withLeadingSlashRemoved: String {
|
var withLeadingSlashRemoved: String {
|
||||||
if !hasPrefix("/") {
|
hasPrefix("/") ? String(dropFirst("/".count)) : self
|
||||||
return self
|
}
|
||||||
|
|
||||||
|
var withLeadingSlash: String {
|
||||||
|
hasPrefix("/") ? self : "/" + self
|
||||||
|
}
|
||||||
|
|
||||||
|
var withTrailingSlashRemoved: String {
|
||||||
|
hasSuffix("/") ? String(dropLast("/".count)) : self
|
||||||
|
}
|
||||||
|
|
||||||
|
var withTrailingSlash: String {
|
||||||
|
hasSuffix("/") ? self : self + "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
var withHttpPrefixRemoved: String {
|
||||||
|
if hasPrefix("https://") {
|
||||||
|
return String(dropFirst("https://".count))
|
||||||
}
|
}
|
||||||
return String(dropFirst("/".count))
|
if hasPrefix("http://") {
|
||||||
|
return String(dropFirst("http://".count))
|
||||||
|
}
|
||||||
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
var removingSurroundingQuotes: String {
|
var removingSurroundingQuotes: String {
|
||||||
|
@ -39,6 +39,9 @@ struct MainView: App {
|
|||||||
@StateObject
|
@StateObject
|
||||||
private var content: Content = .init()
|
private var content: Content = .init()
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
private var upload: RemotePush = .init()
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var language: ContentLanguage = .english
|
private var language: ContentLanguage = .english
|
||||||
|
|
||||||
@ -60,6 +63,12 @@ struct MainView: App {
|
|||||||
@State
|
@State
|
||||||
private var showGenerationSheet = false
|
private var showGenerationSheet = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var showPreviewSheet = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var showUploadSheet = false
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var sidebar: some View {
|
var sidebar: some View {
|
||||||
switch selection.tab {
|
switch selection.tab {
|
||||||
@ -67,7 +76,6 @@ struct MainView: App {
|
|||||||
case .pages: PageListView()
|
case .pages: PageListView()
|
||||||
case .tags: TagListView()
|
case .tags: TagListView()
|
||||||
case .files: FileListView(selectedFile: $selection.file)
|
case .files: FileListView(selectedFile: $selection.file)
|
||||||
case .browser: EmptyView()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,9 +90,6 @@ struct MainView: App {
|
|||||||
SelectedContentView<TagContentView>(selected: $selection.tag)
|
SelectedContentView<TagContentView>(selected: $selection.tag)
|
||||||
case .files:
|
case .files:
|
||||||
SelectedContentView<FileContentView>(selected: $selection.file)
|
SelectedContentView<FileContentView>(selected: $selection.file)
|
||||||
case .browser:
|
|
||||||
WebContentView()
|
|
||||||
.environmentObject(server)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,16 +104,13 @@ struct MainView: App {
|
|||||||
SelectedDetailView<TagDetailView>(selected: $selection.tag)
|
SelectedDetailView<TagDetailView>(selected: $selection.tag)
|
||||||
case .files:
|
case .files:
|
||||||
SelectedDetailView<FileDetailView>(selected: $selection.file)
|
SelectedDetailView<FileDetailView>(selected: $selection.file)
|
||||||
case .browser:
|
|
||||||
WebDetailView()
|
|
||||||
.environmentObject(server)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var addItemSheet: some View {
|
var addItemSheet: some View {
|
||||||
switch selection.tab {
|
switch selection.tab {
|
||||||
case .posts, .browser:
|
case .posts:
|
||||||
AddPostView(selected: $selection.post)
|
AddPostView(selected: $selection.post)
|
||||||
case .pages:
|
case .pages:
|
||||||
AddPageView(selected: $selection.page)
|
AddPageView(selected: $selection.page)
|
||||||
@ -141,7 +143,6 @@ struct MainView: App {
|
|||||||
Text("Pages").tag(MainViewTab.pages)
|
Text("Pages").tag(MainViewTab.pages)
|
||||||
Text("Tags").tag(MainViewTab.tags)
|
Text("Tags").tag(MainViewTab.tags)
|
||||||
Text("Files").tag(MainViewTab.files)
|
Text("Files").tag(MainViewTab.files)
|
||||||
Text("Preview").tag(MainViewTab.browser)
|
|
||||||
}.pickerStyle(.segmented)
|
}.pickerStyle(.segmented)
|
||||||
}.frame(minWidth: 400)
|
}.frame(minWidth: 400)
|
||||||
}
|
}
|
||||||
@ -179,8 +180,13 @@ struct MainView: App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ToolbarItem {
|
ToolbarItem {
|
||||||
Button(action: toggleWebServer) {
|
Button(action: { showPreviewSheet = true }) {
|
||||||
Image(systemSymbol: server.isRunning ? .eye : .eyeSlash)
|
Image(systemSymbol: .eye)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolbarItem {
|
||||||
|
Button(action: { showUploadSheet = true }) {
|
||||||
|
Image(systemSymbol: .squareAndArrowUp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ToolbarItem {
|
ToolbarItem {
|
||||||
@ -220,6 +226,16 @@ struct MainView: App {
|
|||||||
GenerationContentView()
|
GenerationContentView()
|
||||||
.environmentObject(content)
|
.environmentObject(content)
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $showPreviewSheet) {
|
||||||
|
WebsitePreviewSheet()
|
||||||
|
.environmentObject(content)
|
||||||
|
.environmentObject(server)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showUploadSheet) {
|
||||||
|
UploadSheet()
|
||||||
|
.environmentObject(content)
|
||||||
|
.environmentObject(upload)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,18 +283,5 @@ struct MainView: App {
|
|||||||
showInitialSetupSheet = true
|
showInitialSetupSheet = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func toggleWebServer() {
|
|
||||||
guard !server.isRunning else {
|
|
||||||
server.stopServer()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let folder = content.storage.outputScope?.url.path() else {
|
|
||||||
print("No output folder to start server")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
server.startServer(in: folder)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,5 @@ enum MainViewTab {
|
|||||||
case pages
|
case pages
|
||||||
case tags
|
case tags
|
||||||
case files
|
case files
|
||||||
case browser
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +63,10 @@ final class Content: ObservableObject {
|
|||||||
storage: storage,
|
storage: storage,
|
||||||
settings: settings)
|
settings: settings)
|
||||||
storage.errorNotification = { [weak self] error in
|
storage.errorNotification = { [weak self] error in
|
||||||
self?.storageErrors.append(error)
|
guard let self else { return }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.storageErrors.append(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
settings.content = self
|
settings.content = self
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,22 @@ final class GeneralSettings: ObservableObject {
|
|||||||
@Published
|
@Published
|
||||||
var linkPreviewImageHeight: Int
|
var linkPreviewImageHeight: Int
|
||||||
|
|
||||||
init(url: String, linkPreviewImageWidth: Int, linkPreviewImageHeight: Int) {
|
@Published
|
||||||
|
var remoteUserForUpload: String
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var remotePortForUpload: Int
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var remotePathForUpload: String
|
||||||
|
|
||||||
|
init(url: String, linkPreviewImageWidth: Int, linkPreviewImageHeight: Int, remoteUserForUpload: String, remotePortForUpload: Int, remotePathForUpload: String) {
|
||||||
self.url = url
|
self.url = url
|
||||||
self.linkPreviewImageWidth = linkPreviewImageWidth
|
self.linkPreviewImageWidth = linkPreviewImageWidth
|
||||||
self.linkPreviewImageHeight = linkPreviewImageHeight
|
self.linkPreviewImageHeight = linkPreviewImageHeight
|
||||||
|
self.remoteUserForUpload = remoteUserForUpload
|
||||||
|
self.remotePortForUpload = remotePortForUpload
|
||||||
|
self.remotePathForUpload = remotePathForUpload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,19 +36,28 @@ extension GeneralSettings {
|
|||||||
self.init(
|
self.init(
|
||||||
url: data.url,
|
url: data.url,
|
||||||
linkPreviewImageWidth: data.linkPreviewImageWidth,
|
linkPreviewImageWidth: data.linkPreviewImageWidth,
|
||||||
linkPreviewImageHeight: data.linkPreviewImageHeight)
|
linkPreviewImageHeight: data.linkPreviewImageHeight,
|
||||||
|
remoteUserForUpload: data.remoteUserForUpload,
|
||||||
|
remotePortForUpload: data.remotePortForUpload,
|
||||||
|
remotePathForUpload: data.remotePathForUpload)
|
||||||
}
|
}
|
||||||
|
|
||||||
var data: Data {
|
var data: Data {
|
||||||
.init(
|
.init(
|
||||||
url: url,
|
url: url,
|
||||||
linkPreviewImageWidth: linkPreviewImageWidth,
|
linkPreviewImageWidth: linkPreviewImageWidth,
|
||||||
linkPreviewImageHeight: linkPreviewImageHeight)
|
linkPreviewImageHeight: linkPreviewImageHeight,
|
||||||
|
remoteUserForUpload: remoteUserForUpload,
|
||||||
|
remotePortForUpload: remotePortForUpload,
|
||||||
|
remotePathForUpload: remotePathForUpload)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Data: Codable, Equatable {
|
struct Data: Codable, Equatable {
|
||||||
let url: String
|
let url: String
|
||||||
let linkPreviewImageWidth: Int
|
let linkPreviewImageWidth: Int
|
||||||
let linkPreviewImageHeight: Int
|
let linkPreviewImageHeight: Int
|
||||||
|
let remoteUserForUpload: String
|
||||||
|
let remotePortForUpload: Int
|
||||||
|
let remotePathForUpload: String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,10 @@ extension GeneralSettings {
|
|||||||
static let `default`: GeneralSettings = .init(
|
static let `default`: GeneralSettings = .init(
|
||||||
url: "https://example.com",
|
url: "https://example.com",
|
||||||
linkPreviewImageWidth: 1200,
|
linkPreviewImageWidth: 1200,
|
||||||
linkPreviewImageHeight: 630)
|
linkPreviewImageHeight: 630,
|
||||||
|
remoteUserForUpload: "user",
|
||||||
|
remotePortForUpload: 22,
|
||||||
|
remotePathForUpload: "/home/user/web")
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AudioPlayerSettings {
|
extension AudioPlayerSettings {
|
||||||
|
85
CHDataManagement/Push/RemotePush.swift
Normal file
85
CHDataManagement/Push/RemotePush.swift
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class RemotePush: ObservableObject {
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var isTransmittingToRemote = false
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var lastPushWasSuccessful = true
|
||||||
|
|
||||||
|
func transmitToRemote(settings: GeneralSettings, outputFolder: String, outputHandler: @escaping (String) -> Void) {
|
||||||
|
guard !isTransmittingToRemote else { return }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isTransmittingToRemote = true
|
||||||
|
}
|
||||||
|
DispatchQueue.global().async {
|
||||||
|
let success = self.transmit(
|
||||||
|
outputFolder: outputFolder,
|
||||||
|
remoteUser: settings.remoteUserForUpload,
|
||||||
|
remotePort: settings.remotePortForUpload,
|
||||||
|
remoteDomain: settings.url,
|
||||||
|
remotePath: settings.remotePathForUpload,
|
||||||
|
excludedItems: [".git"],
|
||||||
|
outputHandler: outputHandler)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isTransmittingToRemote = false
|
||||||
|
self.lastPushWasSuccessful = success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func transmit(
|
||||||
|
outputFolder: String,
|
||||||
|
remoteUser: String,
|
||||||
|
remotePort: Int,
|
||||||
|
remoteDomain: String,
|
||||||
|
remotePath: String,
|
||||||
|
excludedItems: [String],
|
||||||
|
outputHandler: @escaping (String) -> Void
|
||||||
|
) -> Bool {
|
||||||
|
let remoteDomain = remoteDomain.withHttpPrefixRemoved
|
||||||
|
let remotePath = remotePath.withLeadingSlash.withTrailingSlash
|
||||||
|
let outputFolder = outputFolder.withLeadingSlash.withTrailingSlash
|
||||||
|
|
||||||
|
let process = Process()
|
||||||
|
process.executableURL = URL(fileURLWithPath: "/bin/bash")
|
||||||
|
let arguments = [
|
||||||
|
"/opt/homebrew/bin/rsync",
|
||||||
|
"-hrutv",
|
||||||
|
"--info=progress2"]
|
||||||
|
+ excludedItems.reduce(into: []) { $0 += ["--exclude", $1] }
|
||||||
|
+ [
|
||||||
|
"-e", "\"/opt/homebrew/bin/ssh -p \(remotePort)\"",
|
||||||
|
outputFolder,
|
||||||
|
"\(remoteUser)@\(remoteDomain):\(remotePath)"
|
||||||
|
]
|
||||||
|
|
||||||
|
let argument = arguments.joined(separator: " ")
|
||||||
|
|
||||||
|
process.arguments = ["-c", argument]
|
||||||
|
|
||||||
|
print(argument)
|
||||||
|
|
||||||
|
let pipe = Pipe()
|
||||||
|
process.standardOutput = pipe
|
||||||
|
process.standardError = pipe
|
||||||
|
|
||||||
|
let fileHandle = pipe.fileHandleForReading
|
||||||
|
|
||||||
|
// Use a DispatchQueue to read output asynchronously
|
||||||
|
fileHandle.readabilityHandler = { fileHandle in
|
||||||
|
if let output = String(data: fileHandle.availableData, encoding: .utf8), !output.isEmpty {
|
||||||
|
outputHandler(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.launch()
|
||||||
|
process.waitUntilExit()
|
||||||
|
|
||||||
|
if process.terminationStatus == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
71
CHDataManagement/Push/UploadSheet.swift
Normal file
71
CHDataManagement/Push/UploadSheet.swift
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
struct UploadSheet: View {
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var content: Content
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var upload: RemotePush
|
||||||
|
|
||||||
|
@Environment(\.dismiss)
|
||||||
|
private var dismiss
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var output = ""
|
||||||
|
|
||||||
|
private var uploadSymbol: SFSymbol {
|
||||||
|
if upload.isTransmittingToRemote {
|
||||||
|
return .squareAndArrowUpBadgeClock
|
||||||
|
}
|
||||||
|
if !upload.lastPushWasSuccessful {
|
||||||
|
return .squareAndArrowUpTrianglebadgeExclamationmark
|
||||||
|
}
|
||||||
|
return .squareAndArrowUp
|
||||||
|
}
|
||||||
|
|
||||||
|
var header: String {
|
||||||
|
let user = content.settings.general.remoteUserForUpload
|
||||||
|
let port = content.settings.general.remotePortForUpload
|
||||||
|
let url = content.settings.general.remotePathForUpload.withHttpPrefixRemoved
|
||||||
|
let path = content.settings.general.remotePathForUpload
|
||||||
|
return "\(user)@\(url):\(port)/\(path)"
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Button("Upload", action: startUpload)
|
||||||
|
.disabled(upload.isTransmittingToRemote)
|
||||||
|
Text(header)
|
||||||
|
Spacer()
|
||||||
|
Button("Close", action: { dismiss() })
|
||||||
|
}
|
||||||
|
ScrollView {
|
||||||
|
Text(output)
|
||||||
|
.font(.body.monospaced())
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.frame(minWidth: 500, idealWidth: 600, idealHeight: 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startUpload() {
|
||||||
|
guard let folder = content.storage.outputScope?.url.path() else {
|
||||||
|
print("No output folder to start upload")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
output = ""
|
||||||
|
|
||||||
|
upload.transmitToRemote(
|
||||||
|
settings: content.settings.general,
|
||||||
|
outputFolder: folder) { newContent in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.output += newContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,20 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct WebContentView: View {
|
|
||||||
|
|
||||||
@EnvironmentObject
|
|
||||||
private var server: WebServer
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
if server.isRunning {
|
|
||||||
WebView(viewModel: server)
|
|
||||||
} else {
|
|
||||||
VStack {
|
|
||||||
Text("Webserver disabled")
|
|
||||||
.font(.title)
|
|
||||||
Text("Enable it to check out the generated content")
|
|
||||||
}
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
import SFSafeSymbols
|
|
||||||
|
|
||||||
struct WebDetailView: View {
|
|
||||||
|
|
||||||
@EnvironmentObject
|
|
||||||
private var content: Content
|
|
||||||
|
|
||||||
@EnvironmentObject
|
|
||||||
private var server: WebServer
|
|
||||||
|
|
||||||
var text: String {
|
|
||||||
server.isRunning ? "Stop" : "Start"
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
TextField("", text: $server.currentUrl)
|
|
||||||
.disabled(true)
|
|
||||||
.textFieldStyle(.roundedBorder)
|
|
||||||
HStack {
|
|
||||||
Button(text, action: toggleWebServer)
|
|
||||||
.disabled(!server.isRunning && content.storage.outputScope == nil)
|
|
||||||
Button(action: { server.reloadPage() }) {
|
|
||||||
Label("Reload", systemSymbol: .arrowClockwise)
|
|
||||||
}
|
|
||||||
.disabled(!server.isRunning)
|
|
||||||
Button(action: { server.loadHomeUrl() }) {
|
|
||||||
Label("Home", systemSymbol: .house)
|
|
||||||
}
|
|
||||||
.disabled(!server.isRunning)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func toggleWebServer() {
|
|
||||||
guard !server.isRunning else {
|
|
||||||
server.stopServer()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let folder = content.storage.outputScope?.url.path() else {
|
|
||||||
print("No output folder to start server")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
server.startServer(in: folder)
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,6 +10,9 @@ final class WebServer: NSObject, ObservableObject, WKNavigationDelegate {
|
|||||||
@Published
|
@Published
|
||||||
var isRunning = false
|
var isRunning = false
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var isStarting = false
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
var port: Int
|
var port: Int
|
||||||
|
|
||||||
@ -19,6 +22,10 @@ final class WebServer: NSObject, ObservableObject, WKNavigationDelegate {
|
|||||||
@Published
|
@Published
|
||||||
var currentUrl: String = ""
|
var currentUrl: String = ""
|
||||||
|
|
||||||
|
var isNotReady: Bool {
|
||||||
|
isStarting || !isRunning
|
||||||
|
}
|
||||||
|
|
||||||
init(port: Int) {
|
init(port: Int) {
|
||||||
self.port = port
|
self.port = port
|
||||||
super.init()
|
super.init()
|
||||||
@ -40,6 +47,9 @@ final class WebServer: NSObject, ObservableObject, WKNavigationDelegate {
|
|||||||
print("WebServer: Already running")
|
print("WebServer: Already running")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
guard !isStarting else { return }
|
||||||
|
self.isStarting = true
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
var vaporArgs = CommandLine.arguments
|
var vaporArgs = CommandLine.arguments
|
||||||
let allowedCommands = ["serve", "routes"]
|
let allowedCommands = ["serve", "routes"]
|
||||||
@ -52,11 +62,10 @@ final class WebServer: NSObject, ObservableObject, WKNavigationDelegate {
|
|||||||
app.middleware.use(TryFilesMiddleware(publicDirectory: directory))
|
app.middleware.use(TryFilesMiddleware(publicDirectory: directory))
|
||||||
app.http.server.configuration.port = 8000
|
app.http.server.configuration.port = 8000
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.isRunning = true
|
|
||||||
}
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) {
|
||||||
self.loadHomeUrl()
|
self.loadHomeUrl()
|
||||||
|
self.isStarting = false
|
||||||
|
self.isRunning = true
|
||||||
}
|
}
|
||||||
print("WebServer: Starting")
|
print("WebServer: Starting")
|
||||||
try await app.execute()
|
try await app.execute()
|
||||||
|
74
CHDataManagement/Server/WebsitePreviewSheet.swift
Normal file
74
CHDataManagement/Server/WebsitePreviewSheet.swift
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct WebsitePreviewSheet: View {
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var content: Content
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var server: WebServer
|
||||||
|
|
||||||
|
@Environment(\.dismiss)
|
||||||
|
private var dismiss
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Button(action: { server.loadHomeUrl() }) {
|
||||||
|
Image(systemSymbol: .house)
|
||||||
|
}
|
||||||
|
.disabled(server.isNotReady)
|
||||||
|
Button(action: { server.reloadPage() }) {
|
||||||
|
Image(systemSymbol: .arrowClockwise)
|
||||||
|
}
|
||||||
|
.disabled(server.isNotReady)
|
||||||
|
TextField("", text: $server.currentUrl)
|
||||||
|
.disabled(true)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
Spacer()
|
||||||
|
Button("Close", action: dismissSheet)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
if server.isRunning {
|
||||||
|
WebView(viewModel: server)
|
||||||
|
} else if server.isStarting {
|
||||||
|
Spacer()
|
||||||
|
ProgressView()
|
||||||
|
Text("Loading preview...")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Spacer()
|
||||||
|
} else {
|
||||||
|
Spacer()
|
||||||
|
Text("Webserver disabled")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Text("Enable it to check out the generated content")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(minWidth: 500, idealWidth: 600, idealHeight: 600)
|
||||||
|
.onAppear(perform: startWebServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func dismissSheet() {
|
||||||
|
if server.isRunning {
|
||||||
|
server.stopServer()
|
||||||
|
}
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startWebServer() {
|
||||||
|
guard !server.isRunning else {
|
||||||
|
server.stopServer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let folder = content.storage.outputScope?.url.path() else {
|
||||||
|
print("No output folder to start server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
server.startServer(in: folder)
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,21 @@ struct GeneralSettingsDetailView: View {
|
|||||||
title: "Link Preview Image Height",
|
title: "Link Preview Image Height",
|
||||||
value: $generalSettings.linkPreviewImageHeight,
|
value: $generalSettings.linkPreviewImageHeight,
|
||||||
footer: "The maximum height of a link preview image")
|
footer: "The maximum height of a link preview image")
|
||||||
|
|
||||||
|
StringPropertyView(
|
||||||
|
title: "Upload User",
|
||||||
|
text: $generalSettings.remoteUserForUpload,
|
||||||
|
footer: "The user on the server to connect via ssh for upload")
|
||||||
|
|
||||||
|
IntegerPropertyView(
|
||||||
|
title: "Upload Port",
|
||||||
|
value: $generalSettings.remotePortForUpload,
|
||||||
|
footer: "The port on the server to rsync the generated website")
|
||||||
|
|
||||||
|
StringPropertyView(
|
||||||
|
title: "Upload Folder",
|
||||||
|
text: $generalSettings.remotePathForUpload,
|
||||||
|
footer: "The path to the folder on the server where the files should be uploaded to")
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user