Commit fcb63f5b7b
Verified · cmc
Layout: unified · split
Package.swift +3
| @@ -18,6 +18,7 @@ let package = Package( | ||
| 18 | 18 | .library(name: "OrgApp", targets: ["OrgApp"]), |
| 19 | 19 | .library(name: "OrgHighlight", targets: ["OrgHighlight"]), |
| 20 | 20 | .executable(name: "Orgstar", targets: ["Orgstar"]), |
| 21 | .executable(name: "OrgstarMobile", targets: ["OrgstarMobile"]), | |
| 21 | 22 | .executable(name: "OrgstarQuickLook", targets: ["OrgstarQuickLook"]), |
| 22 | 23 | .executable(name: "OrgstarSpotlight", targets: ["OrgstarSpotlight"]) |
| 23 | 24 | ], |
| @@ -80,6 +81,8 @@ let package = Package( | ||
| 80 | 81 | .executableTarget(name: "OrgstarSpike", dependencies: ["OrgEditorAppKit"]), |
| 81 | 82 | .target(name: "OrgApp", dependencies: ["OrgWorkspace", "OrgDocument", "OrgPresentation"]), |
| 82 | 83 | .executableTarget(name: "Orgstar", dependencies: ["OrgApp", "OrgEditorAppKit", "OrgHighlight"]), |
| 84 | // The iOS app, bundled by scripts/build-ios-app.sh. | |
| 85 | .executableTarget(name: "OrgstarMobile", dependencies: ["OrgApp"]), | |
| 83 | 86 | // App extensions, bundled into Orgstar.app by scripts/build-app.sh. |
| 84 | 87 | .executableTarget(name: "OrgstarQuickLook", dependencies: ["OrgCore"], linkerSettings: extensionEntry), |
| 85 | 88 | .executableTarget(name: "OrgstarSpotlight", dependencies: ["OrgCore"], linkerSettings: extensionEntry), |
README.md +10 −3
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | # Orgstar |
| 2 | 2 | |
| 3 | A native macOS editor for org-mode files. iOS later. | |
| 3 | A native macOS editor for org-mode files, with an iOS app for the agenda and capture. | |
| 4 | 4 | |
| 5 | 5 | The files are the source of truth: Orgstar edits them in place and leaves |
| 6 | 6 | every byte you didn't change as it was, so Emacs and sync tools can work on |
| @@ -8,8 +8,8 @@ the same folder. | ||
| 8 | 8 | |
| 9 | 9 | Status: macOS v1, design phases 1–6: editing with Emacs, Mac and Doom keys, |
| 10 | 10 | agenda, table formulas, Babel, export, capture, clocking, board, themes and |
| 11 | buffers. Owed: the M1 Air gate run and the VoiceOver check. iOS (phase 7) is | |
| 12 | not started. | |
| 11 | buffers. Owed: the M1 Air gate run and the VoiceOver check. iOS (phase 7): | |
| 12 | agenda and capture, Simulator builds only so far. | |
| 13 | 13 | |
| 14 | 14 | ## Build and run |
| 15 | 15 | |
| @@ -30,6 +30,13 @@ Finder once the app has been launched. | ||
| 30 | 30 | bookmarklets written for org-protocol work unchanged. Shortcuts has a |
| 31 | 31 | "Capture to Orgstar" action that files text with a capture template. |
| 32 | 32 | |
| 33 | The iOS app (agenda and capture, iOS 26) builds for the Simulator: | |
| 34 | ||
| 35 | ```sh | |
| 36 | scripts/build-ios-app.sh # builds .build/ios-app/Orgstar.app | |
| 37 | xcrun simctl install booted .build/ios-app/Orgstar.app | |
| 38 | ``` | |
| 39 | ||
| 33 | 40 | Settings live in `~/.config/orgstar/config.toml` (Orgstar ▸ Edit Config File), |
| 34 | 41 | with keys in `keymap.toml` and capture templates in `capture.toml` beside it. |
| 35 | 42 | Settings ▸ General ▸ Import from Emacs reads an existing Emacs or Doom |
Sources/OrgstarMobile/AgendaScreen.swift added +136
| @@ -0,0 +1,136 @@ | ||
| 1 | #if os(iOS) | |
| 2 | import OrgApp | |
| 3 | import OrgCore | |
| 4 | import SwiftUI | |
| 5 | ||
| 6 | /// The day/week agenda and the TODO list over the workspace's agenda files. | |
| 7 | struct AgendaScreen: View { | |
| 8 | let workspace: WorkspaceModel | |
| 9 | let session: DocumentSession | |
| 10 | @Bindable var agenda: AgendaModel | |
| 11 | @State private var message: String? | |
| 12 | @State private var tick = 0 | |
| 13 | ||
| 14 | private enum View: String, CaseIterable { case agenda = "Agenda", todo = "TODO" } | |
| 15 | ||
| 16 | private var view: Binding<View> { | |
| 17 | Binding { | |
| 18 | if case .todo = agenda.mode { .todo } else { .agenda } | |
| 19 | } set: { value in | |
| 20 | agenda.mode = value == .todo ? .todo(keywords: nil) : .agenda(span: nil, startOffset: nil) | |
| 21 | } | |
| 22 | } | |
| 23 | ||
| 24 | private struct Key: Equatable { | |
| 25 | var files: Int | |
| 26 | var changed: Int? | |
| 27 | var edits: Int | |
| 28 | var tick: Int | |
| 29 | var view: String | |
| 30 | var start: Int? | |
| 31 | } | |
| 32 | ||
| 33 | var body: some SwiftUI.View { | |
| 34 | List { | |
| 35 | if case .agenda = agenda.mode { | |
| 36 | ForEach(agenda.days, id: \.day) { day in | |
| 37 | Section { | |
| 38 | let items = day.items.filter(\.isEntry) | |
| 39 | ForEach(items.indices, id: \.self) { i in row(items[i]) } | |
| 40 | } header: { | |
| 41 | Text(day.header.split(separator: " ", omittingEmptySubsequences: true).prefix(4).joined(separator: " ")) | |
| 42 | .foregroundStyle(day.isToday ? Color.accentColor : .secondary) | |
| 43 | } | |
| 44 | } | |
| 45 | } else { | |
| 46 | ForEach(agenda.list.indices, id: \.self) { i in row(agenda.list[i]) } | |
| 47 | } | |
| 48 | } | |
| 49 | .listStyle(.insetGrouped) | |
| 50 | .navigationTitle(agenda.isComputing && agenda.days.isEmpty ? "Agenda…" : "Agenda") | |
| 51 | .overlay { | |
| 52 | if workspace.rootPaths.isEmpty { | |
| 53 | ContentUnavailableView("No folders", systemImage: "folder", description: Text("Add a folder of org files under Folders.")) | |
| 54 | } | |
| 55 | } | |
| 56 | .safeAreaInset(edge: .bottom) { | |
| 57 | if let message { | |
| 58 | Text(message).font(.callout).padding(8).frame(maxWidth: .infinity).background(.bar) | |
| 59 | } | |
| 60 | } | |
| 61 | .toolbar { | |
| 62 | ToolbarItem(placement: .principal) { | |
| 63 | Picker("View", selection: view) { | |
| 64 | ForEach(View.allCases, id: \.self) { Text($0.rawValue) } | |
| 65 | } | |
| 66 | .pickerStyle(.segmented) | |
| 67 | .fixedSize() | |
| 68 | } | |
| 69 | if case .agenda = agenda.mode { | |
| 70 | ToolbarItem(placement: .topBarLeading) { | |
| 71 | Menu("Days", systemImage: "calendar") { | |
| 72 | Button("Today", systemImage: "calendar.circle") { agenda.showToday() } | |
| 73 | Button("Earlier", systemImage: "chevron.left") { agenda.shift(forward: false) } | |
| 74 | Button("Later", systemImage: "chevron.right") { agenda.shift(forward: true) } | |
| 75 | } | |
| 76 | } | |
| 77 | } | |
| 78 | } | |
| 79 | .refreshable { | |
| 80 | await workspace.refresh() | |
| 81 | await agenda.refresh(open: session.openTexts) | |
| 82 | } | |
| 83 | .task(id: Key(files: workspace.files.count, changed: workspace.changedFile?.version, edits: session.editVersion, tick: tick, | |
| 84 | view: view.wrappedValue.rawValue, start: agenda.start)) { | |
| 85 | await agenda.refresh(open: session.openTexts) | |
| 86 | } | |
| 87 | } | |
| 88 | ||
| 89 | private func row(_ item: AgendaItem) -> some SwiftUI.View { | |
| 90 | VStack(alignment: .leading, spacing: 2) { | |
| 91 | HStack(alignment: .firstTextBaseline, spacing: 6) { | |
| 92 | if let time = item.time.firstMatch(of: /[0-9]{1,2}:[0-9]{2}(-[0-9]{1,2}:[0-9]{2})?/) { | |
| 93 | Text(time.output.0).font(.callout.monospacedDigit()).foregroundStyle(.secondary) | |
| 94 | } | |
| 95 | Text(item.text).strikethrough(item.isDone) | |
| 96 | } | |
| 97 | HStack(spacing: 6) { | |
| 98 | Text(item.category) | |
| 99 | let extra = item.extra.split(whereSeparator: \.isWhitespace).joined(separator: " ") | |
| 100 | if !extra.isEmpty { Text(extra) } | |
| 101 | if !item.tags.isEmpty { Text(item.tags.map { ":" + $0 }.joined() + ":") } | |
| 102 | } | |
| 103 | .font(.caption) | |
| 104 | .foregroundStyle(.secondary) | |
| 105 | } | |
| 106 | .swipeActions(edge: .trailing) { | |
| 107 | if item.todo != nil, !item.isDone, doneKeyword(for: item) != nil { | |
| 108 | Button("Done", systemImage: "checkmark") { markDone(item) }.tint(.green) | |
| 109 | } | |
| 110 | } | |
| 111 | } | |
| 112 | ||
| 113 | /// The first done keyword of the item's TODO sequence. | |
| 114 | private func doneKeyword(for item: AgendaItem) -> String? { | |
| 115 | guard let todo = item.todo, let path = item.path else { return nil } | |
| 116 | let text = session.openTexts[path] ?? (try? String(contentsOfFile: path, encoding: .utf8)) ?? "" | |
| 117 | let settings = OrgParser.parse(text).settings | |
| 118 | return settings.todoSequences.first { ($0.active + $0.done).contains { $0.name == todo } }?.done.first?.name | |
| 119 | } | |
| 120 | ||
| 121 | private func markDone(_ item: AgendaItem) { | |
| 122 | guard let path = item.path, let start = item.headingOffset, let keyword = doneKeyword(for: item) else { return } | |
| 123 | switch session.perform(SetTodo(keyword: keyword), path: path, at: start, expect: item.headingLine.map { (start, $0) }) { | |
| 124 | case .failed(let text): message = text | |
| 125 | case .prompt: message = "This state change asks for a note; make it on the Mac" | |
| 126 | default: message = nil | |
| 127 | } | |
| 128 | tick += 1 | |
| 129 | } | |
| 130 | } | |
| 131 | ||
| 132 | extension AgendaItem { | |
| 133 | /// An entry, not a time-grid line. | |
| 134 | var isEntry: Bool { kind != .timeGrid && kind != .currentTime } | |
| 135 | } | |
| 136 | #endif | |
Sources/OrgstarMobile/CaptureScreen.swift added +93
| @@ -0,0 +1,93 @@ | ||
| 1 | #if os(iOS) | |
| 2 | import OrgApp | |
| 3 | import OrgCore | |
| 4 | import SwiftUI | |
| 5 | ||
| 6 | /// Capture with a template: its text to edit, filed where the template says. | |
| 7 | struct CaptureScreen: View { | |
| 8 | let workspace: WorkspaceModel | |
| 9 | let session: DocumentSession | |
| 10 | let request: CaptureRequest | |
| 11 | @Environment(\.dismiss) private var dismiss | |
| 12 | @State private var templates: [CaptureTemplate] = [] | |
| 13 | @State private var key: String? | |
| 14 | @State private var text = "" | |
| 15 | @State private var selection: TextSelection? | |
| 16 | @State private var problem: String? | |
| 17 | @FocusState private var focused: Bool | |
| 18 | ||
| 19 | private var template: CaptureTemplate? { templates.first { $0.key == key } } | |
| 20 | ||
| 21 | var body: some View { | |
| 22 | NavigationStack { | |
| 23 | Form { | |
| 24 | Picker("Template", selection: $key) { | |
| 25 | ForEach(templates) { Text($0.name).tag(Optional($0.key)) } | |
| 26 | } | |
| 27 | Section { | |
| 28 | TextEditor(text: $text, selection: $selection) | |
| 29 | .font(.body.monospaced()) | |
| 30 | .frame(minHeight: 200) | |
| 31 | .focused($focused) | |
| 32 | } footer: { | |
| 33 | if let problem { Text(problem).foregroundStyle(.red) } | |
| 34 | } | |
| 35 | } | |
| 36 | .navigationTitle("Capture") | |
| 37 | .navigationBarTitleDisplayMode(.inline) | |
| 38 | .toolbar { | |
| 39 | ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } | |
| 40 | ToolbarItem(placement: .confirmationAction) { Button("File", action: file).disabled(template == nil) } | |
| 41 | } | |
| 42 | } | |
| 43 | .onAppear { | |
| 44 | let loaded = CaptureTemplates.load() | |
| 45 | templates = loaded.templates | |
| 46 | problem = loaded.problems.first | |
| 47 | key = request.template.flatMap { wanted in templates.first { $0.key == wanted }?.key } ?? templates.first?.key | |
| 48 | focused = true | |
| 49 | } | |
| 50 | .onChange(of: key, initial: true) { fill() } | |
| 51 | } | |
| 52 | ||
| 53 | /// The template expanded, its prompts taking their defaults. | |
| 54 | private func fill() { | |
| 55 | guard let template else { return } | |
| 56 | var context = CaptureContext( | |
| 57 | initial: request.properties["initial"] ?? "", annotation: request.properties["annotation"] ?? "", | |
| 58 | allTags: (try? workspace.index.tags()) ?? [] | |
| 59 | ) | |
| 60 | context.linkProperties = request.properties | |
| 61 | let answers = Capture.questions(template.template, context: context).map { _ in "" } | |
| 62 | switch Capture.fill(template.template, context: context, answers: answers) { | |
| 63 | case .success(let filled): | |
| 64 | text = filled.text | |
| 65 | // Where the template's `%?` was. | |
| 66 | selection = TextSelection(insertionPoint: String.Index(utf16Offset: filled.cursor, in: filled.text)) | |
| 67 | case .failure(.message(let message)): problem = message | |
| 68 | } | |
| 69 | } | |
| 70 | ||
| 71 | private func file() { | |
| 72 | guard let template else { return } | |
| 73 | var file: String? | |
| 74 | switch template.target { | |
| 75 | case .id(let id): | |
| 76 | guard let location = try? workspace.index.headings(withID: id).first else { | |
| 77 | problem = "Cannot find target ID \"\(id)\"" | |
| 78 | return | |
| 79 | } | |
| 80 | file = location.path | |
| 81 | case .clock: | |
| 82 | problem = "There's no running clock on iOS" | |
| 83 | return | |
| 84 | default: | |
| 85 | break | |
| 86 | } | |
| 87 | switch session.capture(text, template: template, root: workspace.rootPaths.first, file: file) { | |
| 88 | case .failure(let error): problem = error.message | |
| 89 | case .success: dismiss() | |
| 90 | } | |
| 91 | } | |
| 92 | } | |
| 93 | #endif | |
Sources/OrgstarMobile/FoldersScreen.swift added +62
| @@ -0,0 +1,62 @@ | ||
| 1 | #if os(iOS) | |
| 2 | import OrgApp | |
| 3 | import SwiftUI | |
| 4 | import UniformTypeIdentifiers | |
| 5 | ||
| 6 | /// The folders of org files the agenda reads, and their files. | |
| 7 | struct FoldersScreen: View { | |
| 8 | let workspace: WorkspaceModel | |
| 9 | @State private var importing = false | |
| 10 | ||
| 11 | var body: some View { | |
| 12 | List { | |
| 13 | ForEach(workspace.rootPaths, id: \.self) { root in | |
| 14 | Section((root as NSString).lastPathComponent) { | |
| 15 | let files = workspace.files.filter { $0.root == root && $0.kind == .org } | |
| 16 | ForEach(files) { file in | |
| 17 | Label(file.relativePath, systemImage: "doc.text") | |
| 18 | } | |
| 19 | let pending = workspace.downloading.filter { $0.hasPrefix(root + "/") }.sorted() | |
| 20 | ForEach(pending, id: \.self) { path in | |
| 21 | Label(String(path.dropFirst(root.count + 1)), systemImage: "icloud.and.arrow.down") | |
| 22 | .foregroundStyle(.secondary) | |
| 23 | } | |
| 24 | Button("Remove Folder", role: .destructive) { try? workspace.removeRoot(path: root) } | |
| 25 | } | |
| 26 | } | |
| 27 | if let error = workspace.lastError { | |
| 28 | Text(error).foregroundStyle(.red) | |
| 29 | } | |
| 30 | } | |
| 31 | .navigationTitle("Folders") | |
| 32 | .overlay { | |
| 33 | if workspace.rootPaths.isEmpty { | |
| 34 | ContentUnavailableView { | |
| 35 | Label("No folders", systemImage: "folder") | |
| 36 | } description: { | |
| 37 | Text("Add the folder your org files are in, from iCloud Drive or another app's storage.") | |
| 38 | } actions: { | |
| 39 | Button("Add Folder") { importing = true } | |
| 40 | } | |
| 41 | } | |
| 42 | } | |
| 43 | .toolbar { | |
| 44 | ToolbarItem(placement: .topBarLeading) { | |
| 45 | Button("Add Folder", systemImage: "folder.badge.plus") { importing = true } | |
| 46 | } | |
| 47 | } | |
| 48 | .fileImporter(isPresented: $importing, allowedContentTypes: [.folder]) { result in | |
| 49 | guard case .success(let url) = result else { return } | |
| 50 | // Kept for the bookmark; WorkspaceRoot resolves it with access again at launch. | |
| 51 | _ = url.startAccessingSecurityScopedResource() | |
| 52 | Task { | |
| 53 | do { | |
| 54 | try await workspace.addRoot(url) | |
| 55 | } catch { | |
| 56 | workspace.lastError = "Can't add \(url.lastPathComponent): \(error.localizedDescription)" | |
| 57 | } | |
| 58 | } | |
| 59 | } | |
| 60 | } | |
| 61 | } | |
| 62 | #endif | |
Sources/OrgstarMobile/OrgstarMobileApp.swift added +86
| @@ -0,0 +1,86 @@ | ||
| 1 | #if os(iOS) | |
| 2 | import OrgApp | |
| 3 | import OrgCore | |
| 4 | import SwiftUI | |
| 5 | ||
| 6 | /// Orgstar on iPhone and iPad: the agenda, capture, and the folders they read. | |
| 7 | @main | |
| 8 | struct OrgstarMobileApp: App { | |
| 9 | @State private var workspace = Result { try WorkspaceModel(directory: WorkspaceModel.defaultDirectory) } | |
| 10 | @State private var session = DocumentSession() | |
| 11 | ||
| 12 | var body: some Scene { | |
| 13 | WindowGroup { | |
| 14 | switch workspace { | |
| 15 | case .success(let model): | |
| 16 | RootView(workspace: model, session: session) | |
| 17 | case .failure(let error): | |
| 18 | ContentUnavailableView("Can't open the index", systemImage: "exclamationmark.triangle", description: Text(error.localizedDescription)) | |
| 19 | } | |
| 20 | } | |
| 21 | } | |
| 22 | } | |
| 23 | ||
| 24 | /// An org-protocol capture, or a plain one. | |
| 25 | struct CaptureRequest: Identifiable { | |
| 26 | let id = UUID() | |
| 27 | var template: String? | |
| 28 | var properties: [String: String] = [:] | |
| 29 | } | |
| 30 | ||
| 31 | struct RootView: View { | |
| 32 | let workspace: WorkspaceModel | |
| 33 | let session: DocumentSession | |
| 34 | @State private var agenda: AgendaModel | |
| 35 | @State private var capture: CaptureRequest? | |
| 36 | ||
| 37 | init(workspace: WorkspaceModel, session: DocumentSession) { | |
| 38 | self.workspace = workspace | |
| 39 | self.session = session | |
| 40 | _agenda = State(initialValue: AgendaModel(workspace: workspace)) | |
| 41 | } | |
| 42 | ||
| 43 | var body: some View { | |
| 44 | TabView { | |
| 45 | Tab("Agenda", systemImage: "calendar") { | |
| 46 | NavigationStack { | |
| 47 | AgendaScreen(workspace: workspace, session: session, agenda: agenda) | |
| 48 | .toolbar { captureButton } | |
| 49 | } | |
| 50 | } | |
| 51 | Tab("Folders", systemImage: "folder") { | |
| 52 | NavigationStack { | |
| 53 | FoldersScreen(workspace: workspace) | |
| 54 | .toolbar { captureButton } | |
| 55 | } | |
| 56 | } | |
| 57 | } | |
| 58 | .sheet(item: $capture) { request in | |
| 59 | CaptureScreen(workspace: workspace, session: session, request: request) | |
| 60 | } | |
| 61 | .task { | |
| 62 | // Development: `ORGSTAR_ROOT` adds a folder at launch, as on the Mac. | |
| 63 | if let root = ProcessInfo.processInfo.environment["ORGSTAR_ROOT"] { try? await workspace.addRoot(URL(fileURLWithPath: root)) } | |
| 64 | workspace.startWatching() | |
| 65 | await workspace.refresh() | |
| 66 | } | |
| 67 | .onOpenURL { url in | |
| 68 | if case .capture(let template, let properties)? = OrgProtocol.parse(url.absoluteString) { | |
| 69 | capture = CaptureRequest(template: template, properties: properties) | |
| 70 | } | |
| 71 | } | |
| 72 | } | |
| 73 | ||
| 74 | private var captureButton: some ToolbarContent { | |
| 75 | ToolbarItem(placement: .primaryAction) { | |
| 76 | Button("Capture", systemImage: "square.and.pencil") { capture = CaptureRequest() } | |
| 77 | } | |
| 78 | } | |
| 79 | } | |
| 80 | #else | |
| 81 | /// The iOS app has nothing to run elsewhere. | |
| 82 | @main | |
| 83 | enum OrgstarMobileUnavailable { | |
| 84 | static func main() { print("Orgstar for iOS runs on iPhone and iPad.") } | |
| 85 | } | |
| 86 | #endif | |
docs/design.md +1 −1
| @@ -334,7 +334,7 @@ Fidelity is checked against Emacs itself, using the same oracle pattern as orgo' | ||
| 334 | 334 | |
| 335 | 335 | Seven phases, each usable on its own. Phase 1 is read-only, so it can run next to Emacs from the first build. |
| 336 | 336 | |
| 337 | Status, 2026-10-05: phases 1–6 are built for macOS. Owed from phase 1: the items left open below and the gate run on the M1 Air. The gates pass on an M-series Mac except peak memory for small real files (about 20x for a 161 kB file, from fixed overhead). Phase 7 has not started. | |
| 337 | Status, 2026-10-06: phases 1–6 are built for macOS. Owed from phase 1: the items left open below and the gate run on the M1 Air. The gates pass on an M-series Mac except peak memory for small real files (about 20x for a 161 kB file, from fixed overhead). Phase 7 has started: the iOS app has the agenda and capture, and runs in the Simulator; device builds need signing. | |
| 338 | 338 | |
| 339 | 339 | 1. **Core and viewer:** `OrgCore` parser, workspace, index, read-only rendering, folding, search. |
| 340 | 340 | 2. **Editor:** text editing, structure and heading commands, timestamps, `M-q`, table alignment, command palette, Emacs and Mac keymaps, remaining tree-sitter grammars. |
scripts/build-ios-app.sh added +80
| @@ -0,0 +1,80 @@ | ||
| 1 | #!/bin/sh | |
| 2 | # Builds Orgstar.app for the iOS Simulator in .build/ios-app from the OrgstarMobile product, | |
| 3 | # signed ad hoc. Install with `xcrun simctl install booted .build/ios-app/Orgstar.app`. | |
| 4 | # Usage: scripts/build-ios-app.sh [version] | |
| 5 | set -eu | |
| 6 | ||
| 7 | cd "$(dirname "$0")/.." | |
| 8 | version="${1:-0.1.0}" | |
| 9 | app=".build/ios-app/Orgstar.app" | |
| 10 | products=".build/ios/Build/Products/Debug-iphonesimulator" | |
| 11 | ||
| 12 | xcodebuild -scheme OrgstarMobile -destination 'generic/platform=iOS Simulator' -derivedDataPath .build/ios build -quiet | |
| 13 | ||
| 14 | rm -rf "$app" | |
| 15 | mkdir -p "$app" | |
| 16 | cp "$products/OrgstarMobile" "$app/OrgstarMobile" | |
| 17 | for bundle in "$products"/*.bundle; do | |
| 18 | [ -e "$bundle" ] && cp -R "$bundle" "$app/" | |
| 19 | done | |
| 20 | ||
| 21 | cat > "$app/Info.plist" <<PLIST | |
| 22 | <?xml version="1.0" encoding="UTF-8"?> | |
| 23 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| 24 | <plist version="1.0"> | |
| 25 | <dict> | |
| 26 | <key>CFBundleIdentifier</key> | |
| 27 | <string>sh.krz.orgstar</string> | |
| 28 | <key>CFBundleName</key> | |
| 29 | <string>Orgstar</string> | |
| 30 | <key>CFBundleDisplayName</key> | |
| 31 | <string>Orgstar</string> | |
| 32 | <key>CFBundleExecutable</key> | |
| 33 | <string>OrgstarMobile</string> | |
| 34 | <key>CFBundlePackageType</key> | |
| 35 | <string>APPL</string> | |
| 36 | <key>CFBundleShortVersionString</key> | |
| 37 | <string>${version}</string> | |
| 38 | <key>CFBundleVersion</key> | |
| 39 | <string>${version}</string> | |
| 40 | <key>CFBundleSupportedPlatforms</key> | |
| 41 | <array><string>iPhoneSimulator</string></array> | |
| 42 | <key>MinimumOSVersion</key> | |
| 43 | <string>26.0</string> | |
| 44 | <key>UIDeviceFamily</key> | |
| 45 | <array><integer>1</integer><integer>2</integer></array> | |
| 46 | <key>UILaunchScreen</key> | |
| 47 | <dict/> | |
| 48 | <key>UIApplicationSceneManifest</key> | |
| 49 | <dict> | |
| 50 | <key>UIApplicationSupportsMultipleScenes</key> | |
| 51 | <true/> | |
| 52 | </dict> | |
| 53 | <key>UISupportedInterfaceOrientations</key> | |
| 54 | <array> | |
| 55 | <string>UIInterfaceOrientationPortrait</string> | |
| 56 | <string>UIInterfaceOrientationLandscapeLeft</string> | |
| 57 | <string>UIInterfaceOrientationLandscapeRight</string> | |
| 58 | </array> | |
| 59 | <key>UISupportedInterfaceOrientations~ipad</key> | |
| 60 | <array> | |
| 61 | <string>UIInterfaceOrientationPortrait</string> | |
| 62 | <string>UIInterfaceOrientationPortraitUpsideDown</string> | |
| 63 | <string>UIInterfaceOrientationLandscapeLeft</string> | |
| 64 | <string>UIInterfaceOrientationLandscapeRight</string> | |
| 65 | </array> | |
| 66 | <key>CFBundleURLTypes</key> | |
| 67 | <array> | |
| 68 | <dict> | |
| 69 | <key>CFBundleURLName</key> | |
| 70 | <string>org-protocol</string> | |
| 71 | <key>CFBundleURLSchemes</key> | |
| 72 | <array><string>org-protocol</string></array> | |
| 73 | </dict> | |
| 74 | </array> | |
| 75 | </dict> | |
| 76 | </plist> | |
| 77 | PLIST | |
| 78 | ||
| 79 | codesign --force --sign - "$app" | |
| 80 | echo "$app" | |