diff --git a/Lexeme.xcodeproj/project.pbxproj b/Lexeme.xcodeproj/project.pbxproj index 5d79d3a..d7e1d27 100644 --- a/Lexeme.xcodeproj/project.pbxproj +++ b/Lexeme.xcodeproj/project.pbxproj @@ -6,7 +6,12 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + 847E4A882FDC4588007B4BFC /* lexeme.icon in Resources */ = {isa = PBXBuildFile; fileRef = 847E4A872FDC4588007B4BFC /* lexeme.icon */; }; +/* End PBXBuildFile section */ + /* Begin PBXFileReference section */ + 847E4A872FDC4588007B4BFC /* lexeme.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = lexeme.icon; sourceTree = ""; }; 84D4927C2FC00D9900ACEC1C /* Lexeme.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Lexeme.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -47,6 +52,7 @@ children = ( 84D4927E2FC00D9900ACEC1C /* Lexeme */, 84D4927D2FC00D9900ACEC1C /* Products */, + 847E4A872FDC4588007B4BFC /* lexeme.icon */, ); sourceTree = ""; }; @@ -122,6 +128,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 847E4A882FDC4588007B4BFC /* lexeme.icon in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -141,8 +148,9 @@ 84D4928B2FC00D9A00ACEC1C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = lexeme; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = Lexeme/Lexeme.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -153,9 +161,10 @@ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UILaunchStoryboardName = ""; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 26.6; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; + IPHONEOS_DEPLOYMENT_TARGET = 26.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -164,20 +173,25 @@ PRODUCT_BUNDLE_IDENTIFIER = black.harrison.Lexeme; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; 84D4928C2FC00D9A00ACEC1C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = lexeme; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = Lexeme/Lexeme.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -188,9 +202,10 @@ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UILaunchStoryboardName = ""; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 26.6; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; + IPHONEOS_DEPLOYMENT_TARGET = 26.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -199,12 +214,16 @@ PRODUCT_BUNDLE_IDENTIFIER = black.harrison.Lexeme; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; diff --git a/Lexeme/Lexeme.swift b/Lexeme/Lexeme.swift index 505e797..a81de18 100644 --- a/Lexeme/Lexeme.swift +++ b/Lexeme/Lexeme.swift @@ -1,11 +1,10 @@ -import SwiftData import SwiftUI @main struct Lexeme: App { var body: some Scene { WindowGroup { - DefinitionView() + LexemeView() } } } diff --git a/Lexeme/Model/DictionarySourceSetting.swift b/Lexeme/Model/DictionarySourceSetting.swift new file mode 100644 index 0000000..5fedf3a --- /dev/null +++ b/Lexeme/Model/DictionarySourceSetting.swift @@ -0,0 +1,41 @@ +import Foundation + +struct DictionarySourceSetting: Identifiable { + let id: String + let name: String + let detail: String + let requiresKey: Bool + var isEnabled: Bool + var apiKey: String + var keyWarning: String? + + static let defaults = [ + DictionarySourceSetting( + id: "dictionaryapi-dev", + name: "dictionaryapi.dev", + detail: "Free default source", + requiresKey: false, + isEnabled: true, + apiKey: "", + keyWarning: nil + ), + DictionarySourceSetting( + id: "merriam-webster", + name: "Merriam-Webster", + detail: "User-provided API key", + requiresKey: true, + isEnabled: false, + apiKey: "", + keyWarning: nil + ), + DictionarySourceSetting( + id: "oxford", + name: "Oxford", + detail: "User-provided API key", + requiresKey: true, + isEnabled: false, + apiKey: "", + keyWarning: nil + ) + ] +} diff --git a/Lexeme/Model/LookupCard.swift b/Lexeme/Model/LookupCard.swift new file mode 100644 index 0000000..4083364 --- /dev/null +++ b/Lexeme/Model/LookupCard.swift @@ -0,0 +1,20 @@ +import Foundation + +enum LookupCard: Identifiable, Equatable { + case definition(PlaceholderDefinition) + case notFound(String) + + var id: String { + switch self { + case .definition(let definition): "definition-\(definition.id)" + case .notFound(let word): "missing-\(word)" + } + } + + var lookupKey: String { + switch self { + case .definition(let definition): definition.id + case .notFound(let word): "missing-\(word)" + } + } +} diff --git a/Lexeme/Model/PlaceholderDefinition.swift b/Lexeme/Model/PlaceholderDefinition.swift new file mode 100644 index 0000000..130811b --- /dev/null +++ b/Lexeme/Model/PlaceholderDefinition.swift @@ -0,0 +1,100 @@ +import Foundation + +struct PlaceholderDefinition: Identifiable, Equatable { + let id: String + let word: String + let tags: [String] + let definition: String + let source: String + let rootWord: String? + + var shortDefinition: String { + String(definition.prefix(96)) + (definition.count > 96 ? "..." : "") + } + + init( + word: String, + tags: [String], + definition: String, + source: String, + rootWord: String? = nil + ) { + self.id = Self.normalizedKey(word) + self.word = word + self.tags = tags + self.definition = definition + self.source = source + self.rootWord = rootWord + } + + static func normalizedKey(_ value: String) -> String { + value + .trimmingCharacters(in: .whitespacesAndNewlines) + .trimmingCharacters(in: .punctuationCharacters) + .lowercased() + } + + static let samples = [ + PlaceholderDefinition( + word: "lexeme", + tags: ["Linguistics"], + definition: "A fundamental unit of meaning in a language, considered independently from the inflected forms it may take.", + source: "dictionaryapi.dev" + ), + PlaceholderDefinition( + word: "orthodoxy", + tags: ["Philosophy", "Religion"], + definition: "The quality or state of conforming to an accepted doctrine, especially in belief or practice.", + source: "Oxford Dictionary", + rootWord: "orthodox" + ), + PlaceholderDefinition( + word: "orthodox", + tags: ["Philosophy", "Religion"], + definition: "Conforming to what is generally or traditionally accepted as right or true; established and approved.", + source: "Oxford Dictionary" + ), + PlaceholderDefinition( + word: "aporia", + tags: ["Philosophy", "Rhetoric"], + definition: "An irresolvable internal contradiction or logical disjunction in a text, argument, or theory.", + source: "Merriam-Webster" + ), + PlaceholderDefinition( + word: "absurd", + tags: ["Philosophy", "Literature"], + definition: "A condition in which human beings search for meaning in a world that offers no final, rational answer.", + source: "dictionaryapi.dev" + ), + PlaceholderDefinition( + word: "nihilism", + tags: ["Philosophy"], + definition: "The doctrine or attitude that values, meaning, and truth lack objective foundation.", + source: "Oxford Dictionary" + ), + PlaceholderDefinition( + word: "dialectic", + tags: ["Philosophy"], + definition: "A method of reasoning that examines opposing ideas to expose tensions and move toward clearer understanding.", + source: "Merriam-Webster" + ), + PlaceholderDefinition( + word: "entropy", + tags: ["Physics"], + definition: "A measure of disorder, uncertainty, or unavailable energy within a system.", + source: "dictionaryapi.dev" + ), + PlaceholderDefinition( + word: "epistemology", + tags: ["Philosophy"], + definition: "The branch of philosophy concerned with knowledge, justification, belief, and the limits of understanding.", + source: "Oxford Dictionary" + ), + PlaceholderDefinition( + word: "ontology", + tags: ["Philosophy"], + definition: "The study of being, existence, and the categories by which reality is understood.", + source: "Merriam-Webster" + ) + ] +} diff --git a/Lexeme/View/Components/DefinitionCard.swift b/Lexeme/View/Components/DefinitionCard.swift new file mode 100644 index 0000000..c2ef5db --- /dev/null +++ b/Lexeme/View/Components/DefinitionCard.swift @@ -0,0 +1,71 @@ +import SwiftUI + +struct DefinitionCard: View { + let definition: PlaceholderDefinition + let isStarred: Bool + let onToggleStar: () -> Void + let onDefineRoot: (String) -> Void + + private var visibleRootWord: String? { + guard let rootWord = definition.rootWord else { return nil } + return PlaceholderDefinition.normalizedKey(rootWord) == definition.id ? nil : rootWord + } + + var body: some View { + VStack(alignment: .leading, spacing: 9) { + HStack(alignment: .firstTextBaseline) { + Text(definition.word) + .font(.system(size: 36, weight: .semibold, design: .serif)) + .lineLimit(2) + .minimumScaleFactor(0.82) + + Spacer(minLength: 12) + + Button(action: onToggleStar) { + Image(systemName: isStarred ? "star.fill" : "star") + .font(.system(size: 19, weight: .medium)) + .symbolRenderingMode(.hierarchical) + .foregroundStyle(isStarred ? .primary : .secondary) + .frame(width: 34, height: 34) + } + .buttonStyle(.plain) + .accessibilityLabel(isStarred ? "Unstar word" : "Star word") + } + + if !definition.tags.isEmpty { + HStack(spacing: 8) { + ForEach(definition.tags, id: \.self) { tag in + Text(tag.uppercased()) + .font(.caption2.weight(.semibold)) + .tracking(0.7) + .foregroundStyle(Color(red: 0.42, green: 0.51, blue: 0.62)) + } + } + } + + Text(definition.definition) + .font(.body) + .lineSpacing(2) + .foregroundStyle(.primary) + + if let visibleRootWord { + Button("Define \(visibleRootWord)") { + onDefineRoot(visibleRootWord) + } + .buttonStyle(.bordered) + .controlSize(.small) + .padding(.top, 2) + } + + Text("Source: \(definition.source)") + .font(.caption2) + .foregroundStyle(.secondary) + .padding(.top, 2) + } + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 22, style: .continuous)) + .glassEffect(.regular, in: RoundedRectangle(cornerRadius: 22, style: .continuous)) + .shadow(color: .black.opacity(0.12), radius: 18, y: 8) + } +} diff --git a/Lexeme/View/Components/DefinitionRow.swift b/Lexeme/View/Components/DefinitionRow.swift new file mode 100644 index 0000000..37e5735 --- /dev/null +++ b/Lexeme/View/Components/DefinitionRow.swift @@ -0,0 +1,20 @@ +import SwiftUI + +struct DefinitionRow: View { + let word: String + let definition: String + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(word) + .font(.system(size: 18, weight: .semibold, design: .serif)) + .foregroundStyle(.primary) + + Text(definition) + .font(.subheadline) + .foregroundStyle(.secondary) + .lineLimit(2) + } + .padding(.vertical, 4) + } +} diff --git a/Lexeme/View/Components/NoDefinitionCard.swift b/Lexeme/View/Components/NoDefinitionCard.swift new file mode 100644 index 0000000..900ba57 --- /dev/null +++ b/Lexeme/View/Components/NoDefinitionCard.swift @@ -0,0 +1,21 @@ +import SwiftUI + +struct NoDefinitionCard: View { + let word: String + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("No definition found") + .font(.system(size: 24, weight: .semibold, design: .serif)) + + Text("No definition found for: \"\(word)\"") + .font(.subheadline) + .foregroundStyle(.secondary) + } + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 22, style: .continuous)) + .glassEffect(.regular, in: RoundedRectangle(cornerRadius: 22, style: .continuous)) + .shadow(color: .black.opacity(0.12), radius: 18, y: 8) + } +} diff --git a/Lexeme/View/Components/SearchCommandField.swift b/Lexeme/View/Components/SearchCommandField.swift new file mode 100644 index 0000000..1f369fd --- /dev/null +++ b/Lexeme/View/Components/SearchCommandField.swift @@ -0,0 +1,29 @@ +import SwiftUI + +struct SearchCommandField: View { + @Binding var text: String + + let isFocused: FocusState.Binding + let onSubmit: () -> Void + + var body: some View { + HStack(spacing: 8) { + Image(systemName: "magnifyingglass") + .font(.system(size: 17, weight: .medium)) + .symbolRenderingMode(.hierarchical) + .foregroundStyle(.secondary) + + TextField("Define a lexeme", text: $text) + .focused(isFocused) + .submitLabel(.search) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .font(.title3.weight(.medium)) + .onSubmit(onSubmit) + } + .padding(.horizontal, 16) + .frame(height: 56) + .background(.ultraThinMaterial, in: Capsule()) + .glassEffect(.regular, in: Capsule()) + } +} diff --git a/Lexeme/View/Components/SourceSettingRow.swift b/Lexeme/View/Components/SourceSettingRow.swift new file mode 100644 index 0000000..e2fd4cf --- /dev/null +++ b/Lexeme/View/Components/SourceSettingRow.swift @@ -0,0 +1,57 @@ +import SwiftUI + +struct SourceSettingRow: View { + @Binding var source: DictionarySourceSetting + + private var hasRequiredKey: Bool { + !source.requiresKey || !source.apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + HStack(alignment: .top, spacing: 12) { + VStack(alignment: .leading, spacing: 3) { + Text(source.name) + .font(.body.weight(.medium)) + + Text(source.detail) + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + Toggle("Enabled", isOn: $source.isEnabled) + .labelsHidden() + .disabled(!hasRequiredKey) + } + + if source.requiresKey { + SecureField("API key", text: $source.apiKey) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + + HStack(spacing: 10) { + Button("Test API Key") { + source.keyWarning = hasRequiredKey ? nil : "API key required" + } + .buttonStyle(.borderless) + + if let keyWarning = source.keyWarning { + Text(keyWarning) + .font(.caption) + .foregroundStyle(.orange) + } + } + } + } + .padding(.vertical, 4) + .onChange(of: source.apiKey) { _, newValue in + if newValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + source.isEnabled = false + } + + source.keyWarning = nil + } + } +} diff --git a/Lexeme/View/LexemeView.swift b/Lexeme/View/LexemeView.swift new file mode 100644 index 0000000..5153d24 --- /dev/null +++ b/Lexeme/View/LexemeView.swift @@ -0,0 +1,187 @@ +import SwiftUI +import UIKit + +struct LexemeView: View { + @FocusState private var isSearchFocused: Bool + + @State private var searchText = "" + @State private var displayedCards: [LookupCard] = [] + @State private var starredDefinitions: [String: PlaceholderDefinition] = [:] + @State private var selectedTab = MainTab.home + @State private var stackLimit = 20 + @State private var cacheRetention = "30 days" + + private let definitions = PlaceholderDefinition.samples + + private var definitionIndex: [String: PlaceholderDefinition] { + Dictionary(uniqueKeysWithValues: definitions.map { ($0.id, $0) }) + } + + var body: some View { + TabView(selection: $selectedTab) { + ZStack { + Color(uiColor: .systemGroupedBackground) + .ignoresSafeArea() + + mainContent + } + .tabItem { + Label("Home", systemImage: "house") + .labelStyle(.iconOnly) + } + .tag(MainTab.home) + + NavigationStack { + StarredListView( + definitions: starredDefinitions.values.sorted { $0.word < $1.word }, + onSelect: loadStarredDefinition + ) + } + .tabItem { + Label("Saved", systemImage: "star") + .labelStyle(.iconOnly) + } + .tag(MainTab.saved) + + NavigationStack { + SettingsView(stackLimit: $stackLimit, cacheRetention: $cacheRetention) + } + .tabItem { + Label("Settings", systemImage: "gearshape") + .labelStyle(.iconOnly) + } + .tag(MainTab.settings) + } + .tint(Color(red: 0.42, green: 0.51, blue: 0.62)) + .preferredColorScheme(.dark) + .onAppear { + isSearchFocused = true + } + .onChange(of: selectedTab) { _, _ in + dismissKeyboard() + } + } + + private var mainContent: some View { + VStack(spacing: 14) { + SearchCommandField( + text: $searchText, + isFocused: $isSearchFocused, + onSubmit: submitSearch + ) + .padding(.horizontal, 16) + + if displayedCards.isEmpty { + emptyGuidance + .transition(.opacity) + } else { + resultStack + .transition(.opacity) + } + } + .padding(.top, 10) + .animation(.smooth(duration: 0.24), value: displayedCards.isEmpty) + } + + private var emptyGuidance: some View { + Color.clear + .frame(maxWidth: .infinity, maxHeight: .infinity) + .contentShape(Rectangle()) + .onTapGesture { + dismissKeyboard() + } + } + + private var resultStack: some View { + ScrollView { + LazyVStack(spacing: 14) { + ForEach(displayedCards) { card in + switch card { + case .definition(let definition): + DefinitionCard( + definition: definition, + isStarred: starredDefinitions[definition.id] != nil, + onToggleStar: { toggleStar(definition) }, + onDefineRoot: { root in replace(definition, withRoot: root) } + ) + .transition(.opacity) + case .notFound(let word): + NoDefinitionCard(word: word) + .transition(.opacity) + } + } + } + .padding(.horizontal, 16) + .padding(.bottom, 28) + } + .scrollDismissesKeyboard(.interactively) + .simultaneousGesture( + TapGesture().onEnded { + dismissKeyboard() + } + ) + } + + private func submitSearch() { + let normalized = PlaceholderDefinition.normalizedKey(searchText) + guard !normalized.isEmpty else { return } + + searchText = "" + + if let definition = definitionIndex[normalized] { + show(.definition(definition)) + impact(.soft) + } else { + show(.notFound(normalized)) + } + } + + private func show(_ card: LookupCard) { + withAnimation(.smooth(duration: 0.24)) { + displayedCards.removeAll { $0.lookupKey == card.lookupKey } + displayedCards.insert(card, at: 0) + + if displayedCards.count > stackLimit { + displayedCards = Array(displayedCards.prefix(stackLimit)) + } + } + } + + private func toggleStar(_ definition: PlaceholderDefinition) { + if starredDefinitions[definition.id] == nil { + starredDefinitions[definition.id] = definition + } else { + starredDefinitions.removeValue(forKey: definition.id) + } + + impact(.light) + } + + private func replace(_ definition: PlaceholderDefinition, withRoot root: String) { + guard let rootDefinition = definitionIndex[PlaceholderDefinition.normalizedKey(root)] else { return } + let replacement = LookupCard.definition(rootDefinition) + + displayedCards.removeAll { $0.lookupKey == rootDefinition.id } + + if let index = displayedCards.firstIndex(where: { $0.lookupKey == definition.id }) { + displayedCards[index] = replacement + } else { + show(replacement) + } + + impact(.soft) + } + + private func loadStarredDefinition(_ definition: PlaceholderDefinition) { + selectedTab = .home + show(.definition(definition)) + } + + private func dismissKeyboard() { + isSearchFocused = false + } + + private func impact(_ style: UIImpactFeedbackGenerator.FeedbackStyle) { + UIImpactFeedbackGenerator(style: style).impactOccurred() + } +} diff --git a/Lexeme/View/MainTab.swift b/Lexeme/View/MainTab.swift new file mode 100644 index 0000000..c25080d --- /dev/null +++ b/Lexeme/View/MainTab.swift @@ -0,0 +1,7 @@ +import Foundation + +enum MainTab: Hashable { + case home + case saved + case settings +} diff --git a/Lexeme/View/Screens/SettingsView.swift b/Lexeme/View/Screens/SettingsView.swift new file mode 100644 index 0000000..ce9a891 --- /dev/null +++ b/Lexeme/View/Screens/SettingsView.swift @@ -0,0 +1,56 @@ +import SwiftUI + +struct SettingsView: View { + @Binding var stackLimit: Int + @Binding var cacheRetention: String + + @State private var sources = DictionarySourceSetting.defaults + + var body: some View { + List { + Section { + ForEach($sources) { $source in + SourceSettingRow(source: $source) + } + .onMove { indices, newOffset in + sources.move(fromOffsets: indices, toOffset: newOffset) + } + } header: { + Text("Sources") + } footer: { + Text("Sources are tried in order. Keyed sources stay disabled until an API key exists.") + } + + Section { + Picker("Retention", selection: $cacheRetention) { + ForEach(["7 days", "30 days", "90 days", "Forever"], id: \.self) { value in + Text(value).tag(value) + } + } + + Picker("Visible stack", selection: $stackLimit) { + ForEach([5, 10, 20, 50], id: \.self) { value in + Text("\(value)").tag(value) + } + } + } header: { + Text("Cache") + } + + Section { + Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis dapibus posuere velit aliquet.") + .foregroundStyle(.secondary) + } header: { + Text("Privacy") + } + + Section { + LabeledContent("Version", value: "1.0") + } + } + .navigationTitle("Settings") + .toolbar { + EditButton() + } + } +} diff --git a/Lexeme/View/Screens/StarredListView.swift b/Lexeme/View/Screens/StarredListView.swift new file mode 100644 index 0000000..cfafff2 --- /dev/null +++ b/Lexeme/View/Screens/StarredListView.swift @@ -0,0 +1,28 @@ +import SwiftUI + +struct StarredListView: View { + let definitions: [PlaceholderDefinition] + let onSelect: (PlaceholderDefinition) -> Void + + var body: some View { + List { + if definitions.isEmpty { + ContentUnavailableView( + "No Starred Words", + systemImage: "star", + description: Text("Star a definition to keep it here.") + ) + } else { + ForEach(definitions) { definition in + Button { + onSelect(definition) + } label: { + DefinitionRow(word: definition.word, definition: definition.shortDefinition) + } + .buttonStyle(.plain) + } + } + } + .navigationTitle("Starred") + } +} diff --git a/Lexeme/ViewModel/DefinitionView.swift b/Lexeme/ViewModel/DefinitionView.swift deleted file mode 100644 index 910f36b..0000000 --- a/Lexeme/ViewModel/DefinitionView.swift +++ /dev/null @@ -1,12 +0,0 @@ -import SwiftUI - -struct DefinitionView: View { - var body: some View { - VStack { - Text("Definition") - .font(.title) - .fontWeight(.bold) - .padding(.top, 20) - } - } -} diff --git a/lexeme.icon/Assets/book.pages.svg b/lexeme.icon/Assets/book.pages.svg new file mode 100644 index 0000000..cb77f77 --- /dev/null +++ b/lexeme.icon/Assets/book.pages.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/lexeme.icon/icon.json b/lexeme.icon/icon.json new file mode 100644 index 0000000..bd5902b --- /dev/null +++ b/lexeme.icon/icon.json @@ -0,0 +1,38 @@ +{ + "fill" : { + "automatic-gradient" : "extended-srgb:0.00000,0.53333,1.00000,1.00000" + }, + "groups" : [ + { + "layers" : [ + { + "glass" : true, + "hidden" : false, + "image-name" : "book.pages.svg", + "name" : "book.pages", + "position" : { + "scale" : 2, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "circles" : [ + "watchOS" + ], + "squares" : "shared" + } +} \ No newline at end of file