diff --git a/freewrite.xcodeproj/project.pbxproj.backup b/freewrite.xcodeproj/project.pbxproj.backup new file mode 100644 index 0000000..9282e2a --- /dev/null +++ b/freewrite.xcodeproj/project.pbxproj.backup @@ -0,0 +1,609 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 2800F7412DA1CEA0008FE5F9 /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = 2800F7402DA1CE9A008FE5F9 /* .gitignore */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 2857D4822D5F8A8100EE1012 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2857D4682D5F8A8000EE1012 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2857D46F2D5F8A8000EE1012; + remoteInfo = freewrite; + }; + 2857D48C2D5F8A8100EE1012 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2857D4682D5F8A8000EE1012 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2857D46F2D5F8A8000EE1012; + remoteInfo = freewrite; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 2800F7402DA1CE9A008FE5F9 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; + 2857D4702D5F8A8000EE1012 /* freewrite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = freewrite.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2857D4812D5F8A8100EE1012 /* freewriteTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = freewriteTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2857D48B2D5F8A8100EE1012 /* freewriteUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = freewriteUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 2857D4722D5F8A8000EE1012 /* freewrite */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = freewrite; + sourceTree = ""; + }; + 2857D4842D5F8A8100EE1012 /* freewriteTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = freewriteTests; + sourceTree = ""; + }; + 2857D48E2D5F8A8100EE1012 /* freewriteUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = freewriteUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2857D46D2D5F8A8000EE1012 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2857D47E2D5F8A8100EE1012 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2857D4882D5F8A8100EE1012 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2857D4672D5F8A8000EE1012 = { + isa = PBXGroup; + children = ( + 2800F7402DA1CE9A008FE5F9 /* .gitignore */, + 2857D4722D5F8A8000EE1012 /* freewrite */, + 2857D4842D5F8A8100EE1012 /* freewriteTests */, + 2857D48E2D5F8A8100EE1012 /* freewriteUITests */, + 2857D4712D5F8A8000EE1012 /* Products */, + ); + sourceTree = ""; + }; + 2857D4712D5F8A8000EE1012 /* Products */ = { + isa = PBXGroup; + children = ( + 2857D4702D5F8A8000EE1012 /* freewrite.app */, + 2857D4812D5F8A8100EE1012 /* freewriteTests.xctest */, + 2857D48B2D5F8A8100EE1012 /* freewriteUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 2857D46F2D5F8A8000EE1012 /* freewrite */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2857D4952D5F8A8100EE1012 /* Build configuration list for PBXNativeTarget "freewrite" */; + buildPhases = ( + 2857D46C2D5F8A8000EE1012 /* Sources */, + 2857D46D2D5F8A8000EE1012 /* Frameworks */, + 2857D46E2D5F8A8000EE1012 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 2857D4722D5F8A8000EE1012 /* freewrite */, + ); + name = freewrite; + packageProductDependencies = ( + ); + productName = freewrite; + productReference = 2857D4702D5F8A8000EE1012 /* freewrite.app */; + productType = "com.apple.product-type.application"; + }; + 2857D4802D5F8A8100EE1012 /* freewriteTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2857D4982D5F8A8100EE1012 /* Build configuration list for PBXNativeTarget "freewriteTests" */; + buildPhases = ( + 2857D47D2D5F8A8100EE1012 /* Sources */, + 2857D47E2D5F8A8100EE1012 /* Frameworks */, + 2857D47F2D5F8A8100EE1012 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2857D4832D5F8A8100EE1012 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 2857D4842D5F8A8100EE1012 /* freewriteTests */, + ); + name = freewriteTests; + packageProductDependencies = ( + ); + productName = freewriteTests; + productReference = 2857D4812D5F8A8100EE1012 /* freewriteTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 2857D48A2D5F8A8100EE1012 /* freewriteUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2857D49B2D5F8A8100EE1012 /* Build configuration list for PBXNativeTarget "freewriteUITests" */; + buildPhases = ( + 2857D4872D5F8A8100EE1012 /* Sources */, + 2857D4882D5F8A8100EE1012 /* Frameworks */, + 2857D4892D5F8A8100EE1012 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2857D48D2D5F8A8100EE1012 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 2857D48E2D5F8A8100EE1012 /* freewriteUITests */, + ); + name = freewriteUITests; + packageProductDependencies = ( + ); + productName = freewriteUITests; + productReference = 2857D48B2D5F8A8100EE1012 /* freewriteUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 2857D4682D5F8A8000EE1012 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1610; + LastUpgradeCheck = 1610; + TargetAttributes = { + 2857D46F2D5F8A8000EE1012 = { + CreatedOnToolsVersion = 16.1; + }; + 2857D4802D5F8A8100EE1012 = { + CreatedOnToolsVersion = 16.1; + TestTargetID = 2857D46F2D5F8A8000EE1012; + }; + 2857D48A2D5F8A8100EE1012 = { + CreatedOnToolsVersion = 16.1; + TestTargetID = 2857D46F2D5F8A8000EE1012; + }; + }; + }; + buildConfigurationList = 2857D46B2D5F8A8000EE1012 /* Build configuration list for PBXProject "freewrite" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 2857D4672D5F8A8000EE1012; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 2857D4712D5F8A8000EE1012 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2857D46F2D5F8A8000EE1012 /* freewrite */, + 2857D4802D5F8A8100EE1012 /* freewriteTests */, + 2857D48A2D5F8A8100EE1012 /* freewriteUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 2857D46E2D5F8A8000EE1012 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2800F7412DA1CEA0008FE5F9 /* .gitignore in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2857D47F2D5F8A8100EE1012 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2857D4892D5F8A8100EE1012 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 2857D46C2D5F8A8000EE1012 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2857D47D2D5F8A8100EE1012 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2857D4872D5F8A8100EE1012 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 2857D4832D5F8A8100EE1012 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2857D46F2D5F8A8000EE1012 /* freewrite */; + targetProxy = 2857D4822D5F8A8100EE1012 /* PBXContainerItemProxy */; + }; + 2857D48D2D5F8A8100EE1012 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2857D46F2D5F8A8000EE1012 /* freewrite */; + targetProxy = 2857D48C2D5F8A8100EE1012 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 2857D4932D5F8A8100EE1012 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 2857D4942D5F8A8100EE1012 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + 2857D4962D5F8A8100EE1012 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = ""; + "ASSETCATALOG_COMPILER_APPICON_NAME[sdk=macosx*]" = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; + CODE_SIGN_ENTITLEMENTS = freewrite/freewrite.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"freewrite/Preview Content\""; + DEVELOPMENT_TEAM = F5B7Y5BAYG; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 13.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = app.humansongs.freewrite; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = macosx; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + XROS_DEPLOYMENT_TARGET = 2.1; + }; + name = Debug; + }; + 2857D4972D5F8A8100EE1012 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = ""; + "ASSETCATALOG_COMPILER_APPICON_NAME[sdk=macosx*]" = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; + CODE_SIGN_ENTITLEMENTS = freewrite/freewrite.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"freewrite/Preview Content\""; + DEVELOPMENT_TEAM = F5B7Y5BAYG; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 13.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = app.humansongs.freewrite; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = macosx; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + XROS_DEPLOYMENT_TARGET = 2.1; + }; + name = Release; + }; + 2857D4992D5F8A8100EE1012 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6D7X9GGZAW; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + MACOSX_DEPLOYMENT_TARGET = 15.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = humansongs.freewriteTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/freewrite.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/freewrite"; + XROS_DEPLOYMENT_TARGET = 2.1; + }; + name = Debug; + }; + 2857D49A2D5F8A8100EE1012 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6D7X9GGZAW; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + MACOSX_DEPLOYMENT_TARGET = 15.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = humansongs.freewriteTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/freewrite.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/freewrite"; + XROS_DEPLOYMENT_TARGET = 2.1; + }; + name = Release; + }; + 2857D49C2D5F8A8100EE1012 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6D7X9GGZAW; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + MACOSX_DEPLOYMENT_TARGET = 15.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = humansongs.freewriteUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_TARGET_NAME = freewrite; + XROS_DEPLOYMENT_TARGET = 2.1; + }; + name = Debug; + }; + 2857D49D2D5F8A8100EE1012 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6D7X9GGZAW; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + MACOSX_DEPLOYMENT_TARGET = 15.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = humansongs.freewriteUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_TARGET_NAME = freewrite; + XROS_DEPLOYMENT_TARGET = 2.1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2857D46B2D5F8A8000EE1012 /* Build configuration list for PBXProject "freewrite" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2857D4932D5F8A8100EE1012 /* Debug */, + 2857D4942D5F8A8100EE1012 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2857D4952D5F8A8100EE1012 /* Build configuration list for PBXNativeTarget "freewrite" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2857D4962D5F8A8100EE1012 /* Debug */, + 2857D4972D5F8A8100EE1012 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2857D4982D5F8A8100EE1012 /* Build configuration list for PBXNativeTarget "freewriteTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2857D4992D5F8A8100EE1012 /* Debug */, + 2857D49A2D5F8A8100EE1012 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2857D49B2D5F8A8100EE1012 /* Build configuration list for PBXNativeTarget "freewriteUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2857D49C2D5F8A8100EE1012 /* Debug */, + 2857D49D2D5F8A8100EE1012 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 2857D4682D5F8A8000EE1012 /* Project object */; +} diff --git a/freewrite/AudioManager.swift b/freewrite/AudioManager.swift new file mode 100644 index 0000000..73be19a --- /dev/null +++ b/freewrite/AudioManager.swift @@ -0,0 +1,86 @@ +import AVFoundation + +struct KeySound { + let startTime: Double + let duration: Double +} + +class AudioManager { + static let shared = AudioManager() + private var soundData: Data? + private var keySoundMap: [String: KeySound] = [:] + private(set) var isEnabled: Bool = false + + private init() { + setupKeyboardSound() + loadKeyDefinitions() + } + + private func setupKeyboardSound() { + if let soundURL = Bundle.main.url(forResource: "crystal_purple", withExtension: "mp3") { + loadSound(from: soundURL) + } else { + print("Failed to load crystal_purple.mp3") + } + } + + private func loadSound(from url: URL) -> Void { + do { + soundData = try Data(contentsOf: url) + } catch {} + } + + private func loadKeyDefinitions() { + if let configURL = Bundle.main.url(forResource: "crystal_purple_config", withExtension: "json") { + loadConfig(from: configURL) + } else { + print("Failed to load crystal_purple_config.json") + } + } + + private func loadConfig(from url: URL) { + do { + let data = try Data(contentsOf: url) + let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] + + if let defines = json?["defines"] as? [String: [Double]] { + for (key, value) in defines { + if value.count == 2 { + let startTime = value[0] / 1000.0 + let duration = value[1] / 1000.0 + keySoundMap[key] = KeySound(startTime: startTime, duration: duration) + } + } + print("Successfully loaded \(keySoundMap.count) key sounds") + } else { + print("Failed to parse defines from config") + } + } catch { + print("Error loading config: \(error)") + } + } + + func toggleSound() { + isEnabled.toggle() + } + + func playKeyboardSound(forKey key: String = "1") { + guard isEnabled, + let soundData = soundData, + let player = try? AVAudioPlayer(data: soundData) else { + return + } + + let keySound = keySoundMap[key] ?? keySoundMap["1"] + if let soundInfo = keySound { + player.enableRate = true + player.volume = 0.5 + player.currentTime = soundInfo.startTime + player.play() + + DispatchQueue.main.asyncAfter(deadline: .now() + soundInfo.duration) { + player.stop() + } + } + } +} diff --git a/freewrite/ContentView.swift b/freewrite/ContentView.swift index cc4113a..bf213a1 100644 --- a/freewrite/ContentView.swift +++ b/freewrite/ContentView.swift @@ -8,6 +8,7 @@ import SwiftUI import AppKit +import AVFoundation import UniformTypeIdentifiers import PDFKit @@ -49,7 +50,7 @@ struct ContentView: View { @State private var text: String = "" // Remove initial welcome text since we'll handle it in createNewEntry @State private var isFullscreen = false - @State private var selectedFont: String = "Lato-Regular" + @State private var selectedFont: String = UserDefaults.standard.string(forKey: "selectedFont") ?? "Lato-Regular" @State private var currentRandomFont: String = "" @State private var timeRemaining: Int = 900 // Changed to 900 seconds (15 minutes) @State private var timerIsRunning = false @@ -57,7 +58,7 @@ struct ContentView: View { @State private var isHoveringFullscreen = false @State private var hoveredFont: String? = nil @State private var isHoveringSize = false - @State private var fontSize: CGFloat = 18 + @State private var fontSize: CGFloat = UserDefaults.standard.float(forKey: "fontSize") > 0 ? CGFloat(UserDefaults.standard.float(forKey: "fontSize")) : 18 @State private var blinkCount = 0 @State private var isBlinking = false @State private var opacity: Double = 1.0 @@ -82,6 +83,11 @@ struct ContentView: View { @State private var isHoveringHistoryText = false @State private var isHoveringHistoryPath = false @State private var isHoveringHistoryArrow = false + @State private var showWordCount = false + @State private var wordCount = 0 + @State private var characterCount = 0 + @State private var isHoveringSound = false + @State private var isSoundEnabled = false @State private var colorScheme: ColorScheme = .light // Add state for color scheme @State private var isHoveringThemeToggle = false // Add state for theme toggle hover let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @@ -396,6 +402,18 @@ struct ContentView: View { TextEditor(text: Binding( get: { text }, set: { newValue in + // Play keyboard sound when text changes + if newValue.count != text.count { + // Get the key that was pressed + if let event = NSApp.currentEvent, event.type == .keyDown { + let keyCode = String(event.keyCode) + AudioManager.shared.playKeyboardSound(forKey: keyCode) + } else { + // Fallback to default sound if we can't determine the key + AudioManager.shared.playKeyboardSound() + } + } + // Ensure the text always starts with two newlines if !newValue.hasPrefix("\n\n") { text = "\n\n" + newValue.trimmingCharacters(in: .newlines) @@ -442,6 +460,7 @@ struct ContentView: View { if let currentIndex = fontSizes.firstIndex(of: fontSize) { let nextIndex = (currentIndex + 1) % fontSizes.count fontSize = fontSizes[nextIndex] + UserDefaults.standard.set(Float(fontSize), forKey: "fontSize") } } .buttonStyle(.plain) @@ -462,6 +481,7 @@ struct ContentView: View { Button("Lato") { selectedFont = "Lato-Regular" currentRandomFont = "" + UserDefaults.standard.set("Lato-Regular", forKey: "selectedFont") } .buttonStyle(.plain) .foregroundColor(hoveredFont == "Lato" ? textHoverColor : textColor) @@ -481,6 +501,7 @@ struct ContentView: View { Button("Arial") { selectedFont = "Arial" currentRandomFont = "" + UserDefaults.standard.set("Arial", forKey: "selectedFont") } .buttonStyle(.plain) .foregroundColor(hoveredFont == "Arial" ? textHoverColor : textColor) @@ -500,6 +521,7 @@ struct ContentView: View { Button("System") { selectedFont = ".AppleSystemUIFont" currentRandomFont = "" + UserDefaults.standard.set(".AppleSystemUIFont", forKey: "selectedFont") } .buttonStyle(.plain) .foregroundColor(hoveredFont == "System" ? textHoverColor : textColor) @@ -519,6 +541,7 @@ struct ContentView: View { Button("Serif") { selectedFont = "Times New Roman" currentRandomFont = "" + UserDefaults.standard.set("Times New Roman", forKey: "selectedFont") } .buttonStyle(.plain) .foregroundColor(hoveredFont == "Serif" ? textHoverColor : textColor) @@ -539,6 +562,7 @@ struct ContentView: View { if let randomFont = availableFonts.randomElement() { selectedFont = randomFont currentRandomFont = randomFont + UserDefaults.standard.set(randomFont, forKey: "selectedFont") } } .buttonStyle(.plain) @@ -563,6 +587,28 @@ struct ContentView: View { // Utility buttons (moved to right) HStack(spacing: 8) { + Button(action: { + AudioManager.shared.toggleSound() + isSoundEnabled.toggle() // Update state immediately + }) { + Image(systemName: isSoundEnabled ? "speaker.wave.2.fill" : "speaker.slash.fill") + .frame(width: 20, height: 16) // Fixed frame size for both icons + } + .buttonStyle(.plain) + .foregroundColor(isHoveringSound ? .black : .gray) + .onHover { hovering in + isHoveringSound = hovering + isHoveringBottomNav = hovering + if hovering { + NSCursor.pointingHand.push() + } else { + NSCursor.pop() + } + } + + Text("•") + .foregroundColor(.gray) + Button(timerButtonTitle) { let now = Date() if let lastClick = lastClickTime, @@ -572,6 +618,16 @@ struct ContentView: View { lastClickTime = nil } else { timerIsRunning.toggle() + // Hide word count when starting a new timer session + if timerIsRunning { + showWordCount = false + // Ensure bottom nav fades out when starting a new session + if !isHoveringBottomNav { + withAnimation(.easeIn(duration: 1.0)) { + bottomNavOpacity = 0.0 + } + } + } lastClickTime = now } } @@ -701,14 +757,29 @@ struct ContentView: View { Text("•") .foregroundColor(.gray) + // Word and character count display + if showWordCount { + Text("\(wordCount) words | \(characterCount) chars") + .font(.system(size: 13)) + .foregroundColor(.gray) + .padding(.horizontal, 4) + } + + Text("•") + .foregroundColor(.gray) + .opacity(showWordCount ? 1.0 : 0.0) + Button(action: { + // Hide word count when creating a new entry + showWordCount = false createNewEntry() }) { Text("New Entry") .font(.system(size: 13)) } .buttonStyle(.plain) - .foregroundColor(isHoveringNewEntry ? textHoverColor : textColor) + .foregroundColor(timerIsRunning ? Color.gray.opacity(0.5) : (isHoveringNewEntry ? textHoverColor : textColor)) + .disabled(timerIsRunning) // Disable the button while timer is running .onHover { hovering in isHoveringNewEntry = hovering isHoveringBottomNav = hovering @@ -956,6 +1027,11 @@ struct ContentView: View { timeRemaining -= 1 } else if timeRemaining == 0 { timerIsRunning = false + // Calculate word and character count when timer ends + calculateWordAndCharacterCount() + showWordCount = true + // Reset timeRemaining for the next session + timeRemaining = 900 if !isHoveringBottomNav { withAnimation(.easeOut(duration: 1.0)) { bottomNavOpacity = 1.0 @@ -1029,6 +1105,9 @@ struct ContentView: View { } private func createNewEntry() { + // Hide word count when creating a new entry + showWordCount = false + let newEntry = HumanEntry.createNew() entries.insert(newEntry, at: 0) // Add to the beginning selectedEntryId = newEntry.id @@ -1074,6 +1153,18 @@ struct ContentView: View { } } + // Calculate word and character count from the current text + private func calculateWordAndCharacterCount() { + let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines) + + // Calculate character count (excluding whitespace) + characterCount = trimmedText.count + + // Calculate word count + let components = trimmedText.components(separatedBy: .whitespacesAndNewlines) + wordCount = components.filter { !$0.isEmpty }.count + } + private func deleteEntry(entry: HumanEntry) { // Delete the file from the filesystem let documentsDirectory = getDocumentsDirectory() @@ -1305,4 +1396,4 @@ extension NSView { #Preview { ContentView() -} \ No newline at end of file +} diff --git a/freewrite/CustomTextEditor.swift b/freewrite/CustomTextEditor.swift new file mode 100644 index 0000000..5219c53 --- /dev/null +++ b/freewrite/CustomTextEditor.swift @@ -0,0 +1,74 @@ +import SwiftUI +import AppKit + +struct CustomTextEditor: NSViewRepresentable { + @Binding var text: String + var font: NSFont + var textColor: NSColor + var backgroundColor: NSColor + + class Coordinator: NSObject, NSTextViewDelegate { + var parent: CustomTextEditor + + init(_ parent: CustomTextEditor) { + self.parent = parent + } + + func textDidChange(_ notification: Notification) { + if let textView = notification.object as? NSTextView { + parent.text = textView.string + } + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeNSView(context: Context) -> NSScrollView { + let textView = CustomNSTextView() + textView.delegate = context.coordinator + textView.font = font + textView.textColor = textColor + textView.backgroundColor = backgroundColor + textView.string = text + textView.isRichText = false + textView.isEditable = true + textView.isSelectable = true + textView.allowsUndo = true + textView.drawsBackground = true + textView.autoresizingMask = [.width, .height] + textView.minSize = NSSize(width: 0, height: 0) + textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) + textView.textContainer?.widthTracksTextView = true + textView.textContainerInset = NSSize(width: 8, height: 8) + + let scrollView = NSScrollView() + scrollView.documentView = textView + scrollView.hasVerticalScroller = true + scrollView.hasHorizontalScroller = false + scrollView.autohidesScrollers = true + scrollView.backgroundColor = backgroundColor + return scrollView + } + + func updateNSView(_ nsView: NSScrollView, context: Context) { + if let textView = nsView.documentView as? NSTextView { + if textView.string != text { + textView.string = text + } + textView.font = font + textView.textColor = textColor + textView.backgroundColor = backgroundColor + } + } +} + +class CustomNSTextView: NSTextView { + override func keyDown(with event: NSEvent) { + super.keyDown(with: event) + if let chars = event.characters, !chars.isEmpty { + AudioManager.shared.playKeyboardSound(forKey: String(chars.first!)) + } + } +} diff --git a/freewrite/Sounds/crystal_purple.mp3 b/freewrite/Sounds/crystal_purple.mp3 new file mode 100644 index 0000000..2042a3d Binary files /dev/null and b/freewrite/Sounds/crystal_purple.mp3 differ diff --git a/freewrite/Sounds/crystal_purple_config.json b/freewrite/Sounds/crystal_purple_config.json new file mode 100644 index 0000000..0e5b6d6 --- /dev/null +++ b/freewrite/Sounds/crystal_purple_config.json @@ -0,0 +1,469 @@ +{ + "id": "sound-pack-1200000000009", + "name": "EG Crystal Purple", + "default": true, + "key_define_type": "single", + "includes_numpad": false, + "sound": "purple.ogg", + "defines": { + "1": [ + 1088, + 118 + ], + "2": [ + 1839, + 140 + ], + "3": [ + 2207, + 143 + ], + "4": [ + 2507, + 143 + ], + "5": [ + 2809, + 148 + ], + "6": [ + 3099, + 141 + ], + "7": [ + 3408, + 134 + ], + "8": [ + 3716, + 147 + ], + "9": [ + 4017, + 139 + ], + "10": [ + 4352, + 144 + ], + "11": [ + 4657, + 126 + ], + "12": [ + 4946, + 134 + ], + "13": [ + 5250, + 133 + ], + "14": [ + 5566, + 143 + ], + "15": [ + 6792, + 157 + ], + "16": [ + 7080, + 142 + ], + "17": [ + 7367, + 134 + ], + "18": [ + 7680, + 130 + ], + "19": [ + 7973, + 133 + ], + "20": [ + 8259, + 134 + ], + "21": [ + 8549, + 135 + ], + "22": [ + 8838, + 135 + ], + "23": [ + 9103, + 150 + ], + "24": [ + 9405, + 132 + ], + "25": [ + 9703, + 131 + ], + "26": [ + 10015, + 144 + ], + "27": [ + 10397, + 112 + ], + "28": [ + 15361, + 130 + ], + "29": [ + 21408, + 128 + ], + "30": [ + 12061, + 129 + ], + "31": [ + 12339, + 121 + ], + "32": [ + 12632, + 127 + ], + "33": [ + 12916, + 119 + ], + "34": [ + 13227, + 121 + ], + "35": [ + 13526, + 124 + ], + "36": [ + 13819, + 131 + ], + "37": [ + 14115, + 113 + ], + "38": [ + 14406, + 123 + ], + "39": [ + 14711, + 128 + ], + "40": [ + 15022, + 133 + ], + "41": [ + 1088, + 118 + ], + "42": [ + 16600, + 136 + ], + "43": [ + 10718, + 127 + ], + "44": [ + 16879, + 128 + ], + "45": [ + 17196, + 132 + ], + "46": [ + 17503, + 113 + ], + "47": [ + 17788, + 115 + ], + "48": [ + 18069, + 117 + ], + "49": [ + 18349, + 130 + ], + "50": [ + 18653, + 123 + ], + "51": [ + 18964, + 119 + ], + "52": [ + 19282, + 124 + ], + "53": [ + 19594, + 120 + ], + "54": [ + 19966, + 130 + ], + "55": [ + 10398, + 106 + ], + "56": [ + 21956, + 134 + ], + "57": [ + 22501, + 164 + ], + "58": [ + 11797, + 140 + ], + "59": [ + 1837, + 150 + ], + "60": [ + 2210, + 145 + ], + "61": [ + 2503, + 154 + ], + "62": [ + 2806, + 149 + ], + "63": [ + 3099, + 151 + ], + "64": [ + 3405, + 151 + ], + "65": [ + 3712, + 150 + ], + "66": [ + 4015, + 146 + ], + "67": [ + 4354, + 140 + ], + "68": [ + 4650, + 143 + ], + "69": [ + 9700, + 127 + ], + "70": [ + 2205, + 151 + ], + "71": [ + 11070, + 112 + ], + "72": [ + 12338, + 115 + ], + "73": [ + 12634, + 120 + ], + "74": [ + 10718, + 123 + ], + "75": [ + 12912, + 115 + ], + "76": [ + 13226, + 119 + ], + "77": [ + 13529, + 117 + ], + "78": [ + 5567, + 141 + ], + "79": [ + 13814, + 141 + ], + "80": [ + 14114, + 117 + ], + "81": [ + 14401, + 127 + ], + "82": [ + 16592, + 164 + ], + "83": [ + 14715, + 123 + ], + "87": [ + 4941, + 142 + ], + "88": [ + 5249, + 149 + ], + "3612": [ + 11798, + 129 + ], + "3613": [ + 23720, + 119 + ], + "3637": [ + 10015, + 139 + ], + "3639": [ + 1837, + 151 + ], + "3640": [ + 23148, + 122 + ], + "3653": [ + 2499, + 153 + ], + "3655": [ + 7679, + 127 + ], + "3657": [ + 7975, + 123 + ], + "3663": [ + 9110, + 133 + ], + "3665": [ + 9405, + 125 + ], + "3666": [ + 7369, + 119 + ], + "3667": [ + 8839, + 134 + ], + "3675": [ + 21677, + 127 + ], + "3676": [ + 23425, + 120 + ], + "3677": [ + 23425, + 120 + ], + "57416": [ + 20381, + 137 + ], + "57419": [ + 23982, + 128 + ], + "57421": [ + 24215, + 143 + ], + "57424": [ + 24223, + 134 + ], + "60999": [ + 7679, + 127 + ], + "61000": [ + 20381, + 137 + ], + "61001": [ + 7975, + 123 + ], + "61003": [ + 23982, + 128 + ], + "61005": [ + 24215, + 143 + ], + "61007": [ + 9110, + 133 + ], + "61008": [ + 24223, + 134 + ], + "61009": [ + 9405, + 125 + ], + "61010": [ + 7369, + 119 + ], + "61011": [ + 8839, + 134 + ] + }, + "tags": [ + "pre-installed" + ] +} diff --git a/freewrite/Sounds/crystal_purple_config_new.json b/freewrite/Sounds/crystal_purple_config_new.json new file mode 100644 index 0000000..0e5b6d6 --- /dev/null +++ b/freewrite/Sounds/crystal_purple_config_new.json @@ -0,0 +1,469 @@ +{ + "id": "sound-pack-1200000000009", + "name": "EG Crystal Purple", + "default": true, + "key_define_type": "single", + "includes_numpad": false, + "sound": "purple.ogg", + "defines": { + "1": [ + 1088, + 118 + ], + "2": [ + 1839, + 140 + ], + "3": [ + 2207, + 143 + ], + "4": [ + 2507, + 143 + ], + "5": [ + 2809, + 148 + ], + "6": [ + 3099, + 141 + ], + "7": [ + 3408, + 134 + ], + "8": [ + 3716, + 147 + ], + "9": [ + 4017, + 139 + ], + "10": [ + 4352, + 144 + ], + "11": [ + 4657, + 126 + ], + "12": [ + 4946, + 134 + ], + "13": [ + 5250, + 133 + ], + "14": [ + 5566, + 143 + ], + "15": [ + 6792, + 157 + ], + "16": [ + 7080, + 142 + ], + "17": [ + 7367, + 134 + ], + "18": [ + 7680, + 130 + ], + "19": [ + 7973, + 133 + ], + "20": [ + 8259, + 134 + ], + "21": [ + 8549, + 135 + ], + "22": [ + 8838, + 135 + ], + "23": [ + 9103, + 150 + ], + "24": [ + 9405, + 132 + ], + "25": [ + 9703, + 131 + ], + "26": [ + 10015, + 144 + ], + "27": [ + 10397, + 112 + ], + "28": [ + 15361, + 130 + ], + "29": [ + 21408, + 128 + ], + "30": [ + 12061, + 129 + ], + "31": [ + 12339, + 121 + ], + "32": [ + 12632, + 127 + ], + "33": [ + 12916, + 119 + ], + "34": [ + 13227, + 121 + ], + "35": [ + 13526, + 124 + ], + "36": [ + 13819, + 131 + ], + "37": [ + 14115, + 113 + ], + "38": [ + 14406, + 123 + ], + "39": [ + 14711, + 128 + ], + "40": [ + 15022, + 133 + ], + "41": [ + 1088, + 118 + ], + "42": [ + 16600, + 136 + ], + "43": [ + 10718, + 127 + ], + "44": [ + 16879, + 128 + ], + "45": [ + 17196, + 132 + ], + "46": [ + 17503, + 113 + ], + "47": [ + 17788, + 115 + ], + "48": [ + 18069, + 117 + ], + "49": [ + 18349, + 130 + ], + "50": [ + 18653, + 123 + ], + "51": [ + 18964, + 119 + ], + "52": [ + 19282, + 124 + ], + "53": [ + 19594, + 120 + ], + "54": [ + 19966, + 130 + ], + "55": [ + 10398, + 106 + ], + "56": [ + 21956, + 134 + ], + "57": [ + 22501, + 164 + ], + "58": [ + 11797, + 140 + ], + "59": [ + 1837, + 150 + ], + "60": [ + 2210, + 145 + ], + "61": [ + 2503, + 154 + ], + "62": [ + 2806, + 149 + ], + "63": [ + 3099, + 151 + ], + "64": [ + 3405, + 151 + ], + "65": [ + 3712, + 150 + ], + "66": [ + 4015, + 146 + ], + "67": [ + 4354, + 140 + ], + "68": [ + 4650, + 143 + ], + "69": [ + 9700, + 127 + ], + "70": [ + 2205, + 151 + ], + "71": [ + 11070, + 112 + ], + "72": [ + 12338, + 115 + ], + "73": [ + 12634, + 120 + ], + "74": [ + 10718, + 123 + ], + "75": [ + 12912, + 115 + ], + "76": [ + 13226, + 119 + ], + "77": [ + 13529, + 117 + ], + "78": [ + 5567, + 141 + ], + "79": [ + 13814, + 141 + ], + "80": [ + 14114, + 117 + ], + "81": [ + 14401, + 127 + ], + "82": [ + 16592, + 164 + ], + "83": [ + 14715, + 123 + ], + "87": [ + 4941, + 142 + ], + "88": [ + 5249, + 149 + ], + "3612": [ + 11798, + 129 + ], + "3613": [ + 23720, + 119 + ], + "3637": [ + 10015, + 139 + ], + "3639": [ + 1837, + 151 + ], + "3640": [ + 23148, + 122 + ], + "3653": [ + 2499, + 153 + ], + "3655": [ + 7679, + 127 + ], + "3657": [ + 7975, + 123 + ], + "3663": [ + 9110, + 133 + ], + "3665": [ + 9405, + 125 + ], + "3666": [ + 7369, + 119 + ], + "3667": [ + 8839, + 134 + ], + "3675": [ + 21677, + 127 + ], + "3676": [ + 23425, + 120 + ], + "3677": [ + 23425, + 120 + ], + "57416": [ + 20381, + 137 + ], + "57419": [ + 23982, + 128 + ], + "57421": [ + 24215, + 143 + ], + "57424": [ + 24223, + 134 + ], + "60999": [ + 7679, + 127 + ], + "61000": [ + 20381, + 137 + ], + "61001": [ + 7975, + 123 + ], + "61003": [ + 23982, + 128 + ], + "61005": [ + 24215, + 143 + ], + "61007": [ + 9110, + 133 + ], + "61008": [ + 24223, + 134 + ], + "61009": [ + 9405, + 125 + ], + "61010": [ + 7369, + 119 + ], + "61011": [ + 8839, + 134 + ] + }, + "tags": [ + "pre-installed" + ] +} diff --git a/freewrite/Sounds/crystal_purple_new.mp3 b/freewrite/Sounds/crystal_purple_new.mp3 new file mode 100644 index 0000000..2042a3d Binary files /dev/null and b/freewrite/Sounds/crystal_purple_new.mp3 differ diff --git a/freewrite/cherry_black_copy.mp3 b/freewrite/cherry_black_copy.mp3 new file mode 100644 index 0000000..b7b20fa Binary files /dev/null and b/freewrite/cherry_black_copy.mp3 differ diff --git a/preview.html b/preview.html new file mode 100644 index 0000000..01f364d --- /dev/null +++ b/preview.html @@ -0,0 +1,203 @@ + + + + + + Freewrite Word Count Feature + + + +

Freewrite Word Count Feature

+

I've added a word and character count feature to the Freewrite app that appears after the timer session completes. Here's a visual demonstration of how it works:

+ +
+

Interactive Demo

+ + +
+ 15:00 + + + + New Entry +
+ +

Click the timer to start/stop. When the timer ends (set to 5 seconds for this demo), the word count will appear.

+
+ +

Code Changes

+

Here are the key code changes I made to implement this feature:

+ +

1. Added State Variables

+
+@State private var showWordCount = false
+@State private var wordCount = 0
+@State private var characterCount = 0
+ +

2. Added Word Count Calculation Function

+
+// Calculate word and character count from the current text
+private func calculateWordAndCharacterCount() {
+    let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines)
+    
+    // Calculate character count (excluding whitespace)
+    characterCount = trimmedText.count
+    
+    // Calculate word count
+    let components = trimmedText.components(separatedBy: .whitespacesAndNewlines)
+    wordCount = components.filter { !$0.isEmpty }.count
+}
+ +

3. Added UI Elements to Display the Count

+
+// Word and character count display
+if showWordCount {
+    Text("\(wordCount) words | \(characterCount) chars")
+        .font(.system(size: 13))
+        .foregroundColor(.gray)
+        .padding(.horizontal, 4)
+}
+
+Text("•")
+    .foregroundColor(.gray)
+    .opacity(showWordCount ? 1.0 : 0.0)
+ +

4. Calculate and Show Count When Timer Ends

+
+} else if timeRemaining == 0 {
+    timerIsRunning = false
+    // Calculate word and character count when timer ends
+    calculateWordAndCharacterCount()
+    showWordCount = true
+    if !isHoveringBottomNav {
+        withAnimation(.easeOut(duration: 1.0)) {
+            bottomNavOpacity = 1.0
+        }
+    }
+}
+ +

5. Hide Count When Starting a New Timer

+
+} else {
+    timerIsRunning.toggle()
+    // Hide word count when starting a new timer session
+    if timerIsRunning {
+        showWordCount = false
+    }
+    lastClickTime = now
+}
+ + + + diff --git a/word-count-feature.patch b/word-count-feature.patch new file mode 100644 index 0000000..8a3dbae --- /dev/null +++ b/word-count-feature.patch @@ -0,0 +1,86 @@ +From 1a7b2c88ea43403d9161a1e91ca72ebce85257e0 Mon Sep 17 00:00:00 2001 +From: Kastrah +Date: Mon, 14 Apr 2025 17:39:24 +0100 +Subject: [PATCH] Add word and character count feature that displays after + timer completion + +--- + freewrite/ContentView.swift | 34 ++++++++++++++++++++++++++++++++++ + 1 file changed, 34 insertions(+) + +diff --git a/freewrite/ContentView.swift b/freewrite/ContentView.swift +index 319994b..7df787f 100644 +--- a/freewrite/ContentView.swift ++++ b/freewrite/ContentView.swift +@@ -79,6 +79,9 @@ struct ContentView: View { + @State private var isHoveringHistoryText = false + @State private var isHoveringHistoryPath = false + @State private var isHoveringHistoryArrow = false ++ @State private var showWordCount = false ++ @State private var wordCount = 0 ++ @State private var characterCount = 0 + let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + let entryHeight: CGFloat = 40 + +@@ -556,6 +559,10 @@ struct ContentView: View { + lastClickTime = nil + } else { + timerIsRunning.toggle() ++ // Hide word count when starting a new timer session ++ if timerIsRunning { ++ showWordCount = false ++ } + lastClickTime = now + } + } +@@ -685,6 +692,18 @@ struct ContentView: View { + Text("•") + .foregroundColor(.gray) + ++ // Word and character count display ++ if showWordCount { ++ Text("\(wordCount) words | \(characterCount) chars") ++ .font(.system(size: 13)) ++ .foregroundColor(.gray) ++ .padding(.horizontal, 4) ++ } ++ ++ Text("•") ++ .foregroundColor(.gray) ++ .opacity(showWordCount ? 1.0 : 0.0) ++ + Button(action: { + createNewEntry() + }) { +@@ -887,6 +906,9 @@ struct ContentView: View { + timeRemaining -= 1 + } else if timeRemaining == 0 { + timerIsRunning = false ++ // Calculate word and character count when timer ends ++ calculateWordAndCharacterCount() ++ showWordCount = true + if !isHoveringBottomNav { + withAnimation(.easeOut(duration: 1.0)) { + bottomNavOpacity = 1.0 +@@ -1005,6 +1027,18 @@ struct ContentView: View { + } + } + ++ // Calculate word and character count from the current text ++ private func calculateWordAndCharacterCount() { ++ let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines) ++ ++ // Calculate character count (excluding whitespace) ++ characterCount = trimmedText.count ++ ++ // Calculate word count ++ let components = trimmedText.components(separatedBy: .whitespacesAndNewlines) ++ wordCount = components.filter { !$0.isEmpty }.count ++ } ++ + private func deleteEntry(entry: HumanEntry) { + // Delete the file from the filesystem + let documentsDirectory = getDocumentsDirectory() +-- +2.39.5 (Apple Git-154) + diff --git a/wordcount.patch b/wordcount.patch new file mode 100644 index 0000000..e69de29