Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 123 additions & 68 deletions XCStringsEditor/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import OSLog
fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ContentView")

struct ActivityIndicatorModifier: ViewModifier {
@Binding var isPresented: Bool
var isPresented: Bool

func body(content: Content) -> some View {
ZStack {
Expand All @@ -39,7 +39,7 @@ struct ContentView: View {
@State private var nextEditingItem: LocalizeItem?
@FocusState private var focusedField: Field?
@State private var showConfirmClose: Bool = false

@State private var collapsedNodes: Set<LocalizeItem.ID> = []

// init() {
// #if DEBUG
Expand All @@ -49,54 +49,22 @@ struct ContentView: View {

var body: some View {
@Bindable var appModel = appModel

NavigationStack {
Table(selection: $appModel.selected, sortOrder: $appModel.sortOrder) {
// Key
TableColumn("Key", value: \.key) { item in
keyColumnView(item: item)
}


// Source
TableColumn("Default Localization (\(appModel.baseLanguage.code))") { item in
TableColumn("Default Localization (\(appModel.documents.first?.baseLanguage.code ?? ""))") { item in
sourceColumnView(item: item)
}

// Translation
TableColumn(appModel.currentLanguage.localizedName) { item in
ZStack {
Text(verbatim: item.translation ?? item.sourceString)
.foregroundStyle(item.translation == nil ? .secondary.opacity(0.5) : (item.needsWork ? Color.orange : .primary))
.opacity(isEditing && item.id == appModel.editingID ? 0.0 : 1.0)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(nil)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
.contentShape(Rectangle())
.allowsHitTesting(item.children == nil)
.onTapGesture {
onTapTranslation(item: item)
}

if isEditing && appModel.editingID == item.id {
// Editing TextField
TextField(item.sourceString, text: $translation, axis: .vertical)
.lineLimit(nil)
.focused($focusedField, equals: .translation)
.onSubmit {
focusedField = .table
}
.onAppear {
logger.debug("textfield appear")

self.translation = item.translation ?? ""
DispatchQueue.main.async {
focusedField = .translation
}
}
}
}
currentColumnView(item: item)
}

// Reverse Translation
Expand All @@ -114,16 +82,28 @@ struct ContentView: View {
}
.width(80)
.alignment(.center)

} rows: {
OutlineGroup(appModel.localizeItems, children: \.children) { item in
TableRow(item)
.contextMenu { rowContextMenu(for: item) }
ForEach(appModel.documents) { document in
// OutlineGroup(document.item, children: \.children) { item in
// TableRow(item)
// .contextMenu { rowContextMenu(for: item) }
// }

// Simulated header with TableRow for each document.
// Used this way because a simple OutlineGroup always appears initially collapsed.

TableRow(document.item)
if !collapsedNodes.contains(document.item.id) {
OutlineGroup(document.localizeItems, children: \.children) { item in
TableRow(item)
.contextMenu { rowContextMenu(for: item) }
}
}
}
}
.focused($focusedField, equals: .table)
.searchable(text: $appModel.searchText)
.navigationTitle(appModel.title ?? "XCStringsEditor")
.navigationTitle(appModel.title)
.onAppear {
startMonitorKeyboardEvent()

Expand All @@ -143,7 +123,7 @@ struct ContentView: View {
.frame(width: 6, height: 6)
}
}

if appModel.languages.isEmpty == false {
ToolbarItemGroup(placement: .primaryAction) {
Spacer()
Expand Down Expand Up @@ -210,7 +190,6 @@ struct ContentView: View {
.menuIndicator(.hidden)
}
}

}
.toolbarRole(.editor)
.onChange(of: appModel.sortOrder, { oldValue, newValue in
Expand All @@ -219,7 +198,7 @@ struct ContentView: View {
.onChange(of: focusedField) { oldValue, newValue in
if oldValue == .translation && newValue != .translation {
logger.debug("textfield focusout")

let oldSelected = appModel.selected
endEditing()

Expand All @@ -237,7 +216,7 @@ struct ContentView: View {
Button("OK", role: .cancel) {}
} message: {
Text("API Key must be set in settings to use this function.")
}
}
.alert("Confirm Close", isPresented: $showConfirmClose) {
Button("Cancel", role: .cancel) {}
Button("Discard Changes", role: .destructive) {
Expand All @@ -249,7 +228,7 @@ struct ContentView: View {
}

} // NavigationStack
.modifier(ActivityIndicatorModifier(isPresented: $appModel.isLoading))
.modifier(ActivityIndicatorModifier(isPresented: appModel.isLoading))
}

private func endEditing(updateTranslation: Bool = true) {
Expand All @@ -258,7 +237,9 @@ struct ContentView: View {
#endif
// update editing item
if let editingID = appModel.editingID, updateTranslation == true {
appModel.updateTranslation(for: editingID, with: translation)
for document in appModel.documents {
document.updateTranslation(for: editingID, with: translation)
}
}

appModel.editingID = nil
Expand All @@ -279,6 +260,14 @@ struct ContentView: View {

private func keyColumnView(item: LocalizeItem) -> some View {
HStack {
if let children = item.children, !children.isEmpty {
// Chevron reflecting expanded/collapsed state
Image(systemName: collapsedNodes.contains(item.id) ? "chevron.right" : "chevron.down")
.onTapGesture {
toggleCollapsed(item.id)
}
.foregroundStyle(.secondary)
}
Circle()
.fill(.blue)
.frame(width: 6, height: 6)
Expand All @@ -299,6 +288,43 @@ struct ContentView: View {
.foregroundStyle(item.translateLater || item.shouldTranslate == false ? .secondary : .primary)
}

@ViewBuilder
private func currentColumnView(item: LocalizeItem) -> some View {
ZStack {
Text(verbatim: item.translation ?? item.sourceString)
.foregroundStyle(item.translation == nil ? .secondary.opacity(0.5) : (item.needsWork ? Color.orange : .primary))
.opacity(isEditing && item.id == appModel.editingID ? 0.0 : 1.0)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(nil)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
.contentShape(Rectangle())
.allowsHitTesting(item.children == nil)
.onTapGesture {
onTapTranslation(item: item)
}

if isEditing && appModel.editingID == item.id {
// Editing TextField
TextField(item.sourceString, text: $translation, axis: .vertical)
.lineLimit(nil)
.focused($focusedField, equals: .translation)
.onSubmit {
focusedField = .table
}
.onAppear {
logger.debug("textfield appear")

self.translation = item.translation ?? ""
DispatchQueue.main.async {
focusedField = .translation
}
}
}
}
}


@ViewBuilder
private func reverseTranslationColumnView(item: LocalizeItem) -> some View {
if isReverseTranslationMatch(item) {
Expand Down Expand Up @@ -326,56 +352,76 @@ struct ContentView: View {
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
}

@ViewBuilder
private func rowContextMenu(for item: LocalizeItem) -> some View {
let itemIDs = contextMenuItemIDs(itemID: item.id)

Button("Auto Translate") {
Task {
await appModel.translate(ids: itemIDs)
for document in appModel.documents {
await document.translate(ids: itemIDs)
}
}
}
Button("Reverse Translate") {
Task {
await appModel.reverseTranslate(ids: itemIDs)
for document in appModel.documents {
await document.reverseTranslate(ids: itemIDs)
}
}
}

Divider()

Button("Mark for Review") {
appModel.markNeedsReview(ids: itemIDs)
for document in appModel.documents {
document.markNeedsReview(ids: itemIDs)
}
}
Button("Mark as Reviewed") {
appModel.reviewed(ids: itemIDs)
for document in appModel.documents {
document.reviewed(ids: itemIDs)
}
}

Divider()

if appModel.items(with: Array(itemIDs)).allSatisfy({ $0.shouldTranslate == false }) {
if appModel.documents.flatMap({$0.items(with: Array(itemIDs))}).allSatisfy({ $0.shouldTranslate == false }) {
Button("Mark for Translation") {
appModel.setShouldTranslate(true, for: itemIDs)
for document in appModel.documents {
document.setShouldTranslate(true, for: itemIDs)
}
}
} else {
Button("Mark as \"Don't Translate\"") {
appModel.setShouldTranslate(false, for: itemIDs)
for document in appModel.documents {
document.setShouldTranslate(false, for: itemIDs)
}
}
}

Divider()

Button("Mark for Translate Later") {
appModel.markTranslateLater(ids: itemIDs, value: true)
for document in appModel.documents {
document.markTranslateLater(ids: itemIDs, value: true)
}
}
Button("Unmark Translate Later") {
appModel.markTranslateLater(ids: itemIDs, value: false)
for document in appModel.documents {
document.markTranslateLater(ids: itemIDs, value: false)
}
}
Button("Mark for Needs Work") {
appModel.markNeedsWork(ids: itemIDs, value: true)
for document in appModel.documents {
document.markNeedsWork(ids: itemIDs, value: true)
}
}
Button("Unmark Needs Work") {
appModel.markNeedsWork(ids: itemIDs, value: false)
for document in appModel.documents {
document.markNeedsWork(ids: itemIDs, value: false)
}
}
}

Expand Down Expand Up @@ -411,8 +457,9 @@ struct ContentView: View {
// print(item)
// }
// #endif

appModel.save()
for document in appModel.documents {
document.save()
}
}

private func contextMenuItemIDs(itemID: LocalizeItem.ID) -> Set<LocalizeItem.ID> {
Expand Down Expand Up @@ -463,6 +510,14 @@ struct ContentView: View {
private func isReverseTranslationMatch(_ item: LocalizeItem) -> Bool {
return item.reverseTranslation?.uppercased() == item.sourceString.uppercased()
}

private func toggleCollapsed(_ id: LocalizeItem.ID) {
if collapsedNodes.contains(id) {
collapsedNodes.remove(id)
} else {
collapsedNodes.insert(id)
}
}
}

#Preview {
Expand Down
Loading