├── .gitignore ├── .idea ├── gradle.xml └── vcs.xml ├── Dockerfile ├── README.md ├── app ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── fonts │ │ ├── Nunito-italic-200.woff │ │ ├── Nunito-italic-300.woff │ │ ├── Nunito-italic-400.woff │ │ ├── Nunito-italic-600.woff │ │ ├── Nunito-italic-700.woff │ │ ├── Nunito-italic-800.woff │ │ ├── Nunito-italic-900.woff │ │ ├── Nunito-normal-200.woff │ │ ├── Nunito-normal-300.woff │ │ ├── Nunito-normal-400.woff │ │ ├── Nunito-normal-600.woff │ │ ├── Nunito-normal-700.woff │ │ ├── Nunito-normal-800.woff │ │ ├── Nunito-normal-900.woff │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.ttf │ │ ├── fa-regular-400.woff2 │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff2 │ │ ├── fa-v4compatibility.ttf │ │ └── fa-v4compatibility.woff2 │ ├── index.html │ ├── logo.png │ ├── scripts.min.js │ └── styles.min.css │ ├── java │ └── info │ │ └── aario │ │ └── snotepad │ │ ├── MainActivity.kt │ │ ├── Utils.kt │ │ └── WebAppInterface.kt │ └── res │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ └── strings.xml ├── build-assets.sh ├── build.gradle ├── build.sh ├── delete-tag ├── deploy.sh ├── generate-keys.sh ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── html ├── .gitignore ├── gulpfile.js ├── index.html ├── js │ ├── backend.js │ ├── confirmModal.js │ ├── editor.js │ ├── folderView.js │ ├── history.js │ ├── loading.js │ ├── page.js │ ├── settings.js │ ├── sidebar.js │ ├── sort.js │ ├── template.js │ ├── toast.js │ ├── ui.js │ └── util.js ├── logo.png ├── package-lock.json ├── package.json ├── scss │ ├── _buttons.scss │ ├── _cards.scss │ ├── _charts.scss │ ├── _dropdowns.scss │ ├── _editor.scss │ ├── _error.scss │ ├── _footer.scss │ ├── _global.scss │ ├── _login.scss │ ├── _mixins.scss │ ├── _navs.scss │ ├── _splash.scss │ ├── _themes.scss │ ├── _toast.scss │ ├── _utilities.scss │ ├── _variables.scss │ ├── navs │ │ ├── _global.scss │ │ ├── _sidebar.scss │ │ └── _topbar.scss │ ├── styles.scss │ ├── styles │ │ └── _neomorphism.scss │ ├── themes │ │ ├── _theme-default.scss │ │ ├── _theme-forest.scss │ │ ├── _theme-oceanic.scss │ │ └── _theme-sunset.scss │ └── utilities │ │ ├── _animation.scss │ │ ├── _background.scss │ │ ├── _border.scss │ │ ├── _display.scss │ │ ├── _progress.scss │ │ ├── _rotate.scss │ │ └── _text.scss └── templates │ ├── about.html │ ├── editor.html │ ├── folderView-file.html │ ├── folderView.html │ ├── help.html │ ├── navbar-editor.html │ ├── navbar-folderView.html │ ├── navbar-page.html │ ├── navbar-settings.html │ ├── settings-folder.html │ ├── settings.html │ ├── sidebar-folder.html │ └── toast.html ├── keystore.env.example ├── logs.sh ├── make-relase.sh ├── screenshots ├── Screenshot_20250424-103751_SNotePad.jpg.png ├── Screenshot_20250424-103801_SNotePad.jpg.png ├── Screenshot_20250424-103842_SNotePad.jpg.png ├── Screenshot_20250424-104348_SNotePad.jpg.png └── Screenshot_20250424-104555_SNotePad.jpg.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # .gitignore 2 | # Generated files 3 | *.apk 4 | *.aab 5 | *.ap_ 6 | *.a_ 7 | *.dex 8 | *.class 9 | 10 | # Build directory 11 | build/ 12 | */build/ 13 | 14 | # Local configuration files (sensitive info) 15 | local.properties 16 | *.jks 17 | *.keystore 18 | keystore.env 19 | 20 | # IDE files 21 | .idea/ 22 | *.iml 23 | *.ipr 24 | *.iws 25 | .gradle-home/ 26 | captures/ 27 | .DS_Store 28 | */.DS_Store 29 | 30 | # Gradle cache 31 | .gradle/ 32 | .gradle-opt/ 33 | 34 | # Log files 35 | *.log 36 | 37 | #google-font files: 38 | html/fonts.list 39 | html/fonts/ 40 | html/google-fonts.css 41 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM reactnativecommunity/react-native-android 2 | RUN apt update \ 3 | && apt install -y \ 4 | gradle \ 5 | wget \ 6 | unzip \ 7 | curl \ 8 | gulp 9 | 10 | RUN npm install --global gulp-cli \ 11 | && npm install --global sass 12 | 13 | RUN wget https://services.gradle.org/distributions/gradle-8.13-all.zip \ 14 | && unzip gradle-8.13-all.zip \ 15 | && mv gradle-8.13 /opt/gradle \ 16 | && rm gradle-8.13-all.zip 17 | 18 | ENV GRADLE_HOME='/opt/gradle' 19 | ENV PATH="${GRADLE_HOME}/bin:${PATH}" 20 | 21 | RUN groupadd -g 1000 user \ 22 | && useradd -u 1000 -g user user \ 23 | && mkdir /workdir \ 24 | && chown -R user:user /workdir 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SNotepad 2 | SNotePad offers an effortless way to capture your thoughts with a simple Markdown editor, uniquely saving your notes directly as standard files on your phone, giving you full control to easily manage, back up, and share them anytime, anywhere, without being locked into a proprietary database. 3 | 4 | 5 | Get it on F-Droid 6 | 7 |
8 |
9 | 10 | 11 | Copyright (C) 2025 Aario Shahbany 12 | 13 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License along with this program. If not, see [](http://www.gnu.org/licenses/). 18 | 19 | # About SNotepad 20 | SOkay, here is the description formatted with Markdown headers: 21 | 22 | ## Simple Yet Powerful 23 | 24 | SNotePad is designed for anyone who loves simplicity but needs more power than a basic notepad. It provides a clean, focused writing environment using the intuitive **Markdown format**, allowing you to easily structure your thoughts, create lists, emphasize text, and more, without complex menus. Whether you're jotting down quick ideas, drafting longer articles, or keeping track of important information, *SNotePad* makes the process smooth and efficient. 25 | 26 | ## Organize and Find with Ease 27 | 28 | Beyond simple note creation, *SNotePad* offers robust organization features. Group your notes into **folders** that make sense to you, keeping your workspace tidy and your information easy to locate. Finding specific notes is a breeze with built-in **search capabilities** that quickly scan through your file contents, and you can **sort** your files by name or date to always have the most relevant information at your fingertips. Customization options, like choosing between light and dark **themes**, let you tailor the app to your preference. 29 | 30 | ## Your Notes, Your Files, Your Control 31 | 32 | What truly sets *SNotePad* apart is how it respects your ownership of your data. Unlike many apps that lock your notes away in internal databases or proprietary formats, *SNotePad* saves every note directly as **standard files on your phone's storage**. This means *you* are in full control. You can easily access, copy, move, back up using your favorite tools, share your notes via any app, or even edit them with other software – they are *your files*, accessible and portable, just like any other document on your device. 33 | 34 | ## Try SNotePad Today 35 | 36 | If you're looking for a straightforward yet capable note-taking app that puts you in charge of your notes, give *SNotePad* a try. Enjoy the ease of Markdown editing combined with the freedom and flexibility of direct file access for a truly empowering note-taking experience. 37 | 38 | # Features 39 | Here is a list of features found in the SNotePad application, based on the provided Javascript files: 40 | 41 | ## Markdown Editor 42 | 43 | Provides a rich text editing experience using the EasyMDE library, allowing users to format notes with Markdown syntax for headings, lists, bold, italics, links, code blocks, quotes, and more. 44 | 45 | ## Direct File Storage 46 | 47 | Notes are saved directly as individual files (likely `.md` or plain text) onto the user's phone storage, giving full control over the files for backup, sharing, or use with other applications. 48 | 49 | ## Folder Organization 50 | 51 | Users can add multiple folders from their device storage to organize notes within the app. The sidebar displays these added folders for quick navigation. 52 | 53 | ## File Listing & Sorting 54 | 55 | Within a selected folder, the app displays a list of note files, showing the filename and modification date. Users can sort this list by filename (A-Z or Z-A) or by date (newest or oldest first). 56 | 57 | ## File Search (Folder View) 58 | 59 | A search bar allows users to quickly find notes within the currently viewed folder. The search likely includes filenames and file content, utilizing fuzzy search (Fuse.js) for flexible matching. 60 | 61 | ## Text Search (Editor View) 62 | 63 | While editing a note, users can search for specific text within that note. Matches are highlighted. 64 | 65 | ## File Creation, Saving & Renaming 66 | 67 | Users can create new notes within a chosen folder. Changes can be saved, and both new and existing files can be named or renamed via the editor's filename input field. 68 | 69 | ## File Deletion 70 | 71 | Notes can be permanently deleted from the device storage directly through the folder view interface, with a confirmation step. 72 | 73 | ## Theme Customization 74 | 75 | The application supports Light, Dark, and Auto (system preference) themes for user interface customization. 76 | 77 | ## Responsive Sidebar 78 | 79 | A sidebar displays the user-added folders for navigation. It can be toggled on smaller screens. 80 | 81 | ## Image Handling 82 | 83 | Supports inserting images into notes. Includes functionality for uploading images (likely via drag/drop or paste, interacting with the Android backend) and potentially handling image previews within the editor. 84 | 85 | ## Editor Toolbar 86 | 87 | A toolbar provides quick access buttons for common Markdown formatting actions like bold, italic, headings, lists, quotes, code blocks, links, images, tables, horizontal rules, and undo/redo. 88 | 89 | ## Preview Mode 90 | 91 | Users can toggle a preview mode to see how their Markdown notes will be rendered as formatted text. 92 | 93 | ## Side-by-Side View 94 | 95 | Offers a split-screen view showing the Markdown source code alongside its live preview. 96 | 97 | ## Fullscreen Mode 98 | 99 | Allows toggling a distraction-free fullscreen mode for either the editor or the preview. 100 | 101 | ## Clean Block Formatting 102 | 103 | A toolbar action is available to remove Markdown formatting from a selected block of text. 104 | 105 | ## User Feedback & Controls 106 | 107 | Provides user feedback through toasts for actions like saving or errors and uses confirmation modals for destructive actions like deleting files. Loading indicators inform the user during operations. Includes back-button navigation history management. 108 | 109 | ## Android Integration 110 | 111 | The web frontend communicates with a native Android layer (`AndroidInterface`) to perform file system operations, manage preferences, and potentially handle other device interactions. 112 | 113 | # Development 114 | 115 | User interface part of this app is done via html5 and javascript. 116 | Heavy usage of jquery, bootstrap 5 and SB Admin 2 117 | 118 | ## Web Assets 119 | 120 | Web Asset source files are in html folder. After you compiled them, they get copied over to: 121 | ``` 122 | ../app/src/main/assets 123 | ``` 124 | To faster develop and test the web asset files locally, open ../app/src/main/assets/index.html file in a browser wiht `?debug=true`. 125 | 126 | ### Compile 127 | 128 | To compile web assets, run `build-assets.sh` It will compile them inside a docker container. To build the docker container refer to Build the Compile Container Section 129 | 130 | ## Build the Compile Container Section 131 | 132 | Inside project root, run: 133 | 134 | ``` 135 | docker build -t wsn-builder . 136 | ``` 137 | ## Acknowledgements / Credits 138 | 139 | > aario.info - SNotePad HTML v1.0.0 140 | > (Based on [Start Bootstrap SB Admin 2 Theme](https://startbootstrap.com/theme/sb-admin-2)) 141 | > Copyright 2025-2025 Aario Shahbany 142 | 143 | This application utilizes several excellent open-source libraries, including: 144 | 145 | * [jQuery](https://jquery.com/) (License: MIT) 146 | * [Bootstrap](https://getbootstrap.com/) (License: MIT) 147 | * [EasyMDE](https://github.com/Ionaru/easy-markdown-editor) (License: MIT) 148 | * [Hammer.JS](https://hammerjs.github.io/) (License: MIT) 149 | * [Fuse.js](https://fusejs.io/) (License: Apache 2.0) 150 | * [SortableJS](https://sortablejs.github.io/Sortable/) (License: MIT) 151 | * [CodeMirror](https://codemirror.net/) (License: MIT) 152 | * [CodeMirror Spell Checker](https://github.com/sparksuite/codemirror-spell-checker) (License: MIT) 153 | * [Marked](https://marked.js.org/) (License: MIT) 154 | * [Typo.js](https://github.com/cfinke/Typo.js) (License: MIT-style) 155 | * [Font Awesome](https://fontawesome.com/) (License: CC BY 4.0, SIL OFL 1.1, MIT) 156 | * [jQuery Easing](https://github.com/jquery/jquery-easing) (License: BSD) 157 | * _(Others may be included in bundled dependencies)_ 158 | 159 | The SNotePad application code itself is licensed under the GNU General Public License 160 | 161 | ```text 162 | GPL v3 License 163 | 164 | Copyright (c) 2025 Aario Shahbany 165 | 166 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. 167 | 168 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 169 | 170 | You should have received a copy of the GNU General Public License along with this program. If not, see . 171 | ``` 172 | 173 | It takes a lot of time and effort to write a software. If you found SNotepad usefull, please consider becoming a sponsor by clicking below button: 174 | 175 | [![donation](https://img.shields.io/badge/Sponsor-%231EAEDB.svg?style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/aario) 176 | 177 | Thanks in advance. 178 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | // app/build.gradle (App Level) 2 | // Configures settings specific to the 'app' module. 3 | 4 | // Apply necessary plugins 5 | plugins { 6 | id 'com.android.application' // Apply the Android Application plugin 7 | id 'org.jetbrains.kotlin.android' // Apply the Kotlin Android plugin 8 | } 9 | 10 | // REMOVED: Logic to load signing config from keystore.properties is removed. 11 | /* 12 | def keystorePropertiesFile = rootProject.file("keystore.properties") 13 | def keystoreProperties = new Properties() 14 | if (keystorePropertiesFile.exists()) { 15 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 16 | } else { 17 | println("Warning: keystore.properties not found. Using placeholder signing config.") 18 | keystoreProperties['storeFile'] = 'keystore.jks' 19 | keystoreProperties['storePassword'] = 'password' 20 | keystoreProperties['keyAlias'] = 'alias' 21 | keystoreProperties['keyPassword'] = 'password' 22 | } 23 | */ 24 | 25 | // Read signing configuration from environment variables 26 | def keyStoreFileEnv = System.getenv('KEYSTORE_FILE') 27 | def keyStorePasswordEnv = System.getenv('KEYSTORE_PASSWORD') 28 | def keyAliasEnv = System.getenv('KEY_ALIAS') 29 | def keyPasswordEnv = System.getenv('KEY_PASSWORD') 30 | 31 | // Check if environment variables are set 32 | if (keyStoreFileEnv == null || keyStorePasswordEnv == null || keyAliasEnv == null || keyPasswordEnv == null) { 33 | println("Warning: Signing environment variables (KEYSTORE_FILE, KEYSTORE_PASSWORD, KEY_ALIAS, KEY_PASSWORD) not fully set. Release build signing might fail.") 34 | // Optionally, throw an error to fail the build immediately if variables are missing 35 | // throw new GradleException("Missing signing environment variables for release build.") 36 | } 37 | 38 | 39 | android { 40 | namespace 'info.aario.snotepad' // Application package name 41 | compileSdk 34 // Target Android SDK version (use a recent API level) 42 | 43 | defaultConfig { 44 | applicationId "info.aario.snotepad" 45 | minSdk 24 // Minimum Android version supported (Android 7.0 Nougat) 46 | targetSdk 34 // Target SDK version (should match compileSdk) 47 | versionCode 1 48 | versionName "1.0" 49 | 50 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 51 | } 52 | 53 | signingConfigs { 54 | release { 55 | // Use values read from environment variables 56 | // Check if KEYSTORE_FILE is set before trying to create a file object 57 | if (keyStoreFileEnv != null) { 58 | storeFile file(keyStoreFileEnv) // Use the absolute path from the env variable 59 | } else { 60 | // Provide a default or handle the error appropriately 61 | println "Warning: KEYSTORE_FILE environment variable not set." 62 | // storeFile file('dummy.jks') // Example: point to a non-existent file to likely cause later failure if needed 63 | } 64 | storePassword keyStorePasswordEnv 65 | keyAlias keyAliasEnv 66 | keyPassword keyPasswordEnv 67 | } 68 | } 69 | 70 | buildTypes { 71 | release { 72 | minifyEnabled true // Enable code shrinking & obfuscation 73 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 74 | signingConfig signingConfigs.release // Apply the release signing configuration 75 | } 76 | debug { 77 | minifyEnabled false 78 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 79 | // Debug builds are automatically signed with a debug key 80 | } 81 | } 82 | 83 | // Configure Java/Kotlin compiler options 84 | compileOptions { 85 | sourceCompatibility JavaVersion.VERSION_1_8 86 | targetCompatibility JavaVersion.VERSION_1_8 87 | } 88 | kotlinOptions { 89 | jvmTarget = '1.8' 90 | } 91 | 92 | // Ensure assets directory is included 93 | sourceSets { 94 | main { 95 | assets.srcDirs = ['src/main/assets'] 96 | // Ensure res directory is recognized (Gradle usually does this automatically, but doesn't hurt) 97 | res.srcDirs = ['src/main/res'] 98 | } 99 | } 100 | } 101 | 102 | dependencies { 103 | // Core Kotlin libraries 104 | implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.23' // Match plugin version 105 | 106 | // AndroidX libraries (essential for modern Android development) 107 | implementation 'androidx.core:core-ktx:1.13.0' // Core Kotlin extensions 108 | implementation 'androidx.appcompat:appcompat:1.6.1' // App compatibility library 109 | implementation 'androidx.webkit:webkit:1.10.0' // WebView support library 110 | implementation "androidx.documentfile:documentfile:1.0.1" // For Storage Access Framework (SAF) interaction 111 | 112 | // Coroutines for background tasks 113 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' 114 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' 115 | 116 | // Testing libraries (optional, but good practice) 117 | testImplementation 'junit:junit:4.13.2' 118 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 119 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 120 | 121 | // for Material Components: 122 | implementation 'com.google.android.material:material:1.12.0' // Use the latest stable version 123 | } 124 | 125 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # app/proguard-rules.pro 2 | # ProGuard rules for the app module. 3 | 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are applied to implementations of the 6 | # library specified in build.gradle. For example, the following line 7 | # applies code shrinking on the exported library: 8 | #-dontshrink 9 | 10 | # If your project uses WebView with JavaScript, uncomment the following 11 | # and adjust the class name accordingly: 12 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 13 | # public *; 14 | #} 15 | # Keep the JavascriptInterface methods 16 | -keepclassmembers class info.aario.snotepad.WebAppInterface { 17 | @android.webkit.JavascriptInterface ; 18 | } 19 | 20 | # Keep Kotlin metadata for reflection (if needed, often required by libraries) 21 | -keep class kotlin.Metadata { *; } 22 | -keep class kotlin.coroutines.Continuation { *; } # Keep Continuation for coroutines 23 | 24 | # Add any rules specific to libraries you use. For example, if you use Gson: 25 | # -keep class com.google.gson.annotations.** { *; } 26 | # -keep class * implements com.google.gson.TypeAdapterFactory 27 | # -keep class * implements com.google.gson.JsonSerializer 28 | # -keep class * implements com.google.gson.JsonDeserializer 29 | 30 | # Keep default constructors for Activities, Services, etc. 31 | -keep public class * extends android.app.Activity 32 | -keep public class * extends android.app.Application 33 | -keep public class * extends android.app.Service 34 | -keep public class * extends android.content.BroadcastReceiver 35 | -keep public class * extends android.content.ContentProvider 36 | -keep public class * extends android.app.backup.BackupAgentHelper 37 | -keep public class * extends android.preference.Preference 38 | -keep public class com.android.vending.licensing.ILicensingService 39 | 40 | # Keep Parcelable implementations 41 | -keep class * implements android.os.Parcelable { 42 | public static final android.os.Parcelable$Creator *; 43 | } 44 | 45 | # Keep enums 46 | -keepclassmembers enum * { 47 | public static **[] values(); 48 | public static ** valueOf(java.lang.String); 49 | } 50 | 51 | # Keep R class members 52 | -keepclassmembers class **.R$* { 53 | public static ; 54 | } 55 | 56 | # Suppress warnings about kotlinx libraries (common issue) 57 | -dontwarn kotlinx.** 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Nunito-italic-200.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/Nunito-italic-200.woff -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Nunito-italic-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/Nunito-italic-300.woff -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Nunito-italic-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/Nunito-italic-400.woff -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Nunito-italic-600.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/Nunito-italic-600.woff -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Nunito-italic-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/Nunito-italic-700.woff -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Nunito-italic-800.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/Nunito-italic-800.woff -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Nunito-italic-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/Nunito-italic-900.woff -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Nunito-normal-200.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/Nunito-normal-200.woff -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Nunito-normal-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/Nunito-normal-300.woff -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Nunito-normal-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/Nunito-normal-400.woff -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Nunito-normal-600.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/Nunito-normal-600.woff -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Nunito-normal-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/Nunito-normal-700.woff -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Nunito-normal-800.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/Nunito-normal-800.woff -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Nunito-normal-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/Nunito-normal-900.woff -------------------------------------------------------------------------------- /app/src/main/assets/fonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /app/src/main/assets/fonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /app/src/main/assets/fonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /app/src/main/assets/fonts/fa-v4compatibility.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/fa-v4compatibility.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/fa-v4compatibility.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/fonts/fa-v4compatibility.woff2 -------------------------------------------------------------------------------- /app/src/main/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | S Notepad 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | 21 |
22 | 23 |
24 |
25 | 26 | 69 |
70 | 71 | 79 |
80 | 81 |
82 |
83 |
84 |

Welcome to SNotePad!

85 |

Start by adding a folder using the button below:

86 | 89 |
90 |
91 |
92 |
93 |
94 | 95 | 96 | 97 | 98 | 113 |
114 |
115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /app/src/main/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/assets/logo.png -------------------------------------------------------------------------------- /app/src/main/java/info/aario/snotepad/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package info.aario.snotepad 2 | 3 | // Imports remain largely the same, ensure necessary ones are present 4 | import android.annotation.SuppressLint 5 | import android.app.Activity 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.net.Uri 9 | import android.os.Bundle 10 | import android.provider.DocumentsContract 11 | import android.util.Log 12 | import android.webkit.WebChromeClient 13 | import android.webkit.WebResourceRequest 14 | import android.webkit.WebView 15 | import android.webkit.WebViewClient 16 | import android.widget.Toast 17 | import androidx.activity.result.ActivityResultLauncher // Import explicitly if needed 18 | import androidx.activity.result.contract.ActivityResultContracts 19 | import androidx.appcompat.app.AppCompatActivity 20 | import androidx.documentfile.provider.DocumentFile 21 | import androidx.lifecycle.lifecycleScope 22 | import kotlinx.coroutines.Dispatchers 23 | import kotlinx.coroutines.launch 24 | import kotlinx.coroutines.withContext 25 | import org.json.JSONArray 26 | import org.json.JSONObject 27 | import java.io.BufferedReader 28 | import java.io.InputStreamReader 29 | import java.io.OutputStreamWriter 30 | import java.text.SimpleDateFormat 31 | import java.util.Locale 32 | import java.util.Date 33 | import android.os.Build 34 | import android.content.pm.ApplicationInfo 35 | import androidx.activity.OnBackPressedCallback 36 | 37 | class MainActivity : AppCompatActivity() { 38 | 39 | private lateinit var webView: WebView 40 | private lateinit var webViewBackCallback: OnBackPressedCallback 41 | 42 | // Keep ActivityResultLauncher in MainActivity as it's tied to the Activity lifecycle 43 | // Made public so WebAppInterface can access it via the activity instance 44 | val openDirectoryLauncher: ActivityResultLauncher = registerForActivityResult( 45 | ActivityResultContracts.StartActivityForResult() 46 | ) { result -> 47 | if (result.resultCode == Activity.RESULT_OK) { 48 | result.data?.data?.also { uri -> 49 | val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or 50 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 51 | try { 52 | contentResolver.takePersistableUriPermission(uri, takeFlags) 53 | Log.d("MainActivity", "Folder selected: $uri") 54 | val escapedUri = escapeStringForJavaScript(uri.toString()) 55 | callJs("requestFolderSelectionSuccess('$escapedUri')") 56 | } catch (e: SecurityException) { 57 | Log.e("MainActivity", "Failed to take persistable URI permission", e) 58 | toastError("Failed to get persistent permission for the selected folder.") 59 | } 60 | } 61 | } else { 62 | Log.w("MainActivity", "Folder selection cancelled or failed.") 63 | toastError("Folder selection cancelled or failed.") 64 | } 65 | } 66 | 67 | @SuppressLint("SetJavaScriptEnabled") 68 | override fun onCreate(savedInstanceState: Bundle?) { 69 | super.onCreate(savedInstanceState) 70 | 71 | webView = WebView(this).apply { 72 | settings.javaScriptEnabled = true 73 | settings.domStorageEnabled = true 74 | settings.allowFileAccess = true 75 | settings.allowContentAccess = true 76 | 77 | // Replace the default WebViewClient with one that intercepts http/https links 78 | webViewClient = object : WebViewClient() { 79 | override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { 80 | val url = request?.url ?: return false // Get the URL safely 81 | 82 | // Check if the URL scheme is http or https 83 | if (url.scheme == "http" || url.scheme == "https") { 84 | try { 85 | // Create an Intent to view the URL externally 86 | val intent = Intent(Intent.ACTION_VIEW, url) 87 | // Verify that the intent can be handled to avoid crashes 88 | if (intent.resolveActivity(packageManager) != null) { 89 | startActivity(intent) // Start the external browser or app 90 | } else { 91 | Log.w("MainActivity", "No application found to handle URL: $url") 92 | Toast.makeText(this@MainActivity, "No app found to open this link.", Toast.LENGTH_SHORT).show() 93 | } 94 | } catch (e: Exception) { 95 | Log.e("MainActivity", "Could not open external link: $url", e) 96 | Toast.makeText(this@MainActivity, "Could not open link.", Toast.LENGTH_SHORT).show() 97 | } 98 | return true // Indicate that we've handled the URL loading 99 | } 100 | // For other schemes (e.g., file://, javascript:), let the WebView handle it 101 | return false // Let the WebView load the URL internally 102 | // Alternatively: return super.shouldOverrideUrlLoading(view, request) which does the same for standard schemes 103 | } 104 | } 105 | 106 | 107 | // Instantiate the external WebAppInterface, passing this MainActivity instance 108 | addJavascriptInterface(WebAppInterface(this@MainActivity), "AndroidInterface") 109 | 110 | loadUrl("file:///android_asset/index.html") 111 | } 112 | 113 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 114 | if (0 != applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) { 115 | WebView.setWebContentsDebuggingEnabled(true) 116 | } 117 | } 118 | 119 | setContentView(webView) 120 | 121 | // --- Modern Back Press Handling --- 122 | val onBackPressedCallback = object : OnBackPressedCallback(true) { 123 | override fun handleOnBackPressed() { 124 | // This callback is always enabled, so this code will always execute 125 | // when the back button is pressed while this Activity is active. 126 | 127 | // Check if webView is initialized before calling methods on it, 128 | // just as a safety measure, though based on your description it should be. 129 | if (! ::webView.isInitialized) { 130 | return; 131 | } 132 | 133 | callJs("handleBackPress()") 134 | } 135 | } 136 | 137 | // Add the callback to the dispatcher, linking it to the Activity's lifecycle 138 | onBackPressedDispatcher.addCallback(this, onBackPressedCallback) 139 | // --- End Modern Back Press Handling --- 140 | } 141 | 142 | // --- callJs remains in MainActivity (helper for this activity) --- 143 | // Made public so WebAppInterface can call it via the activity instance 144 | fun callJs(script: String) { 145 | if (::webView.isInitialized) { 146 | webView.post { 147 | webView.evaluateJavascript("javascript:window.$script", null) 148 | } 149 | } else { 150 | Log.w("MainActivity", "WebView not initialized when trying to evaluate JS: $script") 151 | } 152 | } 153 | 154 | fun toastError(message: String) { 155 | callJs("showToast('$message', true)"); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /app/src/main/java/info/aario/snotepad/Utils.kt: -------------------------------------------------------------------------------- 1 | package info.aario.snotepad 2 | 3 | import android.util.Log // Import the Log class 4 | import android.webkit.WebView // Import the WebView class 5 | 6 | /** 7 | * Contains utility functions for the SNotePad application. 8 | */ 9 | 10 | /** 11 | * Escapes characters in a string to make it safe for insertion 12 | * into JavaScript single-quoted or double-quoted strings. 13 | * Handles null input by returning an empty string. 14 | * 15 | * @param str The string to escape. 16 | * @return The escaped string, or an empty string if input is null. 17 | */ 18 | fun escapeStringForJavaScript(str: String?): String { 19 | if (str == null) return "" 20 | // Order matters: escape backslash first 21 | return str.replace("\\", "\\\\") 22 | .replace("'", "\\'") 23 | .replace("\"", "\\\"") // Escape double quotes 24 | .replace("\n", "\\n") // Escape newlines 25 | .replace("\r", "\\r") // Escape carriage returns 26 | .replace("\t", "\\t") // Escape tabs 27 | // Add other escapes if necessary (e.g., \b, \f) 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | SNotePad 4 | 5 | -------------------------------------------------------------------------------- /build-assets.sh: -------------------------------------------------------------------------------- 1 | #to build: 2 | # docker build -t wsn-builder . 3 | 4 | gulpCommand=build 5 | if [ "x$1" == 'xwatch' ]; then 6 | gulpCommand=watch 7 | fi 8 | 9 | docker run \ 10 | -ti \ 11 | --rm \ 12 | -v $(pwd):/workspace \ 13 | -v $(pwd)/.gradle-home:/root/.gradle \ 14 | -w /workspace/html \ 15 | wsn-builder \ 16 | /bin/bash -c ' 17 | npm install \ 18 | && gulp clean \ 19 | && gulp '"$gulpCommand"' \ 20 | ' 21 | 22 | echo 'Run build.sh [debug] script to build the apk file.' 23 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // build.gradle (Project Level) 2 | // Configures build settings applicable to all modules in the project. 3 | 4 | buildscript { 5 | repositories { 6 | google() // Needed for Android Gradle plugin 7 | mavenCentral() 8 | } 9 | dependencies { 10 | // Define the Android Gradle plugin version 11 | // Use a recent, stable version. Check https://developer.android.com/studio/releases/gradle-plugin for updates. 12 | classpath 'com.android.tools.build:gradle:8.3.2' // Example version, adjust as needed 13 | 14 | // Define the Kotlin Gradle plugin version 15 | // Use a version compatible with the AGP version. Check https://kotlinlang.org/docs/releases.html#android. 16 | classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23' // Example version, adjust as needed 17 | } 18 | } 19 | 20 | // Apply plugins to all projects (root and sub-projects) 21 | // Base plugin provides tasks like 'clean' 22 | plugins { 23 | id 'base' 24 | } 25 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #to build: 2 | # docker build -t wsn-builder . 3 | mode=Release 4 | 5 | if [ "x$1" == 'xdebug' ]; then 6 | mode=Debug 7 | fi 8 | 9 | docker run \ 10 | -ti \ 11 | --rm \ 12 | -v $(pwd):/workspace \ 13 | -v $(pwd)/.gradle-opt:/opt \ 14 | -v $(pwd)/.gradle-home:/root/.gradle \ 15 | -w /workspace \ 16 | wsn-builder \ 17 | /bin/bash -c ' 18 | source ./keystore.env \ 19 | && ./gradlew clean :app:assemble'"$mode"' \ 20 | ' 21 | find ./app/build -name '*.apk' -exec ls -l {} \; 22 | echo Done. 23 | echo 'Run deploy.sh [debug] script to deploy on your android phone using adb and a usb cable.' 24 | -------------------------------------------------------------------------------- /delete-tag: -------------------------------------------------------------------------------- 1 | git push origin --delete $1 2 | git tag -d $1 3 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mode=release 3 | 4 | if [ "x$1" == 'xdebug' ]; then 5 | mode=debug 6 | shift 7 | fi 8 | 9 | if [ "x$1" == 'xuninstall' ]; then 10 | adb uninstall info.aario.snotepad 11 | fi 12 | 13 | adb install ./app/build/outputs/apk/$mode/app-$mode.apk 14 | adb shell monkey -p info.aario.snotepad -c android.intent.category.LAUNCHER 1 15 | -------------------------------------------------------------------------------- /generate-keys.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo -n 'Company name: '; read COMPANY_NAME 6 | echo -n 'Organization name: '; read ORGANIZATION_NAME 7 | echo -n 'Organization: '; read ORGANIZATION 8 | echo -n 'City: '; read CITY 9 | echo -n 'Stage: '; read STATE 10 | echo -n 'Country Code: '; read COUNTRY_CODE 11 | echo -n 'Key Password: '; read PASSWORD 12 | echo -n 'Key Alias: '; read ALIAS 13 | 14 | keytool \ 15 | -genkeypair \ 16 | -v \ 17 | -keystore keystore.jks \ 18 | -keyalg RSA \ 19 | -keysize 2048 \ 20 | -validity 10000 \ 21 | -dname "CN=$COMPANY_NAME, OU=$ORGANIZATION_NAME, O=$ORGANIZATION, L=$CITY, S=$STATE, C=$COUNTRY_CODE" \ 22 | -storepass "$PASSWORD" \ 23 | -keypass "$PASSWORD" \ 24 | -alias "$ALIAS" 25 | 26 | echo "export KEYSTORE_FILE='/workspace/keystore.jks'" > keystore.env 27 | echo "export KEYSTORE_PASSWORD='$PASSWORD'" >> keystore.env 28 | echo "export KEY_PASSWORD='$PASSWORD'" >> keystore.env 29 | echo "export KEY_ALIAS='$ALIAS'" >> keystore.env 30 | 31 | echo 'Files created:' 32 | ls -l keystore.jks 33 | ls -l keystore.env 34 | 35 | echo 'Done.' 36 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Store signing configuration securely. DO NOT COMMIT THIS FILE to version control. 2 | # Replace placeholders with your actual keystore details. 3 | KEYSTORE_FILE=keystore.jks 4 | KEYSTORE_PASSWORD=your_keystore_password 5 | KEY_ALIAS=app_alias 6 | KEY_PASSWORD=your_key_password 7 | 8 | # Recommended Gradle properties 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | android.useAndroidX=true 11 | # Enable Kotlin specific optimizations 12 | kotlin.incremental=true 13 | org.gradle.configuration-cache=true 14 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /html/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | vendor 3 | css 4 | js/*.min.js 5 | js/scripts.js 6 | -------------------------------------------------------------------------------- /html/gulpfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Load plugins 4 | const autoprefixer = require("gulp-autoprefixer"); 5 | const browsersync = require("browser-sync").create(); 6 | const cleanCSS = require("gulp-clean-css"); 7 | const concat = require("gulp-concat"); 8 | const del = require("del"); 9 | const gulp = require("gulp"); 10 | const header = require("gulp-header"); 11 | const merge = require("merge-stream"); 12 | const plumber = require("gulp-plumber"); 13 | const rename = require("gulp-rename"); 14 | const sass = require('gulp-sass')(require('sass')); 15 | const uglify = require("gulp-uglify"); 16 | const googleWebfonts = require("gulp-google-webfonts"); 17 | 18 | const fs = require('fs'); 19 | const path = require('path'); 20 | 21 | // Load package.json for banner 22 | const pkg = require('./package.json'); 23 | 24 | // Set the banner content 25 | const banner = ['/*!\n', 26 | ' * aario.info - <%= pkg.title %> v<%= pkg.version %> (<%= pkg.homepage %>)\n', 27 | ' * Copyright 2025-' + (new Date()).getFullYear(), ' <%= pkg.author %>\n', 28 | ' * Licensed under <%= pkg.license %> (https://github.com/StartBootstrap/<%= pkg.name %>/blob/master/LICENSE)\n', 29 | ' */\n', 30 | '\n' 31 | ].join(''); 32 | 33 | // --- Configuration --- 34 | const paths = { 35 | templates: { 36 | src: './templates/**/*.html', 37 | dest: './js/build/', 38 | generatedFile: '_templates.js' 39 | }, 40 | js: { 41 | src: './js/*.js', 42 | dest: './js/build/', 43 | minifiedDest: '../app/src/main/assets/', 44 | concatFile: 'scripts.js', 45 | minSuffix: '.min' 46 | }, 47 | etc: { 48 | src: [ 49 | './index.html', 50 | './logo.png', 51 | ], 52 | dest: '../app/src/main/assets/' 53 | }, 54 | fonts: { 55 | src: [ 56 | './fonts/*', 57 | './vendor/fontawesome-free/webfonts/*', 58 | ], 59 | dest: '../app/src/main/assets/fonts' 60 | }, 61 | // --- Fonts Configuration --- 62 | googleFonts: { 63 | src: './fonts.list', 64 | dest: './', // Destination for gulp.dest() after googleWebfonts runs 65 | fontDir: 'fonts/', // Fonts will go to ./fonts/ relative to dest 66 | cssDir: 'css/', // CSS will go to ./css/ relative to dest 67 | cssFilename: 'google-fonts.css', 68 | cssPath: '../fonts/' // <--- Correct relative path from CSS file to font directory 69 | } 70 | // --- End Fonts Configuration --- 71 | }; 72 | const templatesDir = path.join(__dirname, 'templates'); 73 | const generatedTemplatesJsPath = path.join(__dirname, paths.templates.dest, paths.templates.generatedFile); 74 | const generatedFontCssPath = path.join(__dirname, paths.googleFonts.cssDir, paths.googleFonts.cssFilename); // Path to generated font CSS 75 | 76 | // BrowserSync 77 | function browserSync(done) { 78 | browsersync.init({ 79 | server: { 80 | baseDir: "./" 81 | }, 82 | port: 3000, 83 | open: false, 84 | }); 85 | done(); 86 | } 87 | 88 | // BrowserSync reload 89 | function browserSyncReload(done) { 90 | browsersync.reload(); 91 | done(); 92 | } 93 | 94 | // Clean vendor, built assets, and fonts 95 | function clean() { 96 | return del([ 97 | "./vendor/", 98 | "./css/*.css", 99 | "./js/build/*", 100 | "./fonts/" 101 | ]); 102 | } 103 | 104 | // Google Fonts Task - CORRECTED 105 | function googlefonts() { 106 | const fontListPath = paths.googleFonts.src; 107 | if (!fs.existsSync(fontListPath)) { 108 | const fontContent = 'Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i'; 109 | fs.writeFileSync(fontListPath, fontContent); 110 | console.log(`Created ${fontListPath} with default Nunito font.`); 111 | } 112 | 113 | return gulp.src(paths.googleFonts.src) 114 | .pipe(plumber()) // Add plumber for error handling 115 | .pipe(googleWebfonts({ 116 | fontsDir: paths.googleFonts.fontDir, // Relative to dest 117 | cssFilename: paths.googleFonts.cssFilename, 118 | // formats: ['woff2', 'woff'] // Optionally specify formats 119 | })) 120 | .on('error', function(err) { // More specific error logging 121 | console.error('Error in googlefonts task:', err.toString()); 122 | this.emit('end'); // Prevent Gulp from stopping on error 123 | }) 124 | .pipe(gulp.dest(paths.googleFonts.dest)); // Output to root directory './' 125 | } 126 | 127 | 128 | // Bring third party dependencies from node_modules into vendor directory 129 | function modules() { 130 | // Bootstrap JS 131 | var bootstrapJS = gulp.src('./node_modules/bootstrap/dist/js/*') 132 | .pipe(gulp.dest('./vendor/bootstrap/js')); 133 | // Bootstrap SCSS 134 | var bootstrapSCSS = gulp.src('./node_modules/bootstrap/scss/**/*') 135 | .pipe(gulp.dest('./vendor/bootstrap/scss')); 136 | // ChartJS 137 | var chartJS = gulp.src('./node_modules/chart.js/dist/*.js') 138 | .pipe(gulp.dest('./vendor/chart.js')); 139 | // dataTables 140 | var dataTables = gulp.src([ 141 | './node_modules/datatables.net/js/*.js', 142 | './node_modules/datatables.net-bs4/js/*.js', 143 | './node_modules/datatables.net-bs4/css/*.css' 144 | ]) 145 | .pipe(gulp.dest('./vendor/datatables')); 146 | // Font Awesome 147 | var fontAwesome = gulp.src('./node_modules/@fortawesome/**/*') 148 | .pipe(gulp.dest('./vendor')); 149 | // jQuery Easing 150 | var jqueryEasing = gulp.src('./node_modules/jquery.easing/*.js') 151 | .pipe(gulp.dest('./vendor/jquery-easing')); 152 | // jQuery 153 | var jquery = gulp.src([ 154 | './node_modules/jquery/dist/*', 155 | '!./node_modules/jquery/dist/core.js' 156 | ]) 157 | .pipe(gulp.dest('./vendor/jquery')); 158 | // EasyMDE 159 | var easymde = gulp.src([ 160 | './node_modules/easymde/dist/*' 161 | ]) 162 | .pipe(gulp.dest('./vendor/easymde')); 163 | // Hammer.js 164 | var hammerJS = gulp.src('./node_modules/hammerjs/hammer.min.js') 165 | .pipe(gulp.dest('./vendor/hammerjs')); 166 | // Fuse.js 167 | var fuseJS = gulp.src('./node_modules/fuse.js/dist/fuse.min.js') 168 | .pipe(gulp.dest('./vendor/fuse.js')); 169 | // SortableJS 170 | var sortableJS = gulp.src('./node_modules/sortablejs/Sortable.min.js') 171 | .pipe(gulp.dest('./vendor/sortablejs')); 172 | 173 | return merge(bootstrapJS, bootstrapSCSS, chartJS, dataTables, fontAwesome, jquery, jqueryEasing, easymde, hammerJS, fuseJS, sortableJS); 174 | } 175 | 176 | // CSS task - Includes generated font CSS 177 | function css() { 178 | // Process SCSS separately first 179 | var scssStream = gulp.src("./scss/styles.scss") // Process only the main SCSS file 180 | .pipe(plumber()) 181 | .pipe(sass({ 182 | outputStyle: "expanded", 183 | includePaths: "./node_modules", 184 | })) 185 | .on("error", sass.logError) 186 | .pipe(autoprefixer({ 187 | cascade: false 188 | })); 189 | // Stream for the generated google fonts css 190 | var fontCssStream = gulp.src(generatedFontCssPath, { allowEmpty: true }); 191 | 192 | // Merge the two streams, process together 193 | return merge(fontCssStream, scssStream) // Font CSS first, then compiled SCSS 194 | .pipe(plumber()) 195 | .pipe(concat('styles.css')) // Concatenate font CSS and compiled SCSS 196 | .pipe(header(banner, { 197 | pkg: pkg 198 | })) 199 | .pipe(gulp.dest("./css")) // Output the non-minified combined CSS 200 | .pipe(rename({ 201 | suffix: ".min" 202 | })) 203 | .pipe(cleanCSS()) 204 | .pipe(gulp.dest("../app/src/main/assets/")) // Output the minified combined CSS 205 | .pipe(browsersync.stream()); 206 | } 207 | 208 | 209 | function generateTemplatesJS(done) { 210 | // (generateTemplatesJS function remains unchanged) 211 | if (!fs.existsSync(templatesDir)) { 212 | console.warn(`Templates directory not found: ${templatesDir}. Skipping template generation.`); 213 | if (!fs.existsSync(path.dirname(generatedTemplatesJsPath))) { 214 | fs.mkdirSync(path.dirname(generatedTemplatesJsPath), { recursive: true }); 215 | } 216 | fs.writeFileSync(generatedTemplatesJsPath, 'window.templates = {};\n'); 217 | return done(); 218 | } 219 | let templatesObject = 'window.templates = {\n'; 220 | const files = fs.readdirSync(templatesDir); 221 | files.forEach(file => { 222 | const filePath = path.join(templatesDir, file); 223 | const fileStat = fs.statSync(filePath); 224 | if (fileStat.isFile() && path.extname(file).toLowerCase() === '.html') { 225 | const baseName = path.basename(file, '.html'); 226 | const content = fs.readFileSync(filePath, 'utf8'); 227 | const escapedContent = content 228 | .replace(/\\/g, '\\\\') 229 | .replace(/`/g, '\\`') 230 | .replace(/\$\{/g, '\\${'); 231 | templatesObject += ` "${baseName}": \`${escapedContent}\`,\n`; 232 | } 233 | }); 234 | if (templatesObject.endsWith(',\n')) { 235 | templatesObject = templatesObject.slice(0, -2) + '\n'; 236 | } 237 | templatesObject += '};\n'; 238 | if (!fs.existsSync(paths.templates.dest)) { 239 | fs.mkdirSync(paths.templates.dest, { recursive: true }); 240 | } 241 | fs.writeFileSync(generatedTemplatesJsPath, templatesObject); 242 | console.log(`Generated ${paths.templates.generatedFile} from HTML templates.`); 243 | done(); 244 | } 245 | 246 | 247 | // JS task - (remains unchanged from previous version) 248 | function js() { 249 | return gulp 250 | .src([ 251 | './vendor/jquery/jquery.min.js', 252 | './vendor/bootstrap/js/bootstrap.bundle.min.js', 253 | './vendor/jquery-easing/jquery.easing.min.js', 254 | './vendor/easymde/easymde.min.js', 255 | './vendor/hammerjs/hammer.min.js', 256 | './vendor/fuse.js/fuse.min.js', 257 | './vendor/sortablejs/Sortable.min.js', 258 | generatedTemplatesJsPath, 259 | paths.js.src, 260 | ], 261 | { allowEmpty: true } 262 | ) 263 | .pipe(plumber()) 264 | .pipe(concat(paths.js.concatFile)) 265 | .pipe(header(banner, { 266 | pkg: pkg 267 | })) 268 | .pipe(gulp.dest(paths.js.dest)) 269 | .pipe(uglify()) 270 | .pipe(rename({ 271 | suffix: paths.js.minSuffix 272 | })) 273 | .pipe(gulp.dest(paths.js.minifiedDest)) 274 | .pipe(browsersync.stream()); 275 | } 276 | 277 | function etc() { 278 | return gulp 279 | .src(paths.etc.src) 280 | .pipe(gulp.dest(paths.etc.dest)) 281 | } 282 | 283 | function fonts() { 284 | return gulp 285 | .src(paths.fonts.src) 286 | .pipe(gulp.dest(paths.fonts.dest)) 287 | } 288 | 289 | // Watch files - Watches generated font CSS 290 | function watchFiles() { 291 | gulp.watch("./scss/**/*.scss", css); 292 | // Watch the generated font CSS file as well, trigger css task if it changes (e.g., after googlefonts runs) 293 | gulp.watch(generatedFontCssPath, css); 294 | gulp.watch([paths.js.src, `!${generatedTemplatesJsPath}`], js); 295 | gulp.watch(paths.templates.src, gulp.series(generateTemplatesJS, js)); 296 | // Watch the font list file, regenerate fonts and then css if it changes 297 | gulp.watch(paths.googleFonts.src, gulp.series(googlefonts, css)); 298 | gulp.watch("./**/*.html", browserSyncReload); 299 | } 300 | 301 | // Define complex tasks - Ensure googlefonts runs before css 302 | const vendor = gulp.series(clean, googlefonts, modules); 303 | const build = gulp.series(vendor, generateTemplatesJS, gulp.parallel(css, js, etc, fonts)); // css now depends on googlefonts completing first 304 | const watch = gulp.series(build, gulp.parallel(watchFiles, browserSync)); 305 | 306 | // Export tasks 307 | exports.css = css; 308 | exports.js = js; 309 | exports.clean = clean; 310 | exports.vendor = vendor; 311 | exports.build = build; 312 | exports.watch = watch; 313 | exports.default = build; 314 | exports.generateTemplatesJS = generateTemplatesJS; 315 | exports.googlefonts = googlefonts; 316 | -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | S Notepad 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | 21 |
22 | 23 |
24 |
25 | 26 | 69 |
70 | 71 | 79 |
80 | 81 |
82 |
83 |
84 |

Welcome to SNotePad!

85 |

Start by adding a folder using the button below:

86 | 89 |
90 |
91 |
92 |
93 |
94 | 95 | 96 | 97 | 98 | 113 |
114 |
115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /html/js/backend.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | "use strict"; // Start of use strict 3 | const VERSION = 3 4 | 5 | const DEFAULT_VALUES = { 6 | 'paths': {} 7 | } 8 | 9 | const DEBUG = (new URLSearchParams(window.location.search)).get('debug') === 'true' 10 | 11 | console.log('version', VERSION) 12 | 13 | setTimeout(() => { 14 | if (typeof AndroidInterface === 'undefined' && DEBUG === false) { 15 | window.showToast("Android interface not available. Probably I'm incompatible with your phone altogether :-("); 16 | } 17 | }, 18 | 1000 19 | ) 20 | 21 | let callbacks = {} 22 | 23 | window.requestFolderSelection = () => { 24 | if (DEBUG) { 25 | let i = Math.floor(Math.random() * 9999) + 1 26 | window.requestFolderSelectionSuccess('/storage/path-to-folder-' + i + '/Folder ' + i) 27 | return 28 | } 29 | 30 | AndroidInterface.initiateFolderSelection(); 31 | } 32 | 33 | window.requestFolderSelectionSuccess = (path) => { 34 | let paths = JSON.parse(window.readPreferences('paths')); 35 | if (paths === null) { 36 | paths = [] 37 | } 38 | 39 | paths.push(path) 40 | 41 | window.writePreferences('paths', JSON.stringify(paths)) 42 | window.uiUpdateFolders(paths) 43 | window.lunchFolderView(path) 44 | } 45 | 46 | window.requestScanFolder = (path, callback) => { 47 | callbacks['scanFolder' + path] = callback 48 | if (DEBUG) { 49 | let files = [] 50 | const numberWords = [ 51 | "", "one", "two", "three", "four", "five", 52 | "six", "seven", "eight", "nine", "ten" 53 | ]; 54 | for (let i = 1; i <= 10; i++) { 55 | const filename = 'File ' + i 56 | const content = numberWords[i] 57 | const date = new Date(); // Gets the current date and time 58 | date.setDate(date.getDate() - i) 59 | files.push({ 60 | 'filename': filename, 61 | 'content': content, 62 | 'date': date.toLocaleDateString() 63 | }) 64 | } 65 | window.scanFolderCallback(path, JSON.stringify(files)) 66 | 67 | return 68 | } 69 | 70 | AndroidInterface.initiateReadFolder(path, true) 71 | } 72 | 73 | window.scanFolderCallback = (path, folderContentJson, isError) => { 74 | callbacks['scanFolder' + path](path, JSON.parse(folderContentJson), isError) 75 | delete callbacks['scanFolder' + path] 76 | } 77 | 78 | window.releaseFolder = (path) => { 79 | const pathsJson = window.readPreferences('paths') 80 | let paths = JSON.parse(pathsJson); 81 | paths.splice( 82 | $.inArray(path, paths), 83 | 1 84 | ) 85 | if (!DEBUG) { 86 | AndroidInterface.releaseFolder(path); 87 | } 88 | 89 | window.writePreferences('paths', JSON.stringify(paths)) 90 | window.uiUpdateFolders(paths) 91 | } 92 | 93 | window.deleteFile = (path) => { 94 | if (!DEBUG) { 95 | AndroidInterface.deleteFile(path); 96 | } 97 | } 98 | 99 | window.readPreferences = (key) => { 100 | result = window.localStorage.getItem(key) 101 | if (result === undefined) { 102 | result = DEFAULT_VALUES[key] 103 | } 104 | 105 | return result 106 | } 107 | 108 | window.writePreferences = (key, value) => { 109 | window.localStorage.setItem(key, value); 110 | } 111 | 112 | window.requestReadFolder = (path, callback) => { 113 | callbacks['readFolder' + path] = callback 114 | if (DEBUG) { 115 | let files = [] 116 | for (let i = 1; i <= 10; i++) { 117 | const filename = 'File ' + i 118 | const date = new Date(); // Gets the current date and time 119 | date.setDate(date.getDate() - i) 120 | files.push({ 121 | 'filename': filename, 122 | 'date': date.toLocaleDateString() 123 | }) 124 | } 125 | window.readFolderCallback(path, JSON.stringify(files)) 126 | 127 | return 128 | } 129 | 130 | AndroidInterface.initiateReadFolder(path, false) 131 | } 132 | 133 | window.readFolderCallback = (path, folderContentJson, isError) => { 134 | callbacks['readFolder' + path]( 135 | path, 136 | isError ? folderContentJson : JSON.parse(folderContentJson), 137 | isError 138 | ) 139 | delete callbacks['readFolder' + path] 140 | } 141 | 142 | window.requestReadFile = (path, callback) => { 143 | callbacks['readFile' + path] = callback 144 | if (DEBUG) { 145 | let content = '# Sample Content\n\n *' + path + '*\n\n' 146 | for (let i = 1; i <= 30; i++) { 147 | content = content + '- Line ' + i + '\n' 148 | } 149 | 150 | window.readFileCallback(path, content) 151 | 152 | return 153 | } 154 | 155 | AndroidInterface.initiateReadFile(path) 156 | } 157 | 158 | window.readFileCallback = (path, fileContent, isError) => { 159 | callbacks['readFile' + path](path, fileContent, isError) 160 | delete callbacks['readFile' + path] 161 | } 162 | 163 | window.requestWriteFile = (path, fileContent, callback) => { 164 | callbacks['writeFile' + path] = callback 165 | if (DEBUG) { 166 | window.writeFileCallback(path, path) 167 | 168 | return 169 | } 170 | 171 | AndroidInterface.initiateWriteFile(path, fileContent) 172 | } 173 | 174 | window.writeFileCallback = (originalPath, path, isError) => { 175 | callbacks['writeFile' + originalPath](path, isError) 176 | delete callbacks['writeFile' + originalPath] 177 | } 178 | 179 | if (DEBUG) { 180 | $("#btnBack").removeClass('d-none') 181 | } 182 | })(jQuery); // End of use strict 183 | -------------------------------------------------------------------------------- /html/js/confirmModal.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | "use strict"; // Start of use strict 3 | // Get the modal instance 4 | const confirmModal = new bootstrap.Modal($('#confirmModal')); 5 | 6 | window.confirmModal = ( 7 | title, 8 | message, 9 | confirmCallback, 10 | cancelCallback 11 | ) => { 12 | $('#confirmModalLabel').text(title) 13 | $('#confirmModalMessage').html(message) 14 | 15 | // Handle the click on the final confirmation button inside the modal 16 | $('#confirmBtn').off('click').on('click', () => { 17 | // Hide the modal 18 | confirmModal.hide() 19 | 20 | confirmCallback() 21 | }); 22 | 23 | // Handle the click on the final confirmation button inside the modal 24 | let cancelButton = $('#cancelBtn'); 25 | cancelButton.off('click'); 26 | if (cancelCallback) { 27 | cancelButton.on('click', cancelCallback); 28 | } 29 | 30 | // Show the modal 31 | confirmModal.show(); 32 | } 33 | })(jQuery); // End of use strict 34 | -------------------------------------------------------------------------------- /html/js/folderView.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | "use strict"; // Start of use strict 3 | 4 | // Fuse.js options 5 | const FUSE_OPTIONS = { 6 | // includeScore: true, // Uncomment to see scores for debugging/ranking 7 | // threshold: 0.4, // Adjusts fuzziness (0=exact match, 1=match anything). Default 0.6 is often fine. 8 | keys: [ 9 | { 10 | name: 'filename', // Field to search 11 | weight: 0.7 // Higher weight = more importance 12 | }, 13 | { 14 | name: 'content', // Field to search 15 | weight: 0.3 // Lower weight = less importance 16 | } 17 | // 'date' is intentionally omitted, so it won't be searched 18 | ] 19 | }; 20 | 21 | var currentPath = '' 22 | var folderContent = [] 23 | 24 | // Function to set the initial/default icon for the sort dropdown toggle 25 | function setDefaultSortIcon(sort, order) { 26 | 27 | $('#sortDropdown i').removeClass().addClass( 28 | 'fas fa-' 29 | + ( 30 | sort === 'filename' 31 | ? (order === 'asc' ? 'arrow-down-a-z' : 'arrow-up-z-a') 32 | : (order === 'asc' ? 'arrow-down-short-wide' : 'arrow-up-wide-short') 33 | ) 34 | + ' fa-fw' 35 | ); // Remove existing classes, add new ones 36 | } 37 | 38 | function renderFolderContent() { 39 | const $itemsDiv = $("#items"); 40 | $itemsDiv.empty() 41 | $.each(folderContent, (i, file) => { 42 | $itemsDiv.append( 43 | window.renderTemplate( 44 | { 45 | 'path': currentPath + '/' + file.filename, 46 | 'basename': file.filename, 47 | 'date': file.date, 48 | 'id': i 49 | }, 50 | 'folderView-file' 51 | ) 52 | ) 53 | }) 54 | 55 | let fileToDeleteId = null; // Variable to store the ID of the file to delete 56 | let $itemToDelete = null; // Variable to store the jQuery element to remove 57 | let fileToDeleteName = null 58 | 59 | $('.btn-delete').on('click', function(button) { 60 | fileToDeleteId = $(this).data('id') 61 | $itemToDelete = $('#div-file-' + fileToDeleteId) 62 | fileToDeleteName = $('#div-filename-' + fileToDeleteId).text() 63 | 64 | window.confirmModal( 65 | 'Delete File', 66 | 'Are you sure you want to permanently delete this file?
' + fileToDeleteName, 67 | () => { 68 | if (fileToDeleteId !== null && $itemToDelete) { 69 | let path = $('#div-file-path-' + fileToDeleteId).text() 70 | console.log("Deleting file with path:", path); 71 | window.deleteFile(path) 72 | 73 | $itemToDelete.fadeOut(300, function() { 74 | $(this).remove(); 75 | }); 76 | 77 | window.showToast('File deleted:
' + fileToDeleteName) 78 | // Reset the stored variables 79 | fileToDeleteId = null; 80 | $itemToDelete = null; 81 | } else { 82 | console.error("Could not find file ID or element to delete."); 83 | } 84 | } 85 | ) 86 | }) 87 | 88 | $('.div-file-open').on('click', function(button) { 89 | let id = $(this).data('id') 90 | let path = $('#div-file-path-' + id).text() 91 | window.editorOpenFile(path) 92 | }) 93 | } 94 | 95 | function arrangeitems(sort, order) { 96 | folderContent = window.sortArray(folderContent, sort, order) 97 | renderFolderContent() 98 | window.writePreferences('sort', sort) 99 | window.writePreferences('order', order) 100 | 101 | setDefaultSortIcon(sort, order) 102 | } 103 | 104 | function performSearch(fuse, data, searchTerm) { 105 | searchTerm = searchTerm.trim(); // Remove leading/trailing whitespace 106 | console.log("Search triggered. Term:", searchTerm); 107 | 108 | let results = []; 109 | if (searchTerm) { 110 | // Perform the search using Fuse.js 111 | // Fuse returns an array of objects: { item: originalObject, refIndex: ..., score: ... } 112 | folderContent = fuse.search(searchTerm).map(result => result.item);; 113 | } else { 114 | folderContent = data; 115 | } 116 | 117 | // Display the results 118 | renderFolderContent(); 119 | } 120 | 121 | let confirmExit = (callback) => { 122 | callback(true) 123 | } 124 | 125 | // Define the function that should be called for settings 126 | window.lunchFolderView = (path) => { 127 | currentPath = path 128 | window.showLoading('Loading folder...') 129 | console.log("lunchFolderView function called!"); 130 | 131 | window.setNavBar('navbar-folderView', {}); 132 | 133 | window.setPage( 134 | 'folderView', 135 | { 136 | 'path': path, 137 | 'basename': window.getHumanReadableBasename(path) 138 | } 139 | ); 140 | $('#btn-add-file').on('click', function() { 141 | let path = $("#div-folderView-path").text() 142 | window.editorNewFile(path) 143 | }); 144 | 145 | window.requestReadFolder(path, window.folderViewReadFolderCallback) 146 | window.writePreferences('lastPath', path) 147 | window.historyPush( 148 | lunchFolderView, 149 | [path], 150 | confirmExit 151 | ) 152 | } 153 | 154 | window.folderViewReadFolderCallback = (path, content, isError) => { 155 | if (isError) { 156 | window.showToast(content, isError) 157 | 158 | return 159 | } 160 | 161 | folderContent = content 162 | const lastSort = window.readPreferences('sort') 163 | const lastOrder = window.readPreferences('order') 164 | arrangeitems( 165 | lastSort === null ? 'date' : lastSort, 166 | lastOrder === null ? 'desc' : lastOrder 167 | ) 168 | 169 | // Event handler for when a sort option is clicked 170 | $('[aria-labelledby="sortDropdown"] .dropdown-item').on('click', function(e) { 171 | e.preventDefault(); // Prevent default anchor behavior 172 | 173 | // Find the text of the selected option 174 | let sort = $(this).find('.font-weight-bold').text(); 175 | switch (sort) { 176 | case 'Name A - Z': arrangeitems('filename', 'asc'); break 177 | case 'Name Z - A': arrangeitems('filename', 'desc'); break 178 | case 'Date Newest First': arrangeitems('date', 'desc'); break 179 | default: arrangeitems('date', 'asc'); break 180 | } 181 | }); 182 | 183 | var data = null; 184 | var fuse = null; 185 | let searchHandler = function() { 186 | let searchTerm = ''; 187 | if ($(this).is('input')) { 188 | searchTerm = $(this).val(); 189 | } else { // It's the button click 190 | searchTerm = $(this).closest('.input-group').find('.folder-search-input').val(); 191 | } 192 | 193 | if (fuse === null) { 194 | // Initialize Fuse with the data and options 195 | // This creates the index the first time. 196 | let scanSuccessHandler = (path, scannedData, isError) => { 197 | if (isError) { 198 | window.showToast(path, isError) 199 | 200 | return 201 | } 202 | 203 | data = scannedData; 204 | fuse = new Fuse(data, FUSE_OPTIONS); 205 | window.hideLoading() 206 | performSearch(fuse, data, searchTerm) 207 | } 208 | window.showLoading('Indexing folder...') 209 | window.requestScanFolder(currentPath, scanSuccessHandler) 210 | 211 | return 212 | } 213 | 214 | performSearch(fuse, data, searchTerm) 215 | } 216 | 217 | // Attach event listener for typing in the search input fields 218 | $('.folder-search-input').on('input', searchHandler); 219 | 220 | // Attach event listener for clicking the search buttons 221 | // Select buttons within the input group append divs 222 | $('.input-group-append .btn').on('click', searchHandler); 223 | 224 | // Also handle form submission (e.g., pressing Enter in the input) 225 | $('form.navbar-search').on('submit', function(event) { 226 | event.preventDefault(); // Prevent default form submission 227 | searchHandler.call($(this).find('.folder-search-input')); 228 | }); 229 | 230 | window.hideSidebar() 231 | window.hideLoading() 232 | } 233 | 234 | })(jQuery); // End of use strict 235 | -------------------------------------------------------------------------------- /html/js/history.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | "use strict"; // Start of use strict 3 | let history = [] 4 | 5 | window.historyPush = ( 6 | actionHandler, 7 | args, 8 | confirmExit 9 | ) => { 10 | history.push({ 11 | 'actionHandler': actionHandler, 12 | 'args': args, 13 | 'confirmExit': confirmExit 14 | }) 15 | console.log('historyPush', history) 16 | } 17 | 18 | const confirmExitResultHandler = (allowed) => { 19 | console.log('confirmExitResultHandler', allowed, history) 20 | if (!allowed) { 21 | return 22 | } 23 | 24 | if (history.length < 2) { 25 | return //Nothing to go back to 26 | } 27 | 28 | //First pop the current action as we don't need it anymore 29 | history.pop() 30 | //Now we get to the action the user was doing before the current one: 31 | lastAction = history.pop() 32 | if (lastAction === undefined) { 33 | return 34 | } 35 | 36 | lastAction['actionHandler'](...lastAction['args']) 37 | } 38 | 39 | window.handleBackPress = () => { 40 | console.log('handleBackPress', history) 41 | //The last item in the history stack, is always the current action. 42 | const currentAction = history.at(-1) 43 | if (currentAction === undefined) { 44 | return 45 | } 46 | 47 | currentAction['confirmExit'](confirmExitResultHandler) 48 | } 49 | 50 | window.getCurrentAction = () => { 51 | lastHistoryItem = history.at(-1) 52 | if (lastHistoryItem === undefined) { 53 | return null 54 | } 55 | 56 | return lastHistoryItem['actionHandler'] ?? null 57 | } 58 | 59 | $("#btnBack").on('click', window.handleBackPress) 60 | })(jQuery); // End of use strict 61 | -------------------------------------------------------------------------------- /html/js/loading.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | "use strict"; // Start of use strict 3 | window.showLoading = (prompt) => { 4 | const $loadingOverlay = $('#loading-overlay'); 5 | $('#loading-prompt').text(prompt) 6 | $loadingOverlay.removeClass('hidden'); 7 | } 8 | 9 | window.hideLoading = () => { 10 | $('#loading-overlay').addClass('hidden'); 11 | } 12 | })(jQuery); // End of use strict 13 | -------------------------------------------------------------------------------- /html/js/page.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | let confirmExit = (callback) => { 3 | callback(true) 4 | } 5 | 6 | window.lunchPage = (page) => { 7 | console.log("lunchPage function called: " + page); 8 | 9 | const title = page[0].toUpperCase() + page.slice(1) 10 | window.setNavBar('navbar-page', {'title': title}); 11 | 12 | window.setPage(page, {}); 13 | 14 | window.hideSidebar() 15 | window.historyPush( 16 | window.lunchPage, 17 | [page], 18 | confirmExit 19 | ) 20 | } 21 | })(jQuery); // End of use strict 22 | 23 | -------------------------------------------------------------------------------- /html/js/settings.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | let confirmExit = (callback) => { 3 | callback(true) 4 | } 5 | 6 | // --- Update UI Controls --- 7 | const updateAppearanceUI = (selectedTheme, selectedStyle, selectedMode) => { 8 | // Update Theme Selector 9 | const themeSelect = document.getElementById('themeSelect'); 10 | if (themeSelect) { 11 | themeSelect.value = selectedTheme; 12 | } else { 13 | console.warn("Theme select element not found"); 14 | } 15 | 16 | const styleSelect = document.getElementById('styleSelect'); 17 | if (styleSelect) { 18 | styleSelect.value = selectedStyle; 19 | } else { 20 | console.warn("Style select element not found"); 21 | } 22 | 23 | // Update Mode Radios 24 | const modeRadio = document.querySelector(`input[name="ui-mode"][value="${selectedMode}"]`); 25 | if (modeRadio) { 26 | modeRadio.checked = true; 27 | } else { 28 | console.warn(`Mode radio for value "${selectedMode}" not found`); 29 | // Fallback check 'auto' if invalid value somehow stored 30 | const autoRadio = document.querySelector('input[name="ui-mode"][value="auto"]'); 31 | if (autoRadio) autoRadio.checked = true; 32 | } 33 | }; 34 | 35 | window.lunchSettings = () => { 36 | console.log("lunchSettings function called!"); 37 | 38 | window.setNavBar('navbar-settings', {}); 39 | 40 | window.setPage('settings', {}); 41 | 42 | // --- Initialize Appearance --- 43 | const initialTheme = window.getStoredTheme(); 44 | const initialStyle = window.getStoredStyle(); 45 | const initialMode = window.getStoredMode(); 46 | updateAppearanceUI(initialTheme, initialStyle, initialMode); 47 | 48 | // --- Event Listener for Theme Selector --- 49 | const themeSelect = document.getElementById('themeSelect'); 50 | if(themeSelect) { 51 | themeSelect.addEventListener('change', (event) => { 52 | const newTheme = event.target.value; 53 | const currentStyle = window.getStoredStyle(); 54 | const currentMode = window.getStoredMode(); // Get current mode setting 55 | window.setStoredTheme(newTheme); // Store the new theme choice 56 | window.applyAppearance(newTheme, currentStyle, currentMode); // Apply the new theme with current mode 57 | }); 58 | } else { 59 | console.error("Theme select element not found during event listener setup"); 60 | } 61 | 62 | // --- Event Listener for Style Selector --- 63 | const styleSelect = document.getElementById('styleSelect'); 64 | if(styleSelect) { 65 | styleSelect.addEventListener('change', (event) => { 66 | const currentTheme = window.getStoredTheme(); 67 | const newStyle = event.target.value; 68 | const currentMode = window.getStoredMode(); // Get current mode setting 69 | window.setStoredStyle(newStyle); // Store the new theme choice 70 | window.applyAppearance(currentTheme, newStyle, currentMode); // Apply the new theme with current mode 71 | }); 72 | } else { 73 | console.error("Style select element not found during event listener setup"); 74 | } 75 | 76 | // --- Event Listeners for Mode Radio Buttons --- 77 | document.querySelectorAll('input[name="ui-mode"]').forEach(radio => { 78 | radio.addEventListener('change', (event) => { 79 | const newMode = event.target.value; 80 | const currentStyle = window.getStoredStyle(); 81 | const currentTheme = window.getStoredTheme(); // Get current theme setting 82 | window.setStoredMode(newMode); // Store the new mode choice 83 | window.applyAppearance(currentTheme, currentStyle, newMode); // Apply the current theme with new mode 84 | }); 85 | }); 86 | 87 | $('#btn-add-folder').on('click', function() { 88 | window.requestFolderSelection() 89 | }) 90 | 91 | window.settingsUpdateFolders( 92 | JSON.parse( 93 | window.readPreferences('paths') 94 | ) 95 | ) 96 | 97 | window.hideSidebar() 98 | window.historyPush( 99 | window.lunchSettings, 100 | [], 101 | confirmExit 102 | ) 103 | } 104 | 105 | window.settingsUpdateFolders = (paths) => { 106 | const $foldersDiv = $("#folders"); 107 | if ($foldersDiv === undefined) { 108 | return 109 | } 110 | 111 | $foldersDiv.empty() 112 | $.each(paths, (i, path) => { 113 | $foldersDiv.append( 114 | window.renderTemplate( 115 | { 116 | 'basename': window.getHumanReadableBasename(path), 117 | 'path': path, 118 | 'id': i 119 | }, 120 | 'settings-folder' 121 | ) 122 | ) 123 | }) 124 | 125 | 126 | let folderToReleaseId = null; // Variable to store the ID of the file to delete 127 | let $itemToDelete = null; // Variable to store the jQuery element to remove 128 | let folderToReleaseName = null 129 | 130 | $('.btn-delete').on('click', function(button) { 131 | folderToReleaseId = $(this).data('id') 132 | $itemToDelete = $('#div-folder-' + folderToReleaseId) 133 | folderToReleaseName = $('#div-folder-name-' + folderToReleaseId).text() 134 | 135 | window.confirmModal( 136 | 'Release Folder', 137 | 'Are you sure you want to release this folder?
Folder and its contents still remain on your device

' + folderToReleaseName, 138 | () => { 139 | if (folderToReleaseId !== null && $itemToDelete) { 140 | let path = $('#code-folder-path-' + folderToReleaseId).text() 141 | console.log("Releasing folder with path:", path); 142 | window.releaseFolder(path) 143 | 144 | $itemToDelete.fadeOut(300, function() { 145 | $(this).remove(); 146 | }); 147 | 148 | 149 | window.showToast('Folder released:
' + folderToReleaseName) 150 | // Reset the stored variables 151 | folderToReleaseId = null; 152 | $itemToDelete = null; 153 | } else { 154 | console.error("Could not find folder ID or element to delete."); 155 | } 156 | } 157 | ) 158 | }) 159 | // Destroy previous instance if it exists to prevent duplicates 160 | if (window.settingsSortableInstance) { 161 | window.settingsSortableInstance.destroy(); 162 | } 163 | window.settingsSortableInstance = new Sortable(($("#folders")[0]), { 164 | handle: '.drag-handle', 165 | animation: 150, 166 | onEnd: () => { 167 | let paths = []; 168 | 169 | $('#folders').find('.folder-path').each(function() { 170 | paths.push($(this).text()) 171 | }) 172 | window.writePreferences('paths', JSON.stringify(paths)) 173 | window.sidebarUpdateFolders(paths) 174 | } 175 | }) 176 | } 177 | })(jQuery); // End of use strict 178 | 179 | -------------------------------------------------------------------------------- /html/js/sidebar.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | "use strict"; // Start of use strict 3 | const sidebar = $("#accordionSidebar") 4 | 5 | // Toggle the side navigation 6 | $("#sidebarToggle, #sidebarToggleTop").on('click', function(e) { 7 | $("body").toggleClass("sidebar-toggled"); 8 | sidebar.toggleClass("toggled"); 9 | if (sidebar.hasClass("toggled")) { 10 | $('.sidebar .collapse').collapse('hide'); 11 | }; 12 | }); 13 | 14 | // Close any open menu accordions when window is resized below 768px 15 | $(window).resize(function() { 16 | if ($(window).width() < 768) { 17 | $('.sidebar .collapse').collapse('hide'); 18 | }; 19 | 20 | // Toggle the side navigation when window is resized below 480px 21 | if ($(window).width() < 480 && !sidebar.hasClass("toggled")) { 22 | $("body").addClass("sidebar-toggled"); 23 | sidebar.addClass("toggled"); 24 | $('.sidebar .collapse').collapse('hide'); 25 | }; 26 | }); 27 | 28 | // Prevent the content wrapper from scrolling when the fixed side navigation hovered over 29 | $('body.fixed-nav .sidebar').on('mousewheel DOMMouseScroll wheel', function(e) { 30 | if ($(window).width() > 768) { 31 | var e0 = e.originalEvent, 32 | delta = e0.wheelDelta || -e0.detail; 33 | this.scrollTop += (delta < 0 ? 1 : -1) * 30; 34 | e.preventDefault(); 35 | } 36 | }); 37 | 38 | window.sidebarUpdateFolders = (paths) => { 39 | sidebar.find(".sidebar-folder").remove(); 40 | const $div = $("#sidebarFoldersBegin"); 41 | 42 | // Insert the new HTML content provided in the 'template' variable 43 | // immediately after the button. 44 | let $lastElement = $div 45 | $.each(paths, (i, path) => { 46 | let active = '' 47 | let editorCurrentPath = window.getEditorCurrentPath() 48 | if (editorCurrentPath !== null) { 49 | if (path === window.dirname(editorCurrentPath)) { 50 | active = 'active' 51 | } 52 | } else { 53 | let folderViewCurrentPath = window.getFolderViewCurrentPath() 54 | if (folderViewCurrentPath !== null && path === folderViewCurrentPath) { 55 | active = 'active' 56 | } 57 | } 58 | 59 | const $newItem = $( 60 | window.renderTemplate( 61 | { 62 | 'basename': window.getHumanReadableBasename(path), 63 | 'path': path, 64 | 'id': i, 65 | 'active': active 66 | }, 67 | 'sidebar-folder' 68 | ) 69 | ) 70 | 71 | $lastElement.after( 72 | $newItem 73 | ) 74 | $lastElement = $newItem 75 | }) 76 | } 77 | 78 | window.sidebarHighlightItem = ($item) => { 79 | // Find the parent li.nav-item of the clicked link 80 | let $parentLi = $item.closest('li.nav-item') 81 | 82 | // Find all li.nav-item elements within the same parent container 83 | // (e.g., all direct children of the surrounding UL/OL) 84 | // and remove the 'active' class from all of them. 85 | // This ensures only one item is active at a time within this group. 86 | sidebar.children('li.nav-item').removeClass('active') 87 | 88 | // Add the 'active' class specifically to the parent li of the clicked link. 89 | $parentLi.addClass('active') 90 | } 91 | 92 | $(document).on('click', ".sidebar .nav-link", function(event) { 93 | // Prevent the default link behavior (e.g., navigating to '#') 94 | event.preventDefault(); 95 | window.sidebarHighlightItem($(this)) 96 | }) 97 | $(document).on('click', '.sidebar-folder', function(event) { 98 | // Prevent the default link behavior (e.g., navigating to '#') 99 | event.preventDefault(); 100 | 101 | // 'this' refers to the specific '.sidebar-folder' link that was clicked. 102 | let $clickedLink = $(this); 103 | 104 | 105 | // Get the value from the 'data-id' attribute of the clicked link 106 | let folderId = $clickedLink.data('id'); // e.g., "123" 107 | 108 | // Construct the specific ID for the hidden div using the retrieved folderId 109 | // This creates a selector like "#sidebar-folder-item-path-123" 110 | let pathDivSelector = '#sidebar-folder-item-path-' + folderId; 111 | 112 | // Find the specific hidden div using its ID, searching only *within* 113 | // the clicked link's descendants for efficiency and context. 114 | let $pathDiv = $clickedLink.find(pathDivSelector); 115 | 116 | let pathValue = $pathDiv.text(); 117 | 118 | // Display the path in an alert dialog 119 | window.lunchFolderView(pathValue) 120 | }); 121 | 122 | window.hideSidebar = () => { 123 | const $button = $("#sidebarToggleTop"); 124 | var isSmallScreen = $button.is(':visible'); 125 | 126 | // Check if the sidebar is currently shown (does not have 'toggled' class) 127 | var isSidebarVisible = !$('#accordionSidebar').hasClass('toggled'); 128 | 129 | // If it's a small screen AND the sidebar is visible 130 | if (isSmallScreen && isSidebarVisible) { 131 | // Trigger a click on the button to hide the sidebar 132 | $button.trigger('click'); 133 | } 134 | } 135 | 136 | $('.sidebar-item-simple-page').on('click', function() { 137 | var pageId = $(this).attr('id'); 138 | lunchPage(pageId); 139 | }); 140 | })(jQuery); // End of use strict 141 | -------------------------------------------------------------------------------- /html/js/sort.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | "use strict"; // Start of use strict 3 | 4 | /** 5 | * Sorts the array by filename in ascending order (A-Z). 6 | * Note: Modifies the original array in place. 7 | * @param {Array} arr The array to sort. 8 | * @returns {Array} The sorted array. 9 | */ 10 | function sortByFilenameAsc(arr) { 11 | if (!Array.isArray(arr)) { 12 | console.error("Input must be an array."); 13 | return arr; // Return original input or handle error appropriately 14 | } 15 | arr.sort((a, b) => { 16 | // Use localeCompare for proper string comparison 17 | return a.filename.localeCompare(b.filename, undefined, { numeric: true }) 18 | }); 19 | return arr; 20 | } 21 | 22 | /** 23 | * Sorts the array by filename in descending order (Z-A). 24 | * Note: Modifies the original array in place. 25 | * @param {Array} arr The array to sort. 26 | * @returns {Array} The sorted array. 27 | */ 28 | function sortByFilenameDesc(arr) { 29 | if (!Array.isArray(arr)) { 30 | console.error("Input must be an array."); 31 | return arr; 32 | } 33 | arr.sort((a, b) => { 34 | // Reverse the comparison for descending order 35 | return b.filename.localeCompare(a.filename, undefined, { numeric: true }) 36 | }); 37 | return arr; 38 | } 39 | 40 | /** 41 | * Sorts the array by date, newest first (descending). 42 | * Note: Modifies the original array in place. 43 | * @param {Array} arr The array to sort. 44 | * @returns {Array} The sorted array. 45 | */ 46 | function sortByDateNewest(arr) { 47 | if (!Array.isArray(arr)) { 48 | console.error("Input must be an array."); 49 | return arr; 50 | } 51 | arr.sort((a, b) => { 52 | // Convert date strings to Date objects for comparison 53 | const dateA = new Date(a.date); 54 | const dateB = new Date(b.date); 55 | // Subtracting dates gives the difference in milliseconds. 56 | // dateB - dateA gives descending order (newest first). 57 | return dateB - dateA; 58 | }); 59 | return arr; 60 | } 61 | 62 | /** 63 | * Sorts the array by date, oldest first (ascending). 64 | * Note: Modifies the original array in place. 65 | * @param {Array} arr The array to sort. 66 | * @returns {Array} The sorted array. 67 | */ 68 | function sortByDateOldest(arr) { 69 | if (!Array.isArray(arr)) { 70 | console.error("Input must be an array."); 71 | return arr; 72 | } 73 | arr.sort((a, b) => { 74 | // Convert date strings to Date objects for comparison 75 | const dateA = new Date(a.date); 76 | const dateB = new Date(b.date); 77 | // dateA - dateB gives ascending order (oldest first). 78 | return dateA - dateB; 79 | }); 80 | return arr; 81 | } 82 | 83 | window.sortArray = (array, sort, order) => { 84 | if (sort === 'filename') { 85 | if (order === 'asc') { 86 | return sortByFilenameAsc(array) 87 | } 88 | 89 | return sortByFilenameDesc(array) 90 | } else { 91 | if (order === 'asc') { 92 | return sortByDateOldest(array) 93 | } 94 | 95 | return sortByDateNewest(array) 96 | } 97 | } 98 | })(jQuery); // End of use strict 99 | -------------------------------------------------------------------------------- /html/js/template.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | function renderTemplate(data, templateName) { 3 | templateString = window.templates[templateName] 4 | // Ensure data is actually an array and not empty 5 | if (!$.isPlainObject(data)) { 6 | console.warn("renderTemplate: data is invalid."); 7 | return ''; // Return empty string if no data 8 | } 9 | 10 | // Ensure templateString is a string 11 | if (typeof templateString !== 'string') { 12 | console.error("renderTemplate: templateString must be a string."); 13 | return ''; // Return empty string if template is invalid 14 | } 15 | 16 | // Iterate over each object in the data 17 | $.each(data, (key, value) => { 18 | templateString = templateString.replaceAll('{' + key + '}', value) 19 | }); 20 | 21 | // Return the complete generated HTML string 22 | return templateString; 23 | } 24 | window.renderTemplate = renderTemplate 25 | })(jQuery); // End of use strict 26 | -------------------------------------------------------------------------------- /html/js/toast.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | "use strict"; // Start of use strict 3 | 4 | // Function to show an error toast 5 | window.showToast = (message, isError) => { 6 | // Generate a unique ID for the new toast 7 | const uniqueId = 'toast-' + Date.now() 8 | 9 | // Append the cloned toast to the container 10 | $('#toastContainer').append( 11 | window.renderTemplate( 12 | { 13 | 'id': uniqueId, 14 | 'message': message, 15 | 'title': isError ? 'Error' : 'Info', 16 | 'bg': isError ? 'danger' : 'info', 17 | 'role': isError ? 'alert' : 'info', 18 | }, 19 | 'toast' 20 | ) 21 | ) 22 | 23 | // Get the native DOM element for Bootstrap and Hammer.js 24 | const toastElement = $('#' + uniqueId)[0] 25 | debugger; 26 | 27 | // Initialize Bootstrap Toast 28 | const bsToast = new bootstrap.Toast(toastElement, { 29 | // autohide: false, // Already set in HTML, but can be set here too 30 | // delay: 5000 // Optional: if you want autohide after a delay 31 | }); 32 | 33 | // --- Hammer.js Swipe Integration --- 34 | const hammer = new Hammer(toastElement); 35 | 36 | // Enable horizontal swiping 37 | hammer.get('swipe').set({ direction: Hammer.DIRECTION_HORIZONTAL }); 38 | 39 | // Listen for swipeleft or swiperight 40 | hammer.on('swipeleft swiperight', function(ev) { 41 | bsToast.hide(); // Hide the toast on swipe 42 | }); 43 | // --- End Hammer.js Integration --- 44 | 45 | // Show the toast 46 | bsToast.show(); 47 | 48 | // Optional: Remove the toast from DOM after it's hidden to prevent buildup 49 | toastElement.addEventListener('hidden.bs.toast', function () { 50 | hammer.destroy(); // Clean up Hammer instance 51 | }); 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /html/js/ui.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | "use strict"; // Start of use strict 3 | // --- Preference Keys --- 4 | const THEME_STORAGE_KEY = 'app-theme'; 5 | const STYLE_STORAGE_KEY = 'app-style'; 6 | const MODE_STORAGE_KEY = 'ui-mode'; // Assuming this is what getStoredTheme/setStoredTheme used 7 | 8 | function setNavBar(templateName, data) { 9 | // Select the button with the ID 'sidebarToggleTop' using jQuery 10 | const $button = $("#btnBack"); 11 | 12 | // Find all sibling elements that come *after* the button 13 | // and remove them from the DOM. 14 | $button.nextAll().remove(); 15 | 16 | // Insert the new HTML content provided in the 'template' variable 17 | // immediately after the button. 18 | $button.after( 19 | window.renderTemplate( 20 | data, 21 | templateName 22 | ) 23 | ); 24 | } 25 | 26 | window.setNavBar = setNavBar 27 | 28 | function setPage(templateName, data) { 29 | const $page = $('#page') 30 | $page.empty() 31 | $page.html( 32 | window.renderTemplate( 33 | data, 34 | templateName 35 | ) 36 | ); 37 | } 38 | window.setPage = setPage 39 | 40 | function navigate(elementId) { 41 | console.log("Navigate called with ID:", elementId); // For debugging 42 | 43 | const prefix = 'sidebar-item-'; 44 | 45 | // Check if the ID starts with the expected prefix 46 | if (elementId && elementId.startsWith(prefix)) { 47 | // Extract the part after "sidebar-item-" 48 | const key = elementId.substring(prefix.length); 49 | console.log("Extracted key:", key); // For debugging 50 | 51 | // Check if the extracted part is 'settings' 52 | if (key === 'settings') { 53 | window.lunchSettings(); // Call the specific function for settings 54 | } else { 55 | console.log("Key is not 'settings'."); // For debugging 56 | // You could add logic here for other keys if needed in the future 57 | } 58 | } else { 59 | console.log("ID does not start with 'sidebar-item-' or is null."); // For debugging 60 | } 61 | } 62 | 63 | // Wait for the DOM to be fully loaded before running jQuery code 64 | $(document).ready(function() { 65 | // Select the settings link by its ID and attach a click event handler 66 | $('#sidebar-item-settings').on('click', function(event) { 67 | // Prevent the default link behavior (e.g., following the '#' href) 68 | event.preventDefault(); 69 | 70 | // Get the ID of the element that was clicked ('sidebar-item-settings') 71 | const clickedElementId = this.id; 72 | 73 | // Call the navigate function, passing the ID 74 | navigate(clickedElementId); 75 | }); 76 | 77 | window.sidebarUpdateFolders( 78 | JSON.parse( 79 | window.readPreferences('paths') 80 | ) 81 | ) 82 | 83 | 84 | const lastPath = window.readPreferences('lastPath') 85 | if (lastPath !== undefined && lastPath !== null) { 86 | window.sidebarHighlightItem($('.sidebar .nav-link div:contains("' + lastPath + '")')) 87 | window.lunchFolderView(lastPath) 88 | } 89 | $('#btn-welcome-add-folder').on('click', function() { 90 | window.requestFolderSelection() 91 | }) 92 | 93 | window.hideLoading() 94 | }); 95 | 96 | // Scroll to top button appear 97 | $(document).on('scroll', function() { 98 | var scrollDistance = $(this).scrollTop(); 99 | if (scrollDistance > 100) { 100 | $('.scroll-to-top').fadeIn(); 101 | } else { 102 | $('.scroll-to-top').fadeOut(); 103 | } 104 | }); 105 | 106 | // Smooth scrolling using jQuery easing 107 | $(document).on('click', 'a.scroll-to-top', function(e) { 108 | var $anchor = $(this); 109 | $('html, body').stop().animate({ 110 | scrollTop: ($($anchor.attr('href')).offset().top) 111 | }, 1000, 'easeInOutExpo'); 112 | e.preventDefault(); 113 | }); 114 | 115 | window.uiUpdateFolders = (paths) => { 116 | window.sidebarUpdateFolders(paths) 117 | if (window.getCurrentAction() === window.lunchSettings) { 118 | window.settingsUpdateFolders(paths) 119 | } 120 | } 121 | 122 | // --- Getters for Preferences --- 123 | window.getStoredTheme = () => { 124 | let theme = localStorage.getItem(THEME_STORAGE_KEY) 125 | if (!theme) { 126 | theme = 'default' // Default to 'default' theme 127 | } 128 | 129 | return theme 130 | } 131 | 132 | // --- Getters for Preferences --- 133 | window.getStoredStyle = () => { 134 | let style = localStorage.getItem(STYLE_STORAGE_KEY) 135 | if (!style) { 136 | style = 'default' // Default to 'default' theme 137 | } 138 | 139 | return style 140 | } 141 | 142 | window.getStoredMode = () => { 143 | let mode = localStorage.getItem(MODE_STORAGE_KEY) 144 | if (!mode) { 145 | mode = 'auto' // Default to 'auto' mode 146 | } 147 | 148 | return mode 149 | } 150 | 151 | // --- Setters for Preferences --- 152 | window.setStoredTheme = (theme) => localStorage.setItem(THEME_STORAGE_KEY, theme); 153 | window.setStoredStyle = (style) => localStorage.setItem(STYLE_STORAGE_KEY, style); 154 | window.setStoredMode = (mode) => localStorage.setItem(MODE_STORAGE_KEY, mode); 155 | 156 | // --- Get Preferred Mode (Handles 'auto') --- 157 | const getPreferredMode = () => { 158 | const storedMode = getStoredMode(); 159 | if (storedMode && storedMode !== 'auto') { 160 | return storedMode; 161 | } 162 | // Check system preference if 'auto' or not set 163 | return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 164 | }; 165 | 166 | // --- Apply Theme, Style and Mode to DOM --- 167 | window.applyAppearance = (theme, style, mode) => { 168 | const effectiveMode = (mode === 'auto') ? getPreferredMode() : mode; 169 | console.log(`Applying Theme: ${theme}, Style: ${style}, Mode: ${mode}, Effective Mode: ${effectiveMode}`); 170 | 171 | // Set theme attribute on body (or html) 172 | document.documentElement.setAttribute('data-app-theme', theme); 173 | 174 | // Set style attribute on body (or html) 175 | document.documentElement.setAttribute('data-app-style', style); 176 | 177 | // Set Bootstrap's theme attribute on html (recommended by Bootstrap) 178 | document.documentElement.setAttribute('data-bs-theme', effectiveMode); 179 | }; 180 | 181 | // --- Initialize Appearance --- 182 | const initialTheme = window.getStoredTheme(); // Default to 'default' theme 183 | const initialStyle = window.getStoredStyle(); // Default to 'default' style 184 | const initialMode = window.getStoredMode(); // Default to 'auto' mode 185 | window.applyAppearance(initialTheme, initialStyle, initialMode); 186 | 187 | // Add listener for system color scheme changes if mode is 'auto' 188 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { 189 | const currentSelectedMode = getStoredMode() || 'auto'; 190 | if (currentSelectedMode === 'auto') { 191 | const currentTheme = getStoredTheme() || 'default'; 192 | const currentStyle = getStoredStyle() || 'default'; 193 | window.applyAppearance(currentTheme, currentStyle, 'auto'); // Re-apply to update effective mode 194 | } 195 | }); 196 | })(jQuery); // End of use strict 197 | -------------------------------------------------------------------------------- /html/js/util.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | "use strict"; // Start of use strict 3 | window.basename = (path) => { 4 | // Split by either forward slash / or backslash \ 5 | return path.split('/').pop(); 6 | } 7 | 8 | window.dirname = (path) => { 9 | let lastSeparatorIndex = path.lastIndexOf('/'); 10 | 11 | if (lastSeparatorIndex === -1) { 12 | // No '/' found. Standard dirname is '.' (current directory) 13 | return '.'; 14 | } else if (lastSeparatorIndex === 0) { 15 | // Path starts with '/', e.g., "/file.txt" or just "/" 16 | // The directory is the root "/" 17 | return '/'; 18 | } else { 19 | // Found '/' somewhere other than the start. 20 | // Return the substring from the beginning up to the last '/' 21 | // Handles "/path/to/file" -> "/path/to" 22 | // Also handles "/path/to/" -> "/path/to" (removes trailing slash implicitly) 23 | return path.substring(0, lastSeparatorIndex); 24 | } 25 | } 26 | 27 | window.getHumanReadableBasename = (path) => { 28 | return window.basename( 29 | decodeURIComponent( 30 | window.basename(path) 31 | ).replaceAll(':', ' : ') 32 | ) 33 | } 34 | })(jQuery); // End of use strict 35 | -------------------------------------------------------------------------------- /html/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/html/logo.png -------------------------------------------------------------------------------- /html/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "SNotePad HTML", 3 | "name": "snotepad-html", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "start": "node_modules/.bin/gulp watch" 7 | }, 8 | "description": "An open source Bootstrap 4 admin theme.", 9 | "keywords": [ 10 | "css", 11 | "sass", 12 | "html", 13 | "responsive", 14 | "theme", 15 | "template", 16 | "admin", 17 | "app" 18 | ], 19 | "homepage": "https://startbootstrap.com/theme/sb-admin-2", 20 | "bugs": { 21 | "url": "https://github.com/aario/snotepad/issues", 22 | "email": "feedback@startbootstrap.com" 23 | }, 24 | "license": "MIT", 25 | "author": "Aario Shahbany", 26 | "contributors": [], 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/aario/snotepad.git" 30 | }, 31 | "dependencies": { 32 | "bootstrap": "^5.3.5", 33 | "chart.js": "2.9.4", 34 | "datatables.net-bs4": "1.10.24", 35 | "easymde": "^2.20.0", 36 | "fuse.js": "^7.1.0", 37 | "hammerjs": "^2.0.8", 38 | "jquery": "3.6.0", 39 | "jquery.easing": "^1.4.1", 40 | "sortablejs": "^1.15.6" 41 | }, 42 | "devDependencies": { 43 | "@fortawesome/fontawesome-free": "^6.7.2", 44 | "browser-sync": "2.26.14", 45 | "del": "6.0.0", 46 | "gulp": "^4.0.2", 47 | "gulp-autoprefixer": "7.0.1", 48 | "gulp-clean-css": "4.3.0", 49 | "gulp-concat": "^2.6.1", 50 | "gulp-google-webfonts": "^4.1.0", 51 | "gulp-header": "2.0.9", 52 | "gulp-plumber": "^1.2.1", 53 | "gulp-rename": "2.0.0", 54 | "gulp-sass": "^6.0.1", 55 | "gulp-uglify": "3.0.2", 56 | "merge-stream": "2.0.0", 57 | "node-sass": "^8.0.0", 58 | "sass": "^1.86.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /html/scss/_buttons.scss: -------------------------------------------------------------------------------- 1 | .btn-circle { 2 | border-radius: 100%; 3 | height: 2.5rem; 4 | width: 2.5rem; 5 | font-size: 1rem; 6 | display: inline-flex; 7 | align-items: center; 8 | justify-content: center; 9 | &.btn-sm { 10 | height: 1.8rem; 11 | width: 1.8rem; 12 | font-size: 0.75rem; 13 | } 14 | &.btn-lg { 15 | height: 3.5rem; 16 | width: 3.5rem; 17 | font-size: 1.35rem; 18 | } 19 | } 20 | 21 | .btn-icon-split { 22 | padding: 0; 23 | overflow: hidden; 24 | display: inline-flex; 25 | align-items: stretch; 26 | justify-content: center; 27 | .icon { 28 | background: fade-out($black, .85); 29 | display: inline-block; 30 | padding: $btn-padding-y $btn-padding-x; 31 | } 32 | .text { 33 | display: inline-block; 34 | padding: $btn-padding-y $btn-padding-x; 35 | } 36 | &.btn-sm { 37 | .icon { 38 | padding: $btn-padding-y-sm $btn-padding-x-sm; 39 | } 40 | .text { 41 | padding: $btn-padding-y-sm $btn-padding-x-sm; 42 | } 43 | } 44 | &.btn-lg { 45 | .icon { 46 | padding: $btn-padding-y-lg $btn-padding-x-lg; 47 | } 48 | .text { 49 | padding: $btn-padding-y-lg $btn-padding-x-lg; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /html/scss/_cards.scss: -------------------------------------------------------------------------------- 1 | // scss/_cards.scss 2 | // Custom Card Styling 3 | 4 | .card { 5 | // Existing styles... 6 | 7 | .card-header { 8 | // Existing styles... 9 | 10 | // Format Dropdowns in Card Headings 11 | .dropdown { 12 | line-height: 1; 13 | .dropdown-menu { 14 | line-height: 1.5; 15 | } 16 | } 17 | } 18 | // Collapsable Card Styling 19 | .card-header[data-toggle="collapse"] { 20 | text-decoration: none; 21 | position: relative; 22 | padding: 0.75rem 3.25rem 0.75rem 1.25rem; 23 | &::after { 24 | position: absolute; 25 | right: 0; 26 | top: 0; 27 | padding-right: 1.725rem; 28 | line-height: 51px; 29 | font-weight: 900; 30 | content: '\f107'; 31 | font-family: 'Font Awesome 5 Free'; 32 | color: var(--bs-secondary-color); // Use variable 33 | } 34 | &.collapsed { 35 | border-radius: $card-border-radius; 36 | &::after { 37 | content: '\f105'; 38 | } 39 | } 40 | } 41 | } 42 | 43 | // --- Card and Editor Scroll --- 44 | // Existing styles... 45 | .card.h-100 { 46 | display: flex; 47 | flex-direction: column; 48 | flex-grow: 1; 49 | overflow: hidden; 50 | } 51 | 52 | .card-body { 53 | flex-grow: 1; 54 | overflow: hidden; 55 | display: flex; 56 | flex-direction: column; 57 | padding: 1rem; 58 | 59 | 60 | .card-item-row { 61 | margin-left: 0; 62 | margin-right: 0; 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /html/scss/_charts.scss: -------------------------------------------------------------------------------- 1 | // Area Chart 2 | .chart-area { 3 | position: relative; 4 | height: 10rem; 5 | width: 100%; 6 | @include media-breakpoint-up(md) { 7 | height: 20rem; 8 | } 9 | } 10 | 11 | // Bar Chart 12 | .chart-bar { 13 | position: relative; 14 | height: 10rem; 15 | width: 100%; 16 | @include media-breakpoint-up(md) { 17 | height: 20rem; 18 | } 19 | } 20 | 21 | // Pie Chart 22 | .chart-pie { 23 | position: relative; 24 | height: 15rem; 25 | width: 100%; 26 | @include media-breakpoint-up(md) { 27 | height: calc(20rem - 43px) !important; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /html/scss/_dropdowns.scss: -------------------------------------------------------------------------------- 1 | // Custom Dropdown Styling 2 | 3 | .dropdown { 4 | .dropdown-menu { 5 | font-size: $dropdown-font-size; 6 | .dropdown-header { 7 | @extend .text-uppercase; 8 | font-weight: 800; 9 | font-size: 0.65rem; 10 | color: $gray-500; 11 | } 12 | } 13 | } 14 | 15 | // Utility class to hide arrow from dropdown 16 | 17 | .dropdown.no-arrow { 18 | .dropdown-toggle::after { 19 | display: none; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /html/scss/_editor.scss: -------------------------------------------------------------------------------- 1 | // scss/_editor.scss 2 | // Theme-aware styling for EasyMDE / CodeMirror 3 | 4 | // --- Base Editor Sizing (Keep existing flex/height styles) --- 5 | .card-body { // Target specifically within card-body 6 | .EasyMDEContainer, 7 | .CodeMirror { 8 | flex-grow: 1; 9 | min-height: 200px; 10 | border: none; // Keep border none unless desired 11 | display: flex; 12 | flex-direction: column; 13 | } 14 | 15 | .CodeMirror { 16 | .CodeMirror-scroll { 17 | min-height: 150px; 18 | flex-grow: 1; 19 | position: relative; 20 | } 21 | .CodeMirror-gutters { height: auto !important; } 22 | } 23 | } 24 | // --- End Base Sizing --- 25 | 26 | 27 | // --- Theme-Aware Editor Styles --- 28 | 29 | // Base styles (Applied by default, uses vars for Light Mode / Themed Light Mode) 30 | .editor-toolbar { 31 | // Use theme variables for background and border 32 | background-color: var(--bs-tertiary-bg); // Slightly off-main background 33 | border: 1px solid var(--bs-border-color); 34 | border-bottom: none; // Often looks better without bottom border if attached to editor 35 | 36 | button, a.fa { // Toolbar buttons 37 | color: var(--bs-secondary-color); // Use secondary text color 38 | border: 1px solid transparent !important; // Make border transparent initially 39 | background-color: transparent; 40 | 41 | &:hover, &.active { 42 | // Use theme variables for hover/active state 43 | background-color: var(--bs-secondary-bg); 44 | border-color: var(--bs-border-color-translucent) !important; // Subtle border on hover 45 | color: var(--bs-emphasis-color); // Emphasized text color 46 | } 47 | } 48 | 49 | i.separator { 50 | border-left: 1px solid var(--bs-border-color); // Use theme border color 51 | border-right: 1px solid var(--bs-border-color); 52 | color: transparent; // Make separator icon invisible 53 | } 54 | } 55 | 56 | .CodeMirror { 57 | // Use theme variables for background, text, and border 58 | background-color: var(--bs-body-bg); // Editor bg matches body bg 59 | color: var(--bs-body-color); // Editor text matches body text 60 | border: 1px solid var(--bs-border-color); 61 | border-radius: 0 0 var(--bs-border-radius) var(--bs-border-radius); // Match card rounding if needed 62 | } 63 | 64 | .CodeMirror-gutters { 65 | // Use a slightly different background, theme variable 66 | background-color: var(--bs-secondary-bg); 67 | border-right: 1px solid var(--bs-border-color); 68 | color: var(--bs-secondary-color); // Line numbers color 69 | } 70 | 71 | .CodeMirror-cursor { 72 | // Use theme variable for border color (often opposite of bg) 73 | border-left: 1px solid var(--bs-emphasis-color); 74 | } 75 | 76 | .CodeMirror-selected { 77 | // Use a theme variable, often primary with transparency 78 | background-color: rgba(var(--bs-primary-rgb), 0.3); 79 | } 80 | 81 | .CodeMirror-activeline-background { 82 | // Use a subtle background, maybe tertiary or transparent light/dark 83 | background-color: var(--bs-tertiary-bg); 84 | // Or use transparency: background-color: rgba(var(--bs-emphasis-color-rgb), 0.07); 85 | } 86 | 87 | .editor-statusbar { 88 | // Use theme variables, often matches toolbar 89 | background-color: var(--bs-tertiary-bg); 90 | color: var(--bs-secondary-color); 91 | border: 1px solid var(--bs-border-color); 92 | border-top: none; // Often looks better without top border 93 | font-size: 0.75rem; 94 | padding: 0.25rem 0.5rem; 95 | border-radius: 0 0 var(--bs-border-radius) var(--bs-border-radius); // Match card rounding 96 | } 97 | 98 | // Editor Preview Area (uses theme variables for HTML elements) 99 | .editor-preview, .editor-preview-side { 100 | background-color: var(--bs-body-bg); 101 | color: var(--bs-body-color); 102 | border: 1px solid var(--bs-border-color); 103 | padding: 1rem; // Add some padding 104 | 105 | h1, h2, h3, h4, h5, h6 { color: var(--bs-heading-color); border-bottom-color: var(--bs-border-color); } 106 | a { color: var(--bs-link-color); &:hover { color: var(--bs-link-hover-color); } } 107 | code { color: var(--bs-code-color); background-color: var(--bs-secondary-bg); padding: .2em .4em; border-radius: .25rem;} 108 | pre { background-color: var(--bs-secondary-bg); border: 1px solid var(--bs-border-color); padding: 1rem; border-radius: .375rem; code { background-color: transparent; padding: 0; border: none; } } 109 | blockquote { color: var(--bs-secondary-color); border-left: .25rem solid var(--bs-border-color); padding-left: 1rem; margin-left: 0; } 110 | hr { border-top-color: var(--bs-border-color-translucent); } 111 | table { th, td { border-color: var(--bs-border-color); } } 112 | } 113 | 114 | // Fullscreen adjustments (ensure it uses theme vars too) 115 | .EasyMDEContainer.fullscreen { 116 | background-color: var(--bs-body-bg); // Fullscreen bg matches theme 117 | border-color: var(--bs-border-color); 118 | z-index: 1050; // Adjust z-index as needed (above navbars, below modals typically) 119 | } 120 | 121 | 122 | // --- Dark Mode Overrides for Editor --- 123 | // These styles apply ONLY when data-bs-theme="dark" is set on HTML 124 | // They assume the base styles above are using the correct variables. 125 | // We only need to override specific things if the standard dark variables don't look right. 126 | html[data-bs-theme="dark"] { 127 | 128 | .CodeMirror-cursor { 129 | // Ensure cursor is visible on dark background 130 | border-left-color: var(--bs-light); // Use light color directly maybe 131 | } 132 | 133 | .CodeMirror-selected { 134 | // Adjust selection if default primary is too dark/light 135 | // background-color: rgba(var(--bs-info-rgb), 0.4); // Example: use info color 136 | } 137 | 138 | // Preview area adjustments if needed for dark mode 139 | .editor-preview, .editor-preview-side { 140 | // Example: Maybe make code blocks slightly different 141 | // pre, code { background-color: var(--bs-dark-subtle); } // Requires Bootstrap 5.3+ dark subtle vars 142 | } 143 | 144 | // You might not need many overrides here if the base styles correctly 145 | // use variables like --bs-body-bg, --bs-tertiary-bg, --bs-border-color etc. 146 | // as these are already defined correctly for dark mode by Bootstrap/your themes. 147 | } 148 | // --- End Dark Mode Overrides --- 149 | 150 | 151 | // --- Other existing styles --- 152 | .search-highlight { 153 | background-color: yellow; // Keep simple or use a specific variable 154 | color: black; 155 | } 156 | 157 | .fixed-editor-toolbar { 158 | position: fixed; 159 | top: 4.5rem; /* Adjust based on your topbar height */ 160 | width: auto; 161 | // Use theme variables 162 | background: var(--bs-tertiary-bg); 163 | border: 1px solid var(--bs-border-color); 164 | z-index: 1000; 165 | padding: 0.25rem 0.5rem; 166 | border-radius: 0.25rem; 167 | box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); // Shadow might need theme adjustment 168 | } 169 | -------------------------------------------------------------------------------- /html/scss/_error.scss: -------------------------------------------------------------------------------- 1 | // scss/_editor.scss 2 | 3 | // --- Base Sizing (Keep your existing styles) --- 4 | .card-body { // Target specifically within card-body if needed 5 | .EasyMDEContainer, 6 | .CodeMirror { 7 | flex-grow: 1; 8 | min-height: 200px; 9 | border: none; // Keep or adjust as needed 10 | display: flex; 11 | flex-direction: column; 12 | } 13 | 14 | .CodeMirror { 15 | .CodeMirror-scroll { 16 | min-height: 150px; 17 | flex-grow: 1; 18 | position: relative; 19 | } 20 | .CodeMirror-gutters { 21 | height: auto !important; 22 | } 23 | .CodeMirror-sizer { 24 | // Sizer usually handles width/height based on content 25 | } 26 | } 27 | } 28 | // --- End Base Sizing --- 29 | 30 | 31 | // *** ADD DARK THEME STYLES FOR EASYMDE / CODEMIRROR *** 32 | [data-bs-theme="dark"] { 33 | 34 | // Style EasyMDE Toolbar 35 | .editor-toolbar { 36 | background-color: var(--bs-tertiary-bg); // Darker background for toolbar 37 | border-color: var(--bs-border-color); 38 | 39 | // Toolbar buttons 40 | button, a.fa { // Target buttons and icon links 41 | color: var(--bs-secondary-color); // Lighter icon color 42 | border-color: var(--bs-border-color) !important; // Ensure borders match theme 43 | 44 | &:hover, &.active { 45 | background-color: var(--bs-secondary-bg); // Darker hover/active background 46 | border-color: var(--bs-border-color-translucent) !important; 47 | color: var(--bs-light); // Brighter icon on hover/active 48 | } 49 | } 50 | 51 | // Separators 52 | i.separator { 53 | border-color: var(--bs-border-color); 54 | border-right: none; // Keep existing separator style logic 55 | } 56 | } 57 | 58 | // Style CodeMirror editor area 59 | .CodeMirror { 60 | background-color: var(--bs-dark); // Dark background for editor 61 | color: var(--bs-body-color); // Default text color for editor 62 | border-color: var(--bs-border-color); // Border if applicable 63 | } 64 | 65 | // CodeMirror Gutters (line numbers, etc.) 66 | .CodeMirror-gutters { 67 | background-color: var(--bs-dark-subtle, var(--bs-dark)); // Slightly different dark bg or same as editor 68 | border-right: 1px solid var(--bs-border-color); 69 | color: var(--bs-secondary-color); // Color for line numbers 70 | } 71 | 72 | // CodeMirror Cursor 73 | .CodeMirror-cursor { 74 | border-left: 1px solid var(--bs-light); // Light cursor for dark background 75 | } 76 | 77 | // CodeMirror Selection 78 | .CodeMirror-selected { 79 | // Use a background color that contrasts with both the editor bg and text 80 | background-color: rgba(var(--bs-primary-rgb), 0.3); // Example: translucent primary 81 | } 82 | 83 | // CodeMirror Active Line (Optional styling) 84 | .CodeMirror-activeline-background { 85 | background-color: rgba(var(--bs-light-rgb), 0.07); // Subtle highlight for the active line 86 | } 87 | 88 | // Style EasyMDE Status Bar 89 | .editor-statusbar { 90 | background-color: var(--bs-tertiary-bg); // Match toolbar or use another dark shade 91 | color: var(--bs-secondary-color); // Text color for status bar items 92 | border-color: var(--bs-border-color); 93 | } 94 | 95 | // Style EasyMDE Preview Area (if needed) 96 | .editor-preview, .editor-preview-side { 97 | background-color: var(--bs-body-bg); // Use body background for preview 98 | color: var(--bs-body-color); // Use body text color for preview 99 | border-color: var(--bs-border-color); 100 | 101 | // Ensure headings, links etc. in preview use dark theme variables 102 | h1, h2, h3, h4, h5, h6 { 103 | color: var(--bs-heading-color); 104 | border-bottom-color: var(--bs-border-color); // Example for heading underlines 105 | } 106 | a { 107 | color: var(--bs-link-color); 108 | &:hover { 109 | color: var(--bs-link-hover-color); 110 | } 111 | } 112 | code { 113 | color: var(--bs-code-color); 114 | background-color: var(--bs-dark-bg-subtle); // Background for inline code 115 | } 116 | pre { 117 | background-color: var(--bs-dark-bg-subtle); // Background for code blocks 118 | border-color: var(--bs-border-color); 119 | code { 120 | background-color: transparent; // Reset inline code bg within pre 121 | } 122 | } 123 | blockquote { 124 | color: var(--bs-secondary-color); 125 | border-left-color: var(--bs-border-color); 126 | } 127 | hr { 128 | border-top-color: var(--bs-border-color-translucent); 129 | } 130 | // Add more styles as needed for tables, lists, etc. 131 | } 132 | 133 | // Adjust Fullscreen Appearance if necessary 134 | .EasyMDEContainer.fullscreen { 135 | background-color: var(--bs-dark); // Ensure fullscreen background is dark 136 | border-color: var(--bs-border-color); 137 | z-index: var(--bs-modal-zindex, 1055); // Ensure it's above other content like Bootstrap modals 138 | } 139 | 140 | } 141 | // *** END DARK THEME STYLES *** 142 | -------------------------------------------------------------------------------- /html/scss/_footer.scss: -------------------------------------------------------------------------------- 1 | footer.sticky-footer { 2 | padding: 2rem 0; 3 | flex-shrink: 0; 4 | .copyright { 5 | line-height: 1; 6 | font-size: 0.8rem; 7 | } 8 | } 9 | 10 | body.sidebar-toggled { 11 | footer.sticky-footer { 12 | width: 100%; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /html/scss/_global.scss: -------------------------------------------------------------------------------- 1 | // scss/_global.scss 2 | // Global component styles 3 | 4 | html { 5 | position: relative; 6 | min-height: 100%; 7 | height: 100%; 8 | overflow: hidden; 9 | } 10 | 11 | body { 12 | height: 100%; 13 | overflow: hidden; 14 | // Body background/color should be controlled by Bootstrap vars via data-bs-theme 15 | } 16 | 17 | a { 18 | &:focus { 19 | outline: none; 20 | } 21 | // Link colors should use Bootstrap vars like var(--bs-link-color) etc. 22 | } 23 | 24 | // Main page wrapper 25 | #wrapper { 26 | display: flex; 27 | height: 100%; 28 | } 29 | 30 | #content-wrapper { 31 | // Use body background variable, will change with theme 32 | background-color: var(--bs-secondary-bg); // Or var(--bs-body-bg) if it should match body 33 | width: 100%; 34 | display: flex; 35 | flex-direction: column; 36 | overflow: hidden; 37 | height: 100%; 38 | } 39 | 40 | #content { 41 | flex: 1 1 auto; 42 | overflow: hidden; 43 | display: flex; 44 | flex-direction: column; 45 | position: relative; 46 | 47 | @include media-breakpoint-down(sm) { 48 | .container-fluid { 49 | padding-left: 0; 50 | padding-right: 0; 51 | .card-body { 52 | padding-left: 0; 53 | padding-right: 0; 54 | } 55 | } 56 | } 57 | } 58 | 59 | // Set container padding to match gutter width instead of default 15px 60 | .container, 61 | .container-fluid { 62 | padding-left: $grid-gutter-width; 63 | padding-right: $grid-gutter-width; 64 | } 65 | 66 | #content > .container-fluid { 67 | flex-grow: 1; 68 | overflow-y: auto; 69 | overflow-x: hidden; 70 | display: flex; 71 | flex-direction: column; 72 | padding-top: 0; 73 | padding-bottom: 1.5rem; 74 | // Background should implicitly be that of #content-wrapper or body 75 | } 76 | 77 | #content > .container-fluid > .row { 78 | flex-grow: 1; 79 | display: flex; 80 | } 81 | 82 | #content > .container-fluid > .row > .col { 83 | // Column styles 84 | } 85 | 86 | 87 | // Scroll to top button 88 | .scroll-to-top { 89 | position: fixed; 90 | right: 1rem; 91 | bottom: 1rem; 92 | display: none; 93 | width: 2.75rem; 94 | height: 2.75rem; 95 | text-align: center; 96 | // Use variables for colors 97 | color: var(--bs-white); // Fixed white text maybe? Or var(--bs-light) 98 | // Use rgba with CSS var for background 99 | background: rgba(var(--bs-dark-rgb), .5); // Translucent dark background 100 | line-height: 46px; 101 | z-index: 1031; 102 | border-radius: $border-radius; // Add rounding if desired 103 | 104 | &:focus, 105 | &:hover { 106 | color: var(--bs-white); // Fixed white text maybe? 107 | } 108 | &:hover { 109 | background: var(--bs-dark); // Solid dark on hover 110 | } 111 | i { 112 | font-weight: 800; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /html/scss/_login.scss: -------------------------------------------------------------------------------- 1 | // Pulling these images from Unsplash 2 | // Toshi the dog from https://unsplash.com/@charlesdeluvio - what a funny dog... 3 | 4 | .bg-login-image { 5 | background: url($login-image); 6 | background-position: center; 7 | background-size: cover; 8 | } 9 | 10 | .bg-register-image { 11 | background: url($register-image); 12 | background-position: center; 13 | background-size: cover; 14 | } 15 | 16 | .bg-password-image { 17 | background: url($password-image); 18 | background-position: center; 19 | background-size: cover; 20 | } 21 | 22 | form.user { 23 | 24 | .custom-checkbox.small { 25 | label { 26 | line-height: 1.5rem; 27 | } 28 | } 29 | 30 | .form-control-user { 31 | font-size: 0.8rem; 32 | border-radius: 10rem; 33 | padding: 1.5rem 1rem; 34 | } 35 | 36 | .btn-user { 37 | font-size: 0.8rem; 38 | border-radius: 10rem; 39 | padding: 0.75rem 1rem; 40 | } 41 | 42 | } 43 | 44 | .btn-google { 45 | @include button-variant($brand-google, $white); 46 | } 47 | 48 | .btn-facebook { 49 | @include button-variant($brand-facebook, $white); 50 | } 51 | -------------------------------------------------------------------------------- /html/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /html/scss/_navs.scss: -------------------------------------------------------------------------------- 1 | @import "navs/global.scss"; 2 | @import "navs/topbar.scss"; 3 | @import "navs/sidebar.scss"; 4 | -------------------------------------------------------------------------------- /html/scss/_splash.scss: -------------------------------------------------------------------------------- 1 | // SCSS for Loading Overlay (e.g., in _global.scss or _splash.scss) 2 | 3 | #loading-overlay { 4 | position: fixed; // Stick to the viewport 5 | top: 0; 6 | left: 0; 7 | width: 100%; 8 | height: 100%; 9 | background-color: rgba(var(--bs-body-bg-rgb), 0.9); // Use body background with opacity 10 | display: flex; // Use flexbox for centering 11 | justify-content: center; 12 | align-items: center; 13 | z-index: 1056; // Ensure it's above other content (Bootstrap modals are ~1050-1055) 14 | opacity: 1; 15 | transition: opacity 0.5s ease-out; // Smooth fade-out transition 16 | 17 | // Style the spinner if needed (optional, Bootstrap defaults are usually fine) 18 | .spinner-border { 19 | width: 3rem; // Example: Make spinner slightly larger 20 | height: 3rem; // Example: Make spinner slightly larger 21 | } 22 | 23 | // Class to apply via jQuery to hide the overlay 24 | &.hidden { 25 | opacity: 0; 26 | pointer-events: none; // Prevent interaction after hiding 27 | } 28 | } 29 | 30 | // Ensure it's hidden initially by default IF JavaScript might be slow or disabled (optional but good practice) 31 | // You can achieve this by adding the .hidden class directly in the HTML initially, 32 | // OR by adding this rule if you prefer CSS-first hiding: 33 | // body:not(.loaded) #loading-overlay { 34 | // /* Styles above */ 35 | // } 36 | // body.loaded #loading-overlay { 37 | // opacity: 0; 38 | // pointer-events: none; 39 | // } 40 | -------------------------------------------------------------------------------- /html/scss/_themes.scss: -------------------------------------------------------------------------------- 1 | // scss/_themes.scss 2 | // Import all individual theme variable definitions 3 | 4 | @import "themes/theme-default"; 5 | @import "themes/theme-oceanic"; 6 | @import "themes/theme-forest"; 7 | @import "themes/theme-sunset"; 8 | -------------------------------------------------------------------------------- /html/scss/_toast.scss: -------------------------------------------------------------------------------- 1 | #toastContainer { 2 | /* Small screens (Default): Bottom Center */ 3 | bottom: 1rem; 4 | left: 50%; 5 | transform: translateX(-50%); 6 | z-index: 1100; /* Ensure it's above most elements */ 7 | width: auto; /* Adjust width as needed */ 8 | max-width: 90%; /* Prevent excessive width on small screens */ 9 | } 10 | 11 | /* Medium screens and up (md breakpoint - adjust if needed) */ 12 | @media (min-width: 768px) { 13 | #toastContainer { 14 | top: 5rem; /* Adjust top position as needed (consider navbar height) */ 15 | right: 1rem; 16 | bottom: auto; /* Override small screen setting */ 17 | left: auto; /* Override small screen setting */ 18 | transform: none; /* Override small screen setting */ 19 | max-width: 350px; /* Optional: Set a max width for toasts */ 20 | } 21 | } 22 | 23 | /* Ensure template is hidden initially */ 24 | #toastTemplate { 25 | display: none; 26 | } 27 | 28 | /* Add some spacing between toasts if multiple appear */ 29 | #toastContainer .toast + .toast { 30 | margin-top: 0.5rem; 31 | } 32 | -------------------------------------------------------------------------------- /html/scss/_utilities.scss: -------------------------------------------------------------------------------- 1 | @import "utilities/animation.scss"; 2 | @import "utilities/background.scss"; 3 | @import "utilities/display.scss"; 4 | @import "utilities/text.scss"; 5 | @import "utilities/border.scss"; 6 | @import "utilities/progress.scss"; 7 | @import "utilities/rotate.scss"; 8 | -------------------------------------------------------------------------------- /html/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | // Override Bootstrap default variables here 2 | // Do not edit any of the files in /vendor/bootstrap/scss/! 3 | 4 | // Color Variables 5 | // Bootstrap Color Overrides 6 | 7 | $white: #fff !default; 8 | $gray-100: #f8f9fc !default; 9 | $gray-200: #eaecf4 !default; 10 | $gray-300: #dddfeb !default; 11 | $gray-400: #d1d3e2 !default; 12 | $gray-500: #b7b9cc !default; 13 | $gray-600: #858796 !default; 14 | $gray-700: #6e707e !default; 15 | $gray-800: #5a5c69 !default; 16 | $gray-900: #3a3b45 !default; 17 | $black: #000 !default; 18 | 19 | $blue: #4e73df !default; 20 | $indigo: #6610f2 !default; 21 | $purple: #6f42c1 !default; 22 | $pink: #e83e8c !default; 23 | $red: #e74a3b !default; 24 | $orange: #fd7e14 !default; 25 | $yellow: #f6c23e !default; 26 | $green: #1cc88a !default; 27 | $teal: #20c9a6 !default; 28 | $cyan: #36b9cc !default; 29 | 30 | // Custom Colors 31 | $brand-google: #ea4335 !default; 32 | $brand-facebook: #3b5998 !default; 33 | 34 | // Set Contrast Threshold 35 | $yiq-contrasted-threshold: 195 !default; 36 | 37 | // Typography 38 | $body-color: $gray-600 !default; 39 | 40 | $font-family-sans-serif: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", 'Noto Color Emoji' !default; 41 | 42 | $font-weight-light: 300 !default; 43 | // $font-weight-base: 400; 44 | $headings-font-weight: 400 !default; 45 | 46 | // Shadows 47 | $box-shadow-sm: 0 0.125rem 0.25rem 0 rgba($gray-900, .2) !default; 48 | $box-shadow: 0 0.15rem 1.75rem 0 rgba($gray-900, .15) !default; 49 | // $box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default; 50 | 51 | // Borders Radius 52 | $border-radius: 0.35rem !default; 53 | $border-color: darken($gray-200, 2%) !default; 54 | 55 | // Spacing Variables 56 | // Change below variable if the height of the navbar changes 57 | $topbar-base-height: 3rem !default; 58 | // Change below variable to change the width of the sidenav 59 | $sidebar-base-width: 14rem !default; 60 | // Change below variable to change the width of the sidenav when collapsed 61 | $sidebar-collapsed-width: 6.5rem !default; 62 | 63 | // Card 64 | $card-cap-bg: $gray-100 !default; 65 | $card-border-color: $border-color !default; 66 | 67 | // Adjust column spacing for symmetry 68 | $spacer: 1rem !default; 69 | $grid-gutter-width: $spacer * 1.5 !default; 70 | 71 | // Transitions 72 | $transition-collapse: height .15s ease !default; 73 | 74 | // Dropdowns 75 | $dropdown-font-size: 0.85rem !default; 76 | $dropdown-border-color: $border-color !default; 77 | 78 | // Images 79 | $login-image: 'https://source.unsplash.com/K4mSJ7kc0As/600x800' !default; 80 | $register-image: 'https://source.unsplash.com/Mv9hjnEUHR4/600x800' !default; 81 | $password-image: 'https://source.unsplash.com/oWTW-jNGl9I/600x800' !default; 82 | -------------------------------------------------------------------------------- /html/scss/navs/_global.scss: -------------------------------------------------------------------------------- 1 | // Global styles for both custom sidebar and topbar compoments 2 | 3 | .sidebar, 4 | .topbar { 5 | .nav-item { 6 | // Customize Dropdown Arrows for Navbar 7 | &.dropdown { 8 | .dropdown-toggle { 9 | &::after { 10 | width: 1rem; 11 | text-align: center; 12 | float: right; 13 | vertical-align: 0; 14 | border: 0; 15 | font-weight: 900; 16 | content: '\f105'; 17 | font-family: 'Font Awesome 5 Free'; 18 | } 19 | } 20 | &.show { 21 | .dropdown-toggle::after { 22 | content: '\f107'; 23 | } 24 | } 25 | } 26 | // Counter for nav links and nav link image sizing 27 | .nav-link { 28 | position: relative; 29 | .badge-counter { 30 | position: absolute; 31 | transform: scale(0.7); 32 | transform-origin: top right; 33 | right: .25rem; 34 | margin-top: -.25rem; 35 | } 36 | .img-profile { 37 | height: 2rem; 38 | width: 2rem; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /html/scss/navs/_topbar.scss: -------------------------------------------------------------------------------- 1 | // scss/navs/_topbar.scss 2 | 3 | // Topbar 4 | .topbar { 5 | height: $topbar-base-height; 6 | // Assume default topbar is light, uses standard navbar classes potentially 7 | // Or set a base background using CSS vars if it's custom 8 | // Example: background-color: var(--bs-light); 9 | // Example: border-bottom: 1px solid var(--bs-border-color); 10 | 11 | #sidebarToggleTop { 12 | height: 2.5rem; 13 | width: 2.5rem; 14 | color: var(--bs-secondary-color); // Use variable for icon color 15 | 16 | &:hover { 17 | background-color: var(--bs-tertiary-bg); // Use variable for hover bg 18 | color: var(--bs-emphasis-color); 19 | } 20 | &:active { 21 | background-color: var(--bs-secondary-bg); // Use variable for active bg 22 | color: var(--bs-emphasis-color); 23 | } 24 | } 25 | 26 | .navbar-search { 27 | width: 25rem; 28 | input { 29 | font-size: 0.85rem; 30 | height: auto; 31 | // Input should inherit theme styles from Bootstrap's form control styles 32 | } 33 | } 34 | 35 | .topbar-divider { 36 | width: 0; 37 | border-right: 1px solid var(--bs-border-color); // Use border variable 38 | height: calc(#{$topbar-base-height} - 2rem); 39 | margin: auto 1rem; 40 | } 41 | 42 | // Style nav links within the topbar context 43 | .navbar-nav { // Target specifically within .topbar if needed 44 | .nav-item { 45 | .nav-link { 46 | height: $topbar-base-height; 47 | display: flex; 48 | align-items: center; 49 | padding: 0 0.75rem; 50 | color: var(--bs-secondary-color); // Default link color (adjust if topbar is dark) 51 | 52 | &:hover { 53 | color: var(--bs-emphasis-color); // Hover color 54 | } 55 | &:active { 56 | color: var(--bs-emphasis-color); // Active color 57 | } 58 | &:focus { 59 | outline: none; 60 | } 61 | } 62 | &:focus { 63 | outline: none; 64 | } 65 | } 66 | } 67 | 68 | 69 | .dropdown { 70 | position: static; // Or relative as needed 71 | .dropdown-menu { 72 | width: calc(100% - #{$grid-gutter-width}); 73 | right: $grid-gutter-width / 2; 74 | // Dropdown menu itself should inherit Bootstrap's theme styles 75 | } 76 | } 77 | 78 | // Style the custom dropdown list within topbar 79 | .dropdown-list { 80 | padding: 0; 81 | border: none; // Use Bootstrap's dropdown border var if needed: var(--bs-border-color) 82 | overflow: hidden; 83 | 84 | .dropdown-header { 85 | // Use theme colors vars for header 86 | background-color: var(--bs-primary); // Example: Use primary color 87 | border: 1px solid var(--bs-primary); 88 | padding-top: 0.75rem; 89 | padding-bottom: 0.75rem; 90 | color: var(--bs-light); // Use light text on primary bg 91 | } 92 | 93 | .dropdown-item { 94 | white-space: normal; 95 | padding-top: 0.5rem; 96 | padding-bottom: 0.5rem; 97 | // Use border variable 98 | border-left: 1px solid var(--bs-border-color); 99 | border-right: 1px solid var(--bs-border-color); 100 | border-bottom: 1px solid var(--bs-border-color); 101 | line-height: 1.3rem; 102 | color: var(--bs-body-color); // Use body color var 103 | 104 | .dropdown-list-image { 105 | position: relative; 106 | height: 2.5rem; 107 | width: 2.5rem; 108 | img { 109 | height: 2.5rem; 110 | width: 2.5rem; 111 | } 112 | .status-indicator { 113 | // Use background variables 114 | background-color: var(--bs-secondary-bg); // Or map $gray-200 equivalent 115 | height: 0.75rem; 116 | width: 0.75rem; 117 | border-radius: 100%; 118 | position: absolute; 119 | bottom: 0; 120 | right: 0; 121 | // Use body background for border against image 122 | border: .125rem solid var(--bs-body-bg); 123 | } 124 | } 125 | .text-truncate { 126 | max-width: 10rem; 127 | } 128 | &:active { 129 | // Use background/color variables for active state 130 | background-color: var(--bs-secondary-bg); 131 | color: var(--bs-emphasis-color); 132 | } 133 | // Hover state is handled by Bootstrap's base dropdown-item styles usually 134 | } 135 | } 136 | @include media-breakpoint-up(sm) { 137 | .dropdown { 138 | position: relative; 139 | .dropdown-menu { 140 | width: auto; 141 | right: 0; 142 | } 143 | } 144 | .dropdown-list { 145 | width: 20rem !important; 146 | .dropdown-item { 147 | .text-truncate { 148 | max-width: 13.375rem; 149 | } 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /html/scss/styles.scss: -------------------------------------------------------------------------------- 1 | // Import Custom SB Admin 2 Variables (Overrides Default Bootstrap Variables) 2 | @import "variables.scss"; 3 | 4 | // Import Bootstrap 5 | @import "../vendor/bootstrap/scss/bootstrap.scss"; 6 | 7 | @import "themes.scss"; 8 | 9 | // Import the main FontAwesome SCSS file 10 | $fa-font-path: "fonts"; 11 | @import "../vendor/fontawesome-free/scss/fontawesome.scss"; // Or all.scss, etc. 12 | @import "../vendor/fontawesome-free/scss/solid.scss"; // Import specific styles if needed 13 | 14 | @import "../vendor/easymde/easymde.min.css"; 15 | 16 | // Import Custom SB Admin 2 Mixins and Components 17 | @import "mixins.scss"; 18 | @import "global.scss"; // Ensure font-family definitions here use "Nunito" 19 | @import "utilities.scss"; 20 | 21 | // Custom Components 22 | @import "dropdowns.scss"; 23 | @import "navs.scss"; 24 | @import "buttons.scss"; 25 | @import "cards.scss"; 26 | @import "charts.scss"; 27 | @import "login.scss"; 28 | @import "error.scss"; 29 | @import "footer.scss"; 30 | @import "editor.scss"; 31 | @import "toast.scss"; 32 | @import "splash.scss"; 33 | 34 | // Import the new style partial LAST 35 | @import "styles/neomorphism.scss"; // <--- ADD THIS LINE 36 | -------------------------------------------------------------------------------- /html/scss/themes/_theme-default.scss: -------------------------------------------------------------------------------- 1 | // scss/themes/_theme-default.scss 2 | // Defines variables for the default theme 3 | 4 | // No specific overrides needed here if it just uses standard Bootstrap 5 | // variables defined via _variables.scss and the core Bootstrap SCSS. 6 | // You could explicitly define variables here if you want the default 7 | // theme to have unique colors different from raw Bootstrap. 8 | 9 | // Example (Optional - only if default needs specific overrides): 10 | // body[data-bs-theme="light"][data-app-theme="default"] { 11 | // --bs-primary: #{$blue}; // Your original blue from _variables.scss 12 | // --bs-body-bg: #{$white}; 13 | // --bs-body-color: #{$gray-600}; 14 | // // ... other light mode default overrides 15 | // } 16 | 17 | // body[data-bs-theme="dark"][data-app-theme="default"] { 18 | // --bs-primary: #{$blue}; // Keep same primary or adjust for dark 19 | // --bs-body-bg: #{$gray-900}; // Your original dark bg 20 | // --bs-body-color: #{$gray-300}; // Adjust text color for dark 21 | // --bs-secondary-bg: #{darken($gray-900, 5%)}; 22 | // --bs-tertiary-bg: #{darken($gray-900, 10%)}; 23 | // // ... other dark mode default overrides 24 | // } 25 | 26 | // Define SCSS variables for your default theme's dark mode 27 | $default-dark-primary: $blue; // Or your specific dark primary 28 | $default-dark-body-bg: $gray-900; // Or your specific dark background 29 | 30 | html[data-app-theme="default"] { 31 | &[data-bs-theme="dark"] { 32 | // Set the main Bootstrap CSS vars for this theme scope if needed 33 | --bs-primary: #{$default-dark-primary}; 34 | --bs-body-bg: #{$default-dark-body-bg}; 35 | // ... other overrides 36 | 37 | .card { 38 | // Use SCSS variables or static colors for the override 39 | --bs-card-cap-bg: #{mix($default-dark-primary, $default-dark-body-bg, 15%)}; // Correct SCSS 40 | // Or use a direct variable: --bs-card-cap-bg: var(--bs-tertiary-bg); // Use Bootstrap's dark tertiary bg 41 | // Or a static color: --bs-card-cap-bg: #2a2a35; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /html/scss/themes/_theme-forest.scss: -------------------------------------------------------------------------------- 1 | // scss/themes/_theme-forest.scss 2 | // Defines variables for the Forest theme 3 | 4 | $forest-green: #2F9E44; 5 | $forest-brown: #8D6E63; 6 | $forest-light-bg: #F1F3F5; 7 | $forest-dark-green: #2B7A39; 8 | $forest-dark-bg: #343A40; // Dark grey/brown 9 | $forest-dark-text: #E9ECEF; 10 | 11 | html[data-app-theme="forest"] { 12 | // --- Light Mode --- 13 | &[data-bs-theme="light"] { 14 | --bs-primary: #{$forest-green}; 15 | --bs-primary-rgb: #{to-rgb($forest-green)}; 16 | --bs-secondary: #{$forest-brown}; 17 | --bs-secondary-rgb: #{to-rgb($forest-brown)}; 18 | --bs-body-bg: #{$forest-light-bg}; 19 | --bs-body-color: #{$gray-700}; 20 | --bs-secondary-bg: #{mix($forest-green, $forest-light-bg, 5%)}; 21 | --bs-tertiary-bg: #{mix($forest-green, $forest-light-bg, 10%)}; 22 | --bs-border-color: #{mix($forest-green, $forest-light-bg, 25%)}; 23 | --bs-heading-color: #{$forest-dark-green}; 24 | 25 | .card { 26 | // --- Add Card Header Variables --- 27 | --bs-card-cap-bg: #{mix($forest-green, $forest-light-bg, 15%)}; // Example: A light mix 28 | --bs-card-cap-color: #{darken($forest-dark-green, 10%)}; // Example: Darker heading text 29 | // --- End Add Card Header Variables --- 30 | } 31 | 32 | --bs-link-color: #{$forest-green}; 33 | --bs-link-hover-color: #{darken($forest-green, 10%)}; 34 | 35 | .bg-gradient-primary { 36 | background-color: $forest-green; 37 | background-image: linear-gradient(180deg, lighten($forest-green, 10%) 10%, $forest-green 100%); 38 | } 39 | .sidebar { background-color: $forest-green; /* Adjust text colors as needed */ } 40 | } 41 | 42 | // --- Dark Mode --- 43 | &[data-bs-theme="dark"] { 44 | --bs-primary: #{lighten($forest-green, 10%)}; // Lighter green for dark primary 45 | --bs-primary-rgb: #{to-rgb(lighten($forest-green, 10%))}; 46 | --bs-secondary: #{lighten($forest-brown, 15%)}; // Lighter brown 47 | --bs-secondary-rgb: #{to-rgb(lighten($forest-brown, 15%))}; 48 | --bs-body-bg: #{$forest-dark-bg}; 49 | --bs-body-color: #{$forest-dark-text}; 50 | --bs-secondary-bg: #{lighten($forest-dark-bg, 5%)}; 51 | --bs-tertiary-bg: #{lighten($forest-dark-bg, 10%)}; 52 | --bs-border-color: #{lighten($forest-dark-bg, 15%)}; 53 | --bs-heading-color: #{lighten($forest-dark-text, 15%)}; 54 | 55 | .card { 56 | // --- Add Card Header Variables --- 57 | --bs-card-cap-bg: #{mix(lighten($forest-green, 10%), $forest-dark-bg, 15%)}; // Example: A light mix 58 | --bs-card-cap-color: #{$forest-dark-green}; // Example: Darker heading text 59 | // --- End Add Card Header Variables --- 60 | } 61 | 62 | --bs-link-color: #{lighten($forest-green, 15%)}; 63 | --bs-link-hover-color: #{lighten($forest-green, 25%)}; 64 | 65 | .bg-gradient-primary { 66 | background-color: $forest-dark-green; 67 | background-image: linear-gradient(180deg, lighten($forest-dark-green, 10%) 10%, $forest-dark-green 100%); 68 | } 69 | .sidebar { background-color: $forest-dark-green; /* Adjust text colors as needed */ } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /html/scss/themes/_theme-oceanic.scss: -------------------------------------------------------------------------------- 1 | // scss/themes/_theme-oceanic.scss 2 | // Defines variables for the Oceanic theme 3 | 4 | $ocean-blue: #0B7285; 5 | $ocean-teal: #0CA678; 6 | $ocean-sand: #F8F9FA; // Light sand 7 | $ocean-deep-blue: #104E5B; 8 | $ocean-dark-bg: #073B4C; 9 | $ocean-dark-text: #CED4DA; 10 | 11 | html[data-app-theme="oceanic"] { 12 | // --- Light Mode --- 13 | &[data-bs-theme="light"] { 14 | --bs-primary: #{$ocean-blue}; 15 | --bs-primary-rgb: #{to-rgb($ocean-blue)}; 16 | --bs-secondary: #{$ocean-teal}; 17 | --bs-secondary-rgb: #{to-rgb($ocean-teal)}; 18 | --bs-body-bg: #{$ocean-sand}; 19 | --bs-body-color: #{$gray-800}; // Darker text on light sand 20 | --bs-secondary-bg: #{mix($ocean-blue, $ocean-sand, 5%)}; 21 | --bs-tertiary-bg: #{mix($ocean-blue, $ocean-sand, 10%)}; 22 | --bs-border-color: #{mix($ocean-blue, $ocean-sand, 25%)}; 23 | --bs-heading-color: #{$ocean-deep-blue}; 24 | 25 | .card { 26 | // --- Add Card Header Variables --- 27 | --bs-card-cap-bg: #{mix($ocean-blue, $ocean-sand, 15%)}; // Example: A light mix 28 | --bs-card-cap-color: #{darken($ocean-deep-blue, 10%)}; // Example: Darker heading text 29 | // --- End Add Card Header Variables --- 30 | } 31 | 32 | --bs-link-color: #{$ocean-blue}; 33 | --bs-link-hover-color: #{darken($ocean-blue, 10%)}; 34 | .bg-gradient-primary { // Example: Update sidebar gradient 35 | background-color: $ocean-blue; 36 | background-image: linear-gradient(180deg, lighten($ocean-blue, 10%) 10%, $ocean-blue 100%); 37 | } 38 | .sidebar { 39 | background-color: $ocean-blue; // Base sidebar bg 40 | .nav-item .nav-link { 41 | color: rgba(255, 255, 255, 0.8); 42 | i { color: rgba(255, 255, 255, 0.4); } 43 | &:hover, &.active { color: #fff; i { color: #fff; } } 44 | } 45 | hr.sidebar-divider { border-top-color: rgba(255, 255, 255, 0.2); } 46 | .sidebar-heading { color: rgba(255, 255, 255, 0.5); } 47 | } 48 | } 49 | 50 | // --- Dark Mode --- 51 | &[data-bs-theme="dark"] { 52 | --bs-primary: #{$ocean-teal}; // Use teal as primary in dark 53 | --bs-primary-rgb: #{to-rgb($ocean-teal)}; 54 | --bs-secondary: #{$ocean-blue}; // Use blue as secondary 55 | --bs-secondary-rgb: #{to-rgb($ocean-blue)}; 56 | --bs-body-bg: #{$ocean-dark-bg}; 57 | --bs-body-color: #{$ocean-dark-text}; 58 | --bs-secondary-bg: #{lighten($ocean-dark-bg, 5%)}; 59 | --bs-tertiary-bg: #{lighten($ocean-dark-bg, 10%)}; 60 | --bs-border-color: #{lighten($ocean-dark-bg, 15%)}; 61 | --bs-heading-color: #{lighten($ocean-dark-text, 15%)}; 62 | 63 | .card { 64 | // --- Add Card Header Variables --- 65 | --bs-card-cap-bg: #{mix($ocean-teal, $ocean-dark-bg, 15%)}; // Example: A light mix 66 | --bs-card-cap-color: #{$ocean-deep-blue}; // Example: Darker heading text 67 | // --- End Add Card Header Variables --- 68 | } 69 | 70 | // Specific component overrides if needed 71 | --bs-link-color: #{$ocean-teal}; 72 | --bs-link-hover-color: #{lighten($ocean-teal, 15%)}; 73 | .bg-gradient-primary { // Example: Update sidebar gradient 74 | background-color: $ocean-deep-blue; 75 | background-image: linear-gradient(180deg, lighten($ocean-deep-blue, 10%) 10%, $ocean-deep-blue 100%); 76 | background-size: cover; 77 | } 78 | .sidebar { 79 | background-color: $ocean-deep-blue; // Base dark sidebar bg 80 | .nav-item .nav-link { 81 | color: rgba(255, 255, 255, 0.7); 82 | i { color: rgba(255, 255, 255, 0.4); } 83 | &:hover, &.active { color: #fff; i { color: #fff; } } 84 | } 85 | hr.sidebar-divider { border-top-color: rgba(255, 255, 255, 0.15); } 86 | .sidebar-heading { color: rgba(255, 255, 255, 0.4); } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /html/scss/themes/_theme-sunset.scss: -------------------------------------------------------------------------------- 1 | // scss/themes/_theme-sunset.scss 2 | // Defines variables for the Sunset theme 3 | 4 | $sunset-orange: #F76707; 5 | $sunset-purple: #7048E8; 6 | $sunset-red: #E03131; 7 | $sunset-light-bg: #FFF8E1; // Creamy light 8 | $sunset-dark-bg: #212529; // Very dark grey 9 | $sunset-dark-purple: #49299A; 10 | $sunset-dark-text: #F8F9FA; 11 | 12 | html[data-app-theme="sunset"] { 13 | // --- Light Mode --- 14 | &[data-bs-theme="light"] { 15 | --bs-primary: #{$sunset-orange}; 16 | --bs-primary-rgb: #{to-rgb($sunset-orange)}; 17 | --bs-secondary: #{$sunset-purple}; 18 | --bs-secondary-rgb: #{to-rgb($sunset-purple)}; 19 | --bs-danger: #{$sunset-red}; // Example: Tie theme color to semantic color 20 | --bs-danger-rgb: #{to-rgb($sunset-red)}; 21 | --bs-body-bg: #{$sunset-light-bg}; 22 | --bs-body-color: #{darken(desaturate($sunset-orange, 30%), 30%)}; // Muted dark orange text 23 | --bs-secondary-bg: #{mix($sunset-orange, $sunset-light-bg, 5%)}; 24 | --bs-tertiary-bg: #{mix($sunset-orange, $sunset-light-bg, 10%)}; 25 | --bs-border-color: #{mix($sunset-orange, $sunset-light-bg, 25%)}; 26 | --bs-heading-color: #{$sunset-red}; 27 | 28 | .card { 29 | // --- Add Card Header Variables --- 30 | --bs-card-cap-bg: #{mix($sunset-orange, $sunset-light-bg, 15%)}; // Example: A light mix 31 | --bs-card-cap-color: #{darken($sunset-red, 10%)}; // Example: Darker heading text 32 | // --- End Add Card Header Variables --- 33 | } 34 | 35 | --bs-link-color: #{$sunset-purple}; 36 | --bs-link-hover-color: #{darken($sunset-purple, 10%)}; 37 | 38 | .bg-gradient-primary { 39 | background-color: $sunset-orange; 40 | background-image: linear-gradient(180deg, lighten($sunset-orange, 10%) 10%, $sunset-orange 100%); 41 | } 42 | .sidebar { background-color: $sunset-orange; /* Adjust text colors as needed */ } 43 | } 44 | 45 | // --- Dark Mode --- 46 | &[data-bs-theme="dark"] { 47 | --bs-primary: #{lighten($sunset-orange, 15%)}; // Lighter orange 48 | --bs-primary-rgb: #{to-rgb(lighten($sunset-orange, 15%))}; 49 | --bs-secondary: #{$sunset-purple}; 50 | --bs-secondary-rgb: #{to-rgb($sunset-purple)}; 51 | --bs-danger: #{lighten($sunset-red, 10%)}; 52 | --bs-danger-rgb: #{to-rgb(lighten($sunset-red, 10%))}; 53 | --bs-body-bg: #{$sunset-dark-bg}; 54 | --bs-body-color: #{$sunset-dark-text}; 55 | --bs-secondary-bg: #{lighten($sunset-dark-bg, 5%)}; 56 | --bs-tertiary-bg: #{lighten($sunset-dark-bg, 10%)}; 57 | --bs-border-color: #{lighten($sunset-dark-bg, 15%)}; 58 | --bs-heading-color: #{lighten($sunset-dark-text, 15%)}; 59 | 60 | .card { 61 | // --- Add Card Header Variables --- 62 | --bs-card-cap-bg: #{mix(lighten($sunset-orange, 15%), $sunset-dark-bg, 15%)}; // Example: A light mix 63 | --bs-card-cap-color: #{$sunset-red}; // Example: Darker heading text 64 | // --- End Add Card Header Variables --- 65 | } 66 | 67 | --bs-link-color: #{lighten($sunset-orange, 20%)}; 68 | --bs-link-hover-color: #{lighten($sunset-orange, 30%)}; 69 | 70 | .bg-gradient-primary { 71 | background-color: $sunset-dark-purple; 72 | background-image: linear-gradient(180deg, lighten($sunset-dark-purple, 10%) 10%, $sunset-dark-purple 100%); 73 | } 74 | .sidebar { background-color: $sunset-dark-purple; /* Adjust text colors as needed */ } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /html/scss/utilities/_animation.scss: -------------------------------------------------------------------------------- 1 | // Animation Utilities 2 | 3 | // Grow In Animation 4 | 5 | @keyframes growIn { 6 | 0% { 7 | transform: scale(0.9); 8 | opacity: 0; 9 | } 10 | 100% { 11 | transform: scale(1); 12 | opacity: 1; 13 | } 14 | } 15 | 16 | .animated--grow-in { 17 | animation-name: growIn; 18 | animation-duration: 200ms; 19 | animation-timing-function: transform cubic-bezier(.18,1.25,.4,1), opacity cubic-bezier(0,1,.4,1); 20 | } 21 | 22 | // Fade In Animation 23 | 24 | @keyframes fadeIn { 25 | 0% { 26 | opacity: 0; 27 | } 28 | 100% { 29 | opacity: 1; 30 | } 31 | } 32 | 33 | .animated--fade-in { 34 | animation-name: fadeIn; 35 | animation-duration: 200ms; 36 | animation-timing-function: opacity cubic-bezier(0,1,.4,1); 37 | } 38 | -------------------------------------------------------------------------------- /html/scss/utilities/_background.scss: -------------------------------------------------------------------------------- 1 | // scss/utilities/_background.scss 2 | 3 | // Background Gradient Utilities 4 | // Note: Using functions like darken() with CSS vars in linear-gradient is tricky. 5 | // You might need to define explicit light/dark gradients or use simpler backgrounds. 6 | 7 | @each $color, $value in $theme-colors { 8 | .bg-gradient-#{$color} { 9 | // Apply the appropriate CSS variable based on $color 10 | // This requires more complex setup or manual var assignment for each color. 11 | // Using standard Bootstrap .bg-* and .text-* utilities is often easier. 12 | } 13 | } 14 | 15 | 16 | // Grayscale Background Utilities 17 | // Recommendation: Replace usages of .bg-gray-XXX with standard Bootstrap 18 | // background utilities like .bg-body, .bg-body-secondary, .bg-body-tertiary, 19 | // .bg-light, .bg-dark where possible. 20 | 21 | @each $level, $value in $grays { 22 | // Map to Bootstrap vars if needed, but prefer standard classes. 23 | // Example: 24 | // .bg-gray-100 { background-color: var(--bs-light) !important; } // If $gray-100 is your light bg 25 | // .bg-gray-200 { background-color: var(--bs-tertiary-bg) !important; } 26 | // .bg-gray-800 { background-color: var(--bs-dark-subtle) !important; } // ? Maybe 27 | // .bg-gray-900 { background-color: var(--bs-dark) !important; } 28 | } 29 | -------------------------------------------------------------------------------- /html/scss/utilities/_border.scss: -------------------------------------------------------------------------------- 1 | @each $color, $value in $theme-colors { 2 | @each $position in ['left', 'bottom'] { 3 | .border-#{$position}-#{$color} { 4 | border-#{$position}: .25rem solid $value !important; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /html/scss/utilities/_display.scss: -------------------------------------------------------------------------------- 1 | // Overflow Hidden 2 | .o-hidden { 3 | overflow: hidden !important; 4 | } 5 | -------------------------------------------------------------------------------- /html/scss/utilities/_progress.scss: -------------------------------------------------------------------------------- 1 | .progress-sm { 2 | height: .5rem; 3 | } 4 | -------------------------------------------------------------------------------- /html/scss/utilities/_rotate.scss: -------------------------------------------------------------------------------- 1 | .rotate-15 { 2 | transform: rotate(15deg); 3 | } 4 | 5 | .rotate-n-15 { 6 | transform: rotate(-15deg); 7 | } 8 | -------------------------------------------------------------------------------- /html/scss/utilities/_text.scss: -------------------------------------------------------------------------------- 1 | // scss/utilities/_text.scss 2 | 3 | // Grayscale Text Utilities 4 | 5 | // Recommendation: Replace usages of .text-gray-XXX in your HTML 6 | // with standard Bootstrap classes like .text-body-secondary, .text-muted, etc. 7 | // If you MUST keep these, you could try mapping them, but it might not be perfect: 8 | 9 | .text-xs { 10 | font-size: .7rem; 11 | } 12 | 13 | .text-lg { 14 | font-size: 1.2rem; 15 | } 16 | 17 | // --- Example Mapping (Use with caution) --- 18 | .text-gray-100 { // Often used for background text? Maybe var(--bs-body-bg)? Unlikely. 19 | color: var(--bs-gray-100) !important; // Assumes you have --bs-gray-100 defined, Bootstrap might not by default 20 | } 21 | .text-gray-200 { 22 | color: var(--bs-gray-200) !important; // Or map to e.g., var(--bs-tertiary-color) ? 23 | } 24 | .text-gray-300 { 25 | color: var(--bs-gray-300) !important; // Or map to e.g., var(--bs-border-color) ? 26 | } 27 | .text-gray-400 { 28 | color: var(--bs-gray-400) !important; // Or map to e.g., var(--bs-secondary-color) / var(--bs-border-color) ? 29 | } 30 | .text-gray-500 { 31 | color: var(--bs-secondary-color) !important; // Good candidate for secondary 32 | } 33 | .text-gray-600 { 34 | color: var(--bs-body-color) !important; // Good candidate for body default 35 | } 36 | .text-gray-700 { 37 | color: var(--bs-emphasis-color) !important; // Good candidate for emphasis 38 | } 39 | .text-gray-800 { 40 | color: var(--bs-dark) !important; // Or a darker emphasis var if available 41 | } 42 | .text-gray-900 { 43 | color: var(--bs-dark) !important; // Good candidate for dark text 44 | } 45 | // --- End Example Mapping --- 46 | 47 | 48 | .icon-circle { 49 | height: 2.5rem; 50 | width: 2.5rem; 51 | border-radius: 100%; 52 | display: flex; 53 | align-items: center; 54 | justify-content: center; 55 | // Background/color should be set by theme color utilities (.bg-primary, .text-white etc) in HTML 56 | } 57 | -------------------------------------------------------------------------------- /html/templates/about.html: -------------------------------------------------------------------------------- 1 |

About SNotePad

2 | 3 |

SNotePad is a straightforward, powerful note-taking application designed for simplicity and user control.

4 | 5 |
6 |

Our Philosophy: Your Notes, Your Files

7 |

In a world where data is often locked into specific apps or cloud services, SNotePad takes a different approach. We believe your notes are *your* data. That's why SNotePad saves every note directly as a standard text or Markdown file in folders you choose on your device's storage.

8 |

This means:

9 |
    10 |
  • You have full control: Access, copy, move, and manage your note files just like any other document using your phone's file manager.
  • 11 |
  • Easy Backup: Back up your notes simply by copying the files or folders to your computer, cloud storage, or external drive.
  • 12 |
  • No Lock-in: Your notes are in a standard format (plain text/Markdown). You can open and edit them with countless other applications on any platform, now or in the future.
  • 13 |
  • Privacy Focused: Your notes stay on your device unless *you* choose to move or share them.
  • 14 |
15 |

We aim to provide a reliable and flexible tool that respects your data ownership.

16 |
17 | 18 |
19 |

Core Features

20 |

SNotePad combines essential note-taking features with the flexibility of direct file management:

21 |
    22 |
  • Simple Markdown Editor: Write and format notes easily using the intuitive Markdown syntax with toolbar assistance.
  • 23 |
  • Folder Organization: Add and manage notes within standard folders on your device.
  • 24 |
  • File Sorting & Searching: Quickly find notes by sorting (name/date) or searching within folders (including file content search).
  • 25 |
  • Direct File Access: Notes are saved as plain files, ensuring accessibility and control.
  • 26 |
  • Customizable Themes: Choose between light, dark, and auto themes.
  • 27 |
  • Preview & Side-by-Side: View your formatted Markdown or see the source and preview together.
  • 28 |
29 |
30 | 31 |
32 |

Technology

33 |

SNotePad utilizes modern web technologies within an Android application wrapper:

34 |
    35 |
  • Frontend: HTML, CSS, and JavaScript.
  • 36 |
  • UI Framework: Bootstrap 5 (inferred from class names like `card`, `btn`, `nav-link`, `modal`, etc.).
  • 37 |
  • Markdown Editor: EasyMDE library.
  • 38 |
  • File Search: Fuse.js for fuzzy searching.
  • 39 |
  • Core Libraries: jQuery.
  • 40 |
  • Icons: Font Awesome (inferred from `fas fa-*` classes).
  • 41 |
  • Backend Interaction: Communicates with native Android code via a JavaScript interface (`AndroidInterface`) for secure access to the device's file system.
  • 42 |
43 |
44 | 45 |
46 |

Credits & License

47 |
48 |

aario.info - SNotePad v2.0.0

49 |

(Based on Start Bootstrap SB Admin 2 Theme)

50 |
Copyright 2025-2025 Aario Shahbany
51 |
52 | 53 |

This application utilizes several open-source libraries, including:

54 | 68 |

The SNotePad application code itself is licensed under the MIT License.

69 |
GPL v3 License
70 | 
71 | Copyright (c) 2025 Aario Shahbany
72 | 
73 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
74 | 
75 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
76 | 
77 | You should have received a copy of the GNU General Public License along with this program. If not, see .
78 |

It takes a lot of time and effort to write a software. If you found SNotepad usefull, please consider becoming a sponsor by clicking below button:
79 | 80 | 85 | Sponsor 86 | 87 | 88 |

90 | 91 |
92 |

SNotePad v2.0.0

93 |
94 | 95 | -------------------------------------------------------------------------------- /html/templates/editor.html: -------------------------------------------------------------------------------- 1 |
2 | 10 |
{path}
11 |
12 |
13 | 14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /html/templates/folderView-file.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 7 | 8 | 9 |
10 |
11 |
12 |
13 |
{basename}
14 |
15 | {date} 16 |
{path}
17 |
18 |
19 |
20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /html/templates/folderView.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 |
6 | 10 |
{basename}
11 |
12 |
13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /html/templates/navbar-editor.html: -------------------------------------------------------------------------------- 1 | 12 | 13 | 48 | -------------------------------------------------------------------------------- /html/templates/navbar-folderView.html: -------------------------------------------------------------------------------- 1 | 12 | 83 | -------------------------------------------------------------------------------- /html/templates/navbar-page.html: -------------------------------------------------------------------------------- 1 |

{title}

2 | -------------------------------------------------------------------------------- /html/templates/navbar-settings.html: -------------------------------------------------------------------------------- 1 |

Settings

2 | -------------------------------------------------------------------------------- /html/templates/settings-folder.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 7 |
8 |
9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 |
{basename}
17 |
18 | {path} 19 |
20 |
21 |
22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /html/templates/settings.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
Appearance
6 |
7 |
8 |
9 | 10 | 16 |
17 | 18 |
19 | 20 | 24 |
25 | 26 |
27 | Mode 28 |
29 |
30 |
31 | 32 | 35 |
36 |
37 | 38 | 41 |
42 |
43 | 44 | 47 |
48 |
49 |
50 |
51 |
52 |
53 | 56 |
57 |
Folders
58 |
Use to drag and drop and sort
59 |
60 |
61 |
62 |
63 |
64 |
65 | -------------------------------------------------------------------------------- /html/templates/sidebar-folder.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /html/templates/toast.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | {title} 5 | 6 |
7 |
8 | {message} 9 |
10 |
11 | -------------------------------------------------------------------------------- /keystore.env.example: -------------------------------------------------------------------------------- 1 | export KEYSTORE_FILE='/workspace/keystore.jks' 2 | export KEYSTORE_PASSWORD='' 3 | export KEY_PASSWORD='' 4 | export KEY_ALIAS='snotepad' 5 | -------------------------------------------------------------------------------- /logs.sh: -------------------------------------------------------------------------------- 1 | adb logcat --pid=$(adb shell pidof info.aario.snotepad) 2 | -------------------------------------------------------------------------------- /make-relase.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # --- Configuration --- 4 | # Path to the APK file to be attached to the release 5 | APK_PATH="./app/build/outputs/apk/release/app-release.apk" 6 | # GitHub repository in the format OWNER/REPO (usually inferred by gh) 7 | # REPO_URL="aario/snotepad" # uncomment and set if gh can't detect it automatically 8 | 9 | # --- Set Bash options for robustness --- 10 | # Exit immediately if a command exits with a non-zero status. 11 | set -e 12 | # Treat unset variables as an error when substituting. 13 | set -u 14 | # Pipe commands return the exit status of the last command in the pipe 15 | set -o pipefail 16 | 17 | # --- Pre-checks --- 18 | echo "--- Running Pre-checks ---" 19 | 20 | # 1. Check if inside a Git repository 21 | if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then 22 | echo "ERROR: This script must be run from within a Git repository." 23 | exit 1 24 | fi 25 | echo "✓ Git repository detected." 26 | 27 | # 2. Check if the GitHub CLI (gh) is installed 28 | if ! command -v gh &> /dev/null; then 29 | echo "ERROR: GitHub CLI (gh) not found. Please install it (https://cli.github.com/) and authenticate (gh auth login)." 30 | exit 1 31 | fi 32 | echo "✓ GitHub CLI (gh) found." 33 | 34 | # 3. Check if gh is authenticated 35 | if ! gh auth status > /dev/null 2>&1; then 36 | echo "ERROR: Not authenticated with GitHub CLI. Please run 'gh auth login'." 37 | exit 1 38 | fi 39 | echo "✓ GitHub CLI is authenticated." 40 | 41 | # 4. Check if the APK file exists 42 | if [ ! -f "$APK_PATH" ]; then 43 | echo "ERROR: APK file not found at '$APK_PATH'. Please build the release APK first." 44 | exit 1 45 | fi 46 | echo "✓ APK file found at '$APK_PATH'." 47 | 48 | echo "--- Pre-checks completed successfully ---" 49 | echo 50 | 51 | # Find the latest tag in the history 52 | LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") 53 | 54 | echo "Last tag found: $LATEST_TAG" 55 | # --- Get Tag Name --- 56 | read -p "Enter the tag name for the new release (e.g., v1.0.0): " TAG_NAME 57 | 58 | if [ -z "$TAG_NAME" ]; then 59 | echo "ERROR: Tag name cannot be empty." 60 | exit 1 61 | fi 62 | 63 | # Check if the tag already exists locally or remotely 64 | if git rev-parse "$TAG_NAME" >/dev/null 2>&1 || git ls-remote --tags origin | grep -q "refs/tags/$TAG_NAME$"; then 65 | echo "ERROR: Tag '$TAG_NAME' already exists locally or remotely." 66 | exit 1 67 | fi 68 | 69 | echo "Using tag name: $TAG_NAME" 70 | echo 71 | 72 | # --- Generate Release Notes --- 73 | echo "--- Generating Release Notes ---" 74 | 75 | RELEASE_NOTES="" 76 | 77 | if [ -z "$LATEST_TAG" ]; then 78 | echo "No previous tags found. Generating notes from all commits." 79 | # If no tags exist, get all commit messages 80 | RELEASE_NOTES=$(git log --pretty=format:"* %s (%h)") 81 | else 82 | echo "Generating notes from commits since tag '$LATEST_TAG'." 83 | # Get commit messages since the last tag 84 | RELEASE_NOTES=$(git log "${LATEST_TAG}"..HEAD --pretty=format:"* %s (%h)") 85 | fi 86 | 87 | if [ -z "$RELEASE_NOTES" ]; then 88 | echo "Warning: No new commits found since the last tag (or no commits found at all)." 89 | RELEASE_NOTES="No changes detected." 90 | exit 1 91 | fi 92 | 93 | echo "--- Release Notes Generated ---" 94 | echo "$RELEASE_NOTES" 95 | echo "------------------------------" 96 | echo 97 | 98 | # --- Create and Push Git Tag --- 99 | echo "--- Tagging and Pushing ---" 100 | echo "Creating local tag '$TAG_NAME'..." 101 | # Create a lightweight tag pointing to the latest commit 102 | git tag "$TAG_NAME" 103 | 104 | echo "Pushing tag '$TAG_NAME' to remote 'origin'..." 105 | # Push the tag to the remote repository 106 | git push origin "$TAG_NAME" 107 | echo "✓ Tag '$TAG_NAME' pushed successfully." 108 | echo 109 | 110 | # --- Create GitHub Release --- 111 | echo "--- Creating GitHub Release ---" 112 | echo "Creating release '$TAG_NAME' on GitHub..." 113 | 114 | # Use gh release create command 115 | # It automatically uses the tag name for the release title 116 | # -t specifies the title (using tag name again for clarity) 117 | # -n specifies the notes 118 | # The last argument is the file to attach 119 | gh release create "$TAG_NAME" \ 120 | --title "$TAG_NAME" \ 121 | --notes "$RELEASE_NOTES" \ 122 | "$APK_PATH" 123 | 124 | # Check the exit status of the gh command 125 | if [ $? -eq 0 ]; then 126 | echo "✓ GitHub release '$TAG_NAME' created successfully!" 127 | echo "You can view it at: https://github.com/$(gh repo view --json nameWithOwner -q .nameWithOwner)/releases/tag/$TAG_NAME" 128 | else 129 | echo "ERROR: Failed to create GitHub release." 130 | # Attempt to clean up the pushed tag if release creation failed 131 | echo "Attempting to delete remote tag '$TAG_NAME'..." 132 | git push --delete origin "$TAG_NAME" || echo "Warning: Failed to delete remote tag '$TAG_NAME'. Manual cleanup might be required." 133 | echo "Attempting to delete local tag '$TAG_NAME'..." 134 | git tag -d "$TAG_NAME" || echo "Warning: Failed to delete local tag '$TAG_NAME'. Manual cleanup might be required." 135 | exit 1 136 | fi 137 | 138 | echo "--- Script Finished ---" 139 | 140 | exit 0 141 | -------------------------------------------------------------------------------- /screenshots/Screenshot_20250424-103751_SNotePad.jpg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/screenshots/Screenshot_20250424-103751_SNotePad.jpg.png -------------------------------------------------------------------------------- /screenshots/Screenshot_20250424-103801_SNotePad.jpg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/screenshots/Screenshot_20250424-103801_SNotePad.jpg.png -------------------------------------------------------------------------------- /screenshots/Screenshot_20250424-103842_SNotePad.jpg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/screenshots/Screenshot_20250424-103842_SNotePad.jpg.png -------------------------------------------------------------------------------- /screenshots/Screenshot_20250424-104348_SNotePad.jpg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/screenshots/Screenshot_20250424-104348_SNotePad.jpg.png -------------------------------------------------------------------------------- /screenshots/Screenshot_20250424-104555_SNotePad.jpg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aario/snotepad/6fb9f6e05334efb5787fd30b63f5c4251e4d731a/screenshots/Screenshot_20250424-104555_SNotePad.jpg.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | // settings.gradle 2 | // Defines the modules included in the build. 3 | 4 | pluginManagement { 5 | repositories { 6 | google() // Repository for Android artifacts 7 | mavenCentral() // General repository for Java/Kotlin libraries 8 | gradlePluginPortal() // Repository for Gradle plugins 9 | } 10 | } 11 | dependencyResolutionManagement { 12 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 13 | repositories { 14 | google() 15 | mavenCentral() 16 | } 17 | } 18 | 19 | // Include the 'app' module in the project 20 | rootProject.name = "SNotePad" 21 | include ':app' 22 | 23 | --------------------------------------------------------------------------------