diff --git a/Package.swift b/Package.swift index d477410..512d300 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,7 @@ import PackageDescription let package = Package( name: "Flashcards", dependencies: [ - .package(url: "https://github.com/AparokshaUI/Adwaita", branch: "0.2.3"), + .package(url: "https://github.com/AparokshaUI/Adwaita", branch: "main"), .package(url: "https://github.com/AparokshaUI/Localized", from: "0.2.2") ], targets: [ diff --git a/Sources/Flashcards.swift b/Sources/Flashcards.swift index 0c932c7..f753791 100644 --- a/Sources/Flashcards.swift +++ b/Sources/Flashcards.swift @@ -11,49 +11,15 @@ struct Flashcards: App { @State("sets", folder: "io.github.david_swift.Flashcards", forceUpdates: true) private var sets: [FlashcardsSet] = [] - @State private var copied = Signal() let id = "io.github.david_swift.Flashcards" var app: GTUIApp! var scene: Scene { Window(id: "main") { window in - ContentView(copied: $copied, sets: $sets, app: app, window: window) + ContentView(sets: $sets, app: app, window: window) } .title("Memorize") .quitShortcut() - .overlay { - AboutWindow(id: "about", appName: "Memorize", developer: "david-swift", version: "0.1.5") - .icon(.custom(name: "io.github.david_swift.Flashcards")) - .website(.init(string: "https://github.com/david-swift/Memorize")) - .issues(.init(string: "https://github.com/david-swift/Memorize/issues")) - for (index, set) in sets.enumerated() { - // Import flashcards and add to a set. - Window(id: "import-\(set.id)", open: 0) { window in - ImportView( - set: .init { set } set: { sets[safe: index] = $0 }, - window: window - ) - } - .defaultSize(width: 500, height: 500) - .keyboardShortcut("Escape") { $0.close() } - - // Export a set. - Window(id: "export-\(set.id)", open: 0) { window in - ExportView(copied: $copied, set: set, window: window) - } - .title(Loc.export(title: set.name)) - .defaultSize(width: 500, height: 500) - .keyboardShortcut("Escape") { $0.close() } - - // Delete a set. - Window(id: "delete-\(set.id)", open: 0) { window in - DeleteView(set: set, window: window) { sets = sets.filter { $0.id != set.id } } - } - .defaultSize(width: 450, height: 350) - .resizable(false) - .keyboardShortcut("Escape") { $0.close() } - } - } } } diff --git a/Sources/Model/FlashcardsView.swift b/Sources/Model/FlashcardsView.swift index 2d4eb45..d996413 100644 --- a/Sources/Model/FlashcardsView.swift +++ b/Sources/Model/FlashcardsView.swift @@ -5,16 +5,13 @@ import Adwaita -enum FlashcardsView: ViewSwitcherOption, Codable { +enum FlashcardsView: CustomStringConvertible { - case overview - case study - case test + case study(set: String) + case test(set: String) - var title: String { + var description: String { switch self { - case .overview: - Loc.overview case .study: Loc.studySwitcher case .test: @@ -22,26 +19,4 @@ enum FlashcardsView: ViewSwitcherOption, Codable { } } - var icon: Icon { - switch self { - case .overview: - return .default(icon: .viewReveal) - case .study: - return .default(icon: .mediaPlaybackStart) - case .test: - return .default(icon: .findLocation) - } - } - - init?(title: String) { - switch title { - case Loc.studySwitcher: - self = .study - case Loc.test: - self = .test - default: - self = .overview - } - } - } diff --git a/Sources/Model/Localized.yml b/Sources/Model/Localized.yml index ee8fe63..9a23497 100644 --- a/Sources/Model/Localized.yml +++ b/Sources/Model/Localized.yml @@ -588,12 +588,6 @@ semicolon: it: Punto e virgola pt_BR: Ponto e vírgula -editSetDescription: - en: Edit the set's content and title - de: Bearbeite den Inhalt und Titel des Sets - it: Modifica il contenuto e il titolo del Set - pt_BR: Editar conteúdo e título do set - createSet: en: Create Set de: Set erstellen @@ -604,3 +598,21 @@ copied: en: Contents copied to clipboard de: Inhalte wurden kopiert it: Contenuto copiato negli appunti + +flashcards(count): + en(count == "1"): 1 flashcard + en: (count) flashcards + de(count == "1"): 1 Karteikarte + de: (count) Karteikarten + +activeStudySession: + en: Active Study Session + de: Aktive Lerneinheit + +activeStudySessionDescription: + en: You are currently in a study session + de: Du befindest dich in einer Lerneinheit + +toggleSidebar: + en: Toggle Sidebar + de: Seitenleiste ein- und ausblenden diff --git a/Sources/View/ContentView.swift b/Sources/View/ContentView.swift index ce8cbc1..ee6fc43 100644 --- a/Sources/View/ContentView.swift +++ b/Sources/View/ContentView.swift @@ -9,12 +9,10 @@ import Foundation struct ContentView: WindowView { - @Binding var copied: Signal @Binding var sets: [FlashcardsSet] @State("selected-set") private var selectedSet = "" - @State("flashcards-view") - private var flashcardsView: FlashcardsView = .overview + @State private var flashcardsView: NavigationStack = .init() @State private var filter: String? @State private var editMode = false @State("width") @@ -23,34 +21,48 @@ struct ContentView: WindowView { private var height = 550 @State("maximized") private var maximized = false + @State private var sidebarVisible = true var app: GTUIApp var window: GTUIApplicationWindow + var smallWindow: Bool { width < 600 } + var view: Body { - OverlaySplitView(visible: .constant(flashcardsView == .overview && !editMode && !sets.isEmpty)) { - sidebar - } content: { - content - .topToolbar(visible: !editMode || flashcardsView != .overview) { - ToolbarView( - flashcardsView: $flashcardsView, - sets: $sets, - selectedSet: $selectedSet, - filter: $filter, - app: app, - window: window, - addSet: addSet - ).content + NavigationView($flashcardsView, Loc.overview) { view in + Bin() + .child { + switch view { + case let .study(set): + ViewStack(element: set) { _ in + StudyView(set: binding(id: set)) + } + case let .test(set): + TestView(set: binding(id: set)) + } + } + .topToolbar { + HeaderBar.empty() } + } initialView: { + OverlaySplitView(visible: .init { sidebarVisible || !smallWindow } set: { sidebarVisible = $0 }) { + sidebar + } content: { + content + } + .collapsed(smallWindow) } - .toast(Loc.copied, signal: copied) } var sidebar: View { ScrollView { List( sets.map { ($0, $0.score(filter)) }.sorted { $0.1 > $1.1 }.filter { $0.1 != 0 }.map { $0.0 }, - selection: $selectedSet + selection: .init { + selectedSet + } set: { newValue in + selectedSet = newValue + sidebarVisible = false + } ) { set in Text(set.name) .ellipsize() @@ -64,12 +76,11 @@ struct ContentView: WindowView { SearchEntry() .placeholderText(Loc.filterSets) .text(.init { filter ?? "" } set: { filter = $0 }) - .focused(.constant(!editMode && flashcardsView == .overview)) + .focused(.constant(filter != nil)) .padding(5, .horizontal.add(.bottom)) } .topToolbar { ToolbarView( - flashcardsView: $flashcardsView, sets: $sets, selectedSet: $selectedSet, filter: $filter, @@ -83,37 +94,50 @@ struct ContentView: WindowView { @ViewBuilder var content: Body { if let index = sets.firstIndex(where: { $0.id == selectedSet }), let set = sets[safe: index] { let binding = Binding { sets[safe: index] ?? .init() } set: { sets[safe: index] = $0 } - switch flashcardsView { - case .overview: - SetOverview(set: binding, editMode: $editMode, app: app, window: window) - case .study: - ViewStack(element: set) { _ in - StudyView(set: binding) - } - case .test: - TestView(set: binding) + SetOverview( + set: binding, + editMode: $editMode, + flashcardsView: $flashcardsView, + sidebarVisible: $sidebarVisible, + smallWindow: smallWindow + ) { + sets = sets.filter { $0.id != set.id } } - } else if !sets.isEmpty { - StatusPage( - Loc.noSelection, - icon: .custom(name: "io.github.david_swift.Flashcards.set-symbolic"), - description: Loc.noSelectionDescription - ) - .centerMinSize() } else { - StatusPage( - Loc.noSets, - icon: .custom(name: "io.github.david_swift.Flashcards.set-symbolic"), - description: Loc.noSetsDescription - ) { - Button(Loc.createSet) { - addSet() + VStack { + if !sets.isEmpty { + StatusPage( + Loc.noSelection, + icon: .custom(name: "io.github.david_swift.Flashcards.set-symbolic"), + description: Loc.noSelectionDescription + ) + .centerMinSize() + } else { + StatusPage( + Loc.noSets, + icon: .custom(name: "io.github.david_swift.Flashcards.set-symbolic"), + description: Loc.noSetsDescription + ) { + Button(Loc.createSet) { + addSet() + } + .style("pill") + .style("suggested-action") + .horizontalCenter() + } + .centerMinSize() + } + } + .topToolbar { + HeaderBar.start { + if smallWindow { + Button(icon: .default(icon: .sidebarShow)) { + sidebarVisible.toggle() + } + .tooltip(Loc.toggleSidebar) + } } - .style("pill") - .style("suggested-action") - .horizontalCenter() } - .centerMinSize() } } @@ -127,6 +151,15 @@ struct ContentView: WindowView { let newSet = FlashcardsSet() sets.insert(newSet, at: 0) selectedSet = newSet.id + editMode = true + } + + func binding(id: String) -> Binding { + .init { + sets.first { $0.id == id } ?? .init() + } set: { newValue in + sets[safe: sets.firstIndex { $0.id == id }] = newValue + } } } diff --git a/Sources/View/DeleteView.swift b/Sources/View/DeleteView.swift index e7d24a4..3e1e279 100644 --- a/Sources/View/DeleteView.swift +++ b/Sources/View/DeleteView.swift @@ -8,8 +8,8 @@ import Adwaita struct DeleteView: View { var set: FlashcardsSet - var window: GTUIWindow var delete: () -> Void + var close: () -> Void var view: Body { ScrollView { @@ -19,12 +19,12 @@ struct DeleteView: View { .topToolbar { HeaderBar(titleButtons: false) { Button(Loc.cancel) { - window.close() + close() } } end: { Button(Loc.delete) { + close() delete() - window.close() } .style("destructive-action") } diff --git a/Sources/View/EditView.swift b/Sources/View/EditView.swift index b475e49..fd46c7f 100644 --- a/Sources/View/EditView.swift +++ b/Sources/View/EditView.swift @@ -11,8 +11,7 @@ struct EditView: View { @Binding var editMode: Bool @State private var expanded = false @State private var focusedFront: String? - var app: GTUIApp - var window: GTUIWindow + @State private var importFlashcards = false var view: Body { ScrollView { @@ -27,20 +26,6 @@ struct EditView: View { .vexpand() .topToolbar { HeaderBar(titleButtons: false) { - ViewStack(element: set) { _ in - HStack { - Button(icon: .default(icon: .userTrash)) { - app.addWindow("delete-\(set.id)", parent: window) - editMode = false - } - .tooltip(Loc.deleteSet) - Button(icon: .custom(name: "io.github.david_swift.Flashcards.share-symbolic")) { - app.addWindow("export-\(set.id)", parent: window) - } - .padding(10, .horizontal) - .tooltip(Loc.exportSet) - } - } } end: { Button(Loc.done) { editMode = false @@ -133,7 +118,10 @@ struct EditView: View { ) { appendFlashcard() } secondary: { - app.addWindow("import-\(set.id)", parent: window) + importFlashcards = true + } + .dialog(visible: $importFlashcards, width: 400, height: 500) { + ImportView(set: $set) { importFlashcards = false } } } diff --git a/Sources/View/ExportView.swift b/Sources/View/ExportView.swift index 2f29c96..e05b7f2 100644 --- a/Sources/View/ExportView.swift +++ b/Sources/View/ExportView.swift @@ -12,7 +12,7 @@ struct ExportView: View { @State private var switchSides = false @Binding var copied: Signal var set: FlashcardsSet - var window: GTUIWindow + var close: () -> Void var view: Body { ScrollView { @@ -45,12 +45,12 @@ struct ExportView: View { .topToolbar { HeaderBar(titleButtons: false) { Button(Loc.cancel) { - window.close() + close() } } end: { Button(Loc.copy) { State.copy(text) - window.close() + close() copied.signal() } .style("suggested-action") @@ -71,7 +71,9 @@ struct ExportView: View { for flashcard in flashcards { text += flashcard.front + termDefinitionSeparator.syntax + flashcard.back + rowSeparator.syntax } - text.removeLast(rowSeparator.syntax.count) + if text.count > rowSeparator.syntax.count { + text.removeLast(rowSeparator.syntax.count) + } return text } diff --git a/Sources/View/ImportView.swift b/Sources/View/ImportView.swift index 7b63f1b..48f9cb9 100644 --- a/Sources/View/ImportView.swift +++ b/Sources/View/ImportView.swift @@ -13,7 +13,7 @@ struct ImportView: View { @State private var text = "" @State private var switchSides = false @State private var navigationStack = NavigationStack() - var window: GTUIWindow + var close: () -> Void var view: Body { VStack { @@ -147,7 +147,7 @@ struct ImportView: View { HeaderBar(titleButtons: false) { if destination == nil { Button(Loc.cancel) { - window.close() + close() } } } end: { @@ -162,7 +162,7 @@ struct ImportView: View { Button(label) { if case .paste = destination { set.flashcards += previewSet.flashcards - window.close() + close() } else { if case let .tutorial(app) = destination { navigationStack.push(.paste(app: app)) diff --git a/Sources/View/SetOverview.swift b/Sources/View/SetOverview.swift index 02eed6c..993608f 100644 --- a/Sources/View/SetOverview.swift +++ b/Sources/View/SetOverview.swift @@ -7,49 +7,80 @@ import Adwaita struct SetOverview: View { - @State("tutorial") - private var tutorial = true @Binding var set: FlashcardsSet @Binding var editMode: Bool - var app: GTUIApp - var window: GTUIWindow + @Binding var flashcardsView: NavigationStack + @Binding var sidebarVisible: Bool + @State private var export = false + @State private var deleteState = false + @State private var copied = Signal() + var smallWindow: Bool + var delete: () -> Void var view: Body { ViewStack(element: set) { _ in - if editMode { - VStack { - EditView(set: $set, editMode: $editMode, app: app, window: window) + title + .centerMinSize() + cards + .frame(minHeight: 270) + .valign(.center) + buttons + .centerMinSize() + } + .topToolbar { + HeaderBar { + HStack { + if smallWindow { + Button(icon: .default(icon: .sidebarShow)) { + sidebarVisible.toggle() + } + .tooltip(Loc.toggleSidebar) + } + Button(icon: .default(icon: .userTrash)) { + deleteState = true + } + .tooltip(Loc.deleteSet) + Button(icon: .custom(name: "io.github.david_swift.Flashcards.share-symbolic")) { + export = true + } + .tooltip(Loc.exportSet) + .insensitive(set.flashcards.isEmpty) } - } else { - ScrollView { - title - cards + .modifyContent(VStack.self) { $0.spacing(5) } + } end: { + Button(icon: .default(icon: .documentEdit)) { + editMode = true } + .tooltip(Loc.editSet) } + .titleWidget { } } + .dialog(visible: $deleteState, title: Loc.deleteTitle(title: set.name), id: "delete", height: 350) { + DeleteView(set: set) { + delete() + } close: { + deleteState = false + } + } + .dialog(visible: $export, title: Loc.export(title: set.name), id: "export", width: 400, height: 400) { + ExportView(copied: $copied, set: set) { + export = false + } + } + .dialog(visible: $editMode, id: "edit", width: 700, height: 550) { + EditView(set: $set, editMode: $editMode) + } + .toast(Loc.copied, signal: copied) } var title: View { - HStack { - Text(set.name) - .style("title-1") - .padding() - Button(icon: .default(icon: .documentEdit)) { - editMode = true - } - .style("circular") - .tooltip(Loc.editSet) - .padding() - .popover(visible: $tutorial) { - Text(Loc.editSetDescription) - .wrap() - .padding(10, .vertical) - .frame(maxWidth: 150) - Button(Loc.editSet) { - editMode = true - tutorial = false - } + VStack { + HStack { + Text(set.name) + .style("title-1") + .padding() } + Text(Loc.flashcards(count: set.flashcards.count)) } .halign(.center) } @@ -61,7 +92,22 @@ struct SetOverview: View { } .transition(.crossfade) } - .centerMinSize() + } + + var buttons: View { + HStack { + Button(Loc.studySwitcher, icon: .default(icon: .mediaPlaybackStart)) { + flashcardsView.push(.study(set: set.id)) + } + .style("pill") + Button(Loc.test, icon: .default(icon: .emblemDocuments)) { + flashcardsView.push(.test(set: set.id)) + } + .style("pill") + } + .halign(.center) + .modifyContent(VStack.self) { $0.spacing(20) } + .padding(20) } } diff --git a/Sources/View/StudyView.swift b/Sources/View/StudyView.swift index 28cf48f..fd2be9f 100644 --- a/Sources/View/StudyView.swift +++ b/Sources/View/StudyView.swift @@ -31,19 +31,17 @@ struct StudyView: View { startConfiguration } } else { - Bin() - .child { - if let flashcard { - if solution { - solutionView(flashcard: flashcard) - } else { - entryView(flashcard: flashcard) - } - } else { - pauseView - } + if let flashcard { + if solution { + solutionView(flashcard: flashcard) + .valign(.center) + } else { + entryView(flashcard: flashcard) + .valign(.center) } - .valign(.center) + } else { + pauseView + } } } .vexpand() @@ -105,21 +103,26 @@ struct StudyView: View { } @ViewBuilder var pauseView: Body { - Form { - sideSwitchRow - } - .padding(20) - .formWidth() - PillButtonSet( - primary: Loc.continueStudying, - icon: .default(icon: .mediaPlaybackStart), - secondary: Loc.terminateStudyMode, - icon: .default(icon: .mediaSkipBackward) - ) { - continueStudying() - } secondary: { - set.resetStudyProgress() - } + StatusPage() + .title(Loc.activeStudySession) + .description(Loc.activeStudySessionDescription) + .child { + Form { + sideSwitchRow + } + .padding(20) + .formWidth() + PillButtonSet( + primary: Loc.continueStudying, + icon: .default(icon: .mediaPlaybackStart), + secondary: Loc.terminateStudyMode, + icon: .default(icon: .mediaSkipBackward) + ) { + continueStudying() + } secondary: { + set.resetStudyProgress() + } + } } func solutionView(flashcard: Flashcard) -> View { diff --git a/Sources/View/ToolbarView.swift b/Sources/View/ToolbarView.swift index 809db8b..23029bb 100644 --- a/Sources/View/ToolbarView.swift +++ b/Sources/View/ToolbarView.swift @@ -7,10 +7,10 @@ import Adwaita struct ToolbarView: View { - @Binding var flashcardsView: FlashcardsView @Binding var sets: [FlashcardsSet] @Binding var selectedSet: String @Binding var filter: String? + @State private var about = false var app: GTUIApp var window: GTUIApplicationWindow var addSet: () -> Void @@ -22,31 +22,21 @@ struct ToolbarView: View { } .tooltip(Loc.addSet) } end: { - if flashcardsView == .overview && !sets.isEmpty { - menu - } + menu } .headerBarTitle { Text(Loc.sets) .style("heading") } - } - - @ViewBuilder var content: Body { - HeaderBar.end { - if flashcardsView != .overview || sets.isEmpty { - menu - } - } - .headerBarTitle { - if sets.contains(where: { $0.id == selectedSet }) { - ViewSwitcher(selection: $flashcardsView) - .wideDesign() - .transition(.crossfade) - } else { - [] - } - } + .aboutDialog( + visible: $about, + app: "Memorize", + developer: "david-swift", + version: "0.1.6", + icon: .custom(name: "io.github.david_swift.Flashcards"), + website: .init(string: "https://github.com/david-swift/Memorize"), + issues: .init(string: "https://github.com/david-swift/Memorize/issues") + ) } var menu: View { @@ -62,7 +52,7 @@ struct ToolbarView: View { viewMenu MenuSection { MenuButton(Loc.about, window: false) { - app.addWindow("about", parent: window) + about = true } } } @@ -72,24 +62,14 @@ struct ToolbarView: View { var viewMenu: MenuSection { .init { - Submenu(Loc.viewMenu) { - for (index, view) in FlashcardsView.allCases.enumerated() { - MenuButton(view.title) { - flashcardsView = view - } - .keyboardShortcut("\(index + 1)".alt()) - } - } - if flashcardsView == .overview { - MenuButton(Loc.filter) { - if filter != nil { - filter = nil - } else { - filter = "" - } + MenuButton(Loc.filter) { + if filter != nil { + filter = nil + } else { + filter = "" } - .keyboardShortcut("f".ctrl()) } + .keyboardShortcut("f".ctrl()) } } diff --git a/io.github.david_swift.Flashcards.json b/io.github.david_swift.Flashcards.json index 0a59733..37b6459 100644 --- a/io.github.david_swift.Flashcards.json +++ b/io.github.david_swift.Flashcards.json @@ -1,7 +1,7 @@ { "app-id": "io.github.david_swift.Flashcards", "runtime": "org.gnome.Platform", - "runtime-version": "45", + "runtime-version": "46", "sdk": "org.gnome.Sdk", "sdk-extensions": [ "org.freedesktop.Sdk.Extension.swift5"