From 54361ee6a3c9f6b6f730f45181db41df3f2da9e7 Mon Sep 17 00:00:00 2001 From: John Smith Date: Sun, 6 Sep 2026 20:28:06 -0400 Subject: [PATCH] feat: initial release of Pwnapple iOS 6 skeuomorphic UI library for Android --- .github/workflows/ci.yml | 33 ++ .github/workflows/release.yml | 35 ++ .gitignore | 11 + LICENSE | 21 ++ README.md | 238 +++++++++++++ build.gradle.kts | 6 + gradle.properties | 5 + gradle/libs.versions.toml | 36 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43583 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 253 ++++++++++++++ jitpack.yml | 6 + pwnapple/build.gradle.kts | 98 ++++++ pwnapple/consumer-rules.pro | 0 pwnapple/proguard-rules.pro | 0 pwnapple/src/main/AndroidManifest.xml | 3 + .../ai/heretek/pwnapple/audio/SoundPlayer.kt | 129 +++++++ .../pwnapple/components/button/IosButton.kt | 119 +++++++ .../components/button/IosSegmentedControl.kt | 147 ++++++++ .../components/dialog/IosActionSheet.kt | 134 +++++++ .../pwnapple/components/dialog/IosAlert.kt | 162 +++++++++ .../indicator/IosActivityIndicator.kt | 65 ++++ .../components/indicator/IosProgressBar.kt | 61 ++++ .../pwnapple/components/input/IosSlider.kt | 157 +++++++++ .../pwnapple/components/input/IosSwitch.kt | 169 +++++++++ .../pwnapple/components/input/IosTextField.kt | 122 +++++++ .../components/navigation/IosBackButton.kt | 95 +++++ .../components/navigation/IosNavigationBar.kt | 114 ++++++ .../components/picker/IosPickerWheel.kt | 221 ++++++++++++ .../components/table/IosGroupedTable.kt | 35 ++ .../pwnapple/components/table/IosTableCell.kt | 201 +++++++++++ .../components/table/IosTableSection.kt | 49 +++ .../pwnapple/components/tabs/IosBadge.kt | 64 ++++ .../pwnapple/components/tabs/IosTabBar.kt | 144 ++++++++ .../pwnapple/graphics/ArrowBackShape.kt | 53 +++ .../pwnapple/graphics/BevelModifiers.kt | 54 +++ .../pwnapple/graphics/GlossModifiers.kt | 109 ++++++ .../ai/heretek/pwnapple/graphics/Textures.kt | 67 ++++ .../heretek/pwnapple/theme/PwnappleAudio.kt | 22 ++ .../heretek/pwnapple/theme/PwnappleColors.kt | 132 +++++++ .../heretek/pwnapple/theme/PwnappleTheme.kt | 46 +++ .../pwnapple/theme/PwnappleTypography.kt | 109 ++++++ pwnapple/src/main/res/values/strings.xml | 3 + .../ai/heretek/pwnapple/PwnappleCoreTest.kt | 39 +++ sample/build.gradle.kts | 62 ++++ sample/proguard-rules.pro | 0 sample/src/main/AndroidManifest.xml | 19 + .../heretek/pwnapple/sample/MainActivity.kt | 74 ++++ .../pwnapple/sample/screens/AlertsScreen.kt | 122 +++++++ .../pwnapple/sample/screens/ControlsScreen.kt | 178 ++++++++++ .../pwnapple/sample/screens/PickersScreen.kt | 92 +++++ .../pwnapple/sample/screens/SettingsScreen.kt | 327 ++++++++++++++++++ sample/src/main/res/values/strings.xml | 3 + settings.gradle.kts | 24 ++ 54 files changed, 4475 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 jitpack.yml create mode 100644 pwnapple/build.gradle.kts create mode 100644 pwnapple/consumer-rules.pro create mode 100644 pwnapple/proguard-rules.pro create mode 100644 pwnapple/src/main/AndroidManifest.xml create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/audio/SoundPlayer.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/components/button/IosButton.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/components/button/IosSegmentedControl.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/components/dialog/IosActionSheet.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/components/dialog/IosAlert.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/components/indicator/IosActivityIndicator.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/components/indicator/IosProgressBar.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/components/input/IosSlider.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/components/input/IosSwitch.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/components/input/IosTextField.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/components/navigation/IosBackButton.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/components/navigation/IosNavigationBar.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/components/picker/IosPickerWheel.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/components/table/IosGroupedTable.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/components/table/IosTableCell.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/components/table/IosTableSection.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/components/tabs/IosBadge.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/components/tabs/IosTabBar.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/graphics/ArrowBackShape.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/graphics/BevelModifiers.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/graphics/GlossModifiers.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/graphics/Textures.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/theme/PwnappleAudio.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/theme/PwnappleColors.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/theme/PwnappleTheme.kt create mode 100644 pwnapple/src/main/java/ai/heretek/pwnapple/theme/PwnappleTypography.kt create mode 100644 pwnapple/src/main/res/values/strings.xml create mode 100644 pwnapple/src/test/java/ai/heretek/pwnapple/PwnappleCoreTest.kt create mode 100644 sample/build.gradle.kts create mode 100644 sample/proguard-rules.pro create mode 100644 sample/src/main/AndroidManifest.xml create mode 100644 sample/src/main/java/ai/heretek/pwnapple/sample/MainActivity.kt create mode 100644 sample/src/main/java/ai/heretek/pwnapple/sample/screens/AlertsScreen.kt create mode 100644 sample/src/main/java/ai/heretek/pwnapple/sample/screens/ControlsScreen.kt create mode 100644 sample/src/main/java/ai/heretek/pwnapple/sample/screens/PickersScreen.kt create mode 100644 sample/src/main/java/ai/heretek/pwnapple/sample/screens/SettingsScreen.kt create mode 100644 sample/src/main/res/values/strings.xml create mode 100644 settings.gradle.kts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ea88497 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Java 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Run Tests & Build Release AAR + run: ./gradlew test :pwnapple:assembleRelease :sample:assembleDebug --stacktrace + + - name: Upload Library AAR + uses: actions/upload-artifact@v4 + with: + name: pwnapple-release-aar + path: pwnapple/build/outputs/aar/pwnapple-release.aar diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8b78140 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,35 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Java 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build Release Bundle & AAR + run: ./gradlew :pwnapple:assembleRelease :pwnapple:publishToMavenLocal + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: | + pwnapple/build/outputs/aar/pwnapple-release.aar + generate_release_notes: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5fe00bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +*/build +.cxx +captures +.externalNativeBuild +.system_generated diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..04e8eeb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024-2026 Heretek AI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f1b1ede --- /dev/null +++ b/README.md @@ -0,0 +1,238 @@ +# Pwnapple 🍎📱 + +> **Authentic iOS 6 Skeuomorphic Design System & UI Library for Android built with Jetpack Compose.** + +[![CI](https://github.com/Heretek-AI/pwnapple/actions/workflows/ci.yml/badge.svg)](https://github.com/Heretek-AI/pwnapple/actions/workflows/ci.yml) +[![JitPack](https://jitpack.io/v/Heretek-AI/pwnapple.svg)](https://jitpack.io/#Heretek-AI/pwnapple) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![Platform](https://img.shields.io/badge/Platform-Android_API_24+-brightgreen.svg)](https://developer.android.com) +[![Kotlin](https://img.shields.io/badge/Kotlin-2.0+-purple.svg)](https://kotlinlang.org) +[![Compose](https://img.shields.io/badge/Jetpack_Compose-2025.02-green.svg)](https://developer.android.com/jetpack/compose) + +--- + +## 🌟 Overview + +**Pwnapple** is a pixel-accurate recreation of the iconic, beloved **iOS 6 skeuomorphic design language** for modern Android apps using **Jetpack Compose**. + +Remember the days of glossy resin buttons, brushed aluminum, stitched leather, woven linen, crisp drop shadows, and satisfying tactile clicks? Pwnapple brings that warmth, depth, and tangible texture back to mobile development. + +### ✨ Features +- 🎨 **Faithful Skeuomorphic Graphics Engine**: + - `Modifier.glossHighlight()`: 3D resin specular curved sheen. + - `Modifier.innerShadow()`: Inset depth for recessed controls. + - `Modifier.skeuoBevel()`: Multi-layer outer borders with top highlight and bottom shadow. + - `Modifier.pinstripeBackground()`: The classic vertical pinstripe wallpaper (`#C5CCD4`). + - `Modifier.darkLinenBackground()`: Dark woven linen texture. + - `ArrowBackShape`: Iconic chevron back button outline with pointed arrowhead. +- 🎛️ **Comprehensive Component Suite**: + - **Navigation**: `IosNavigationBar` (Gloss Blue, Slate Black) & `IosBackButton`. + - **Buttons**: `IosButton` (Silver, Action Blue, Destructive Red, Black) with depressed states & `IosSegmentedControl`. + - **Inputs**: `IosSwitch` (the legendary sliding ON/OFF toggle), `IosSlider`, and inset `IosTextField`. + - **Lists**: `IosGroupedTable`, `IosTableCell` (disclosure chevrons, icons, subtitles, interactive selection), and `IosTableSection`. + - **Tabs**: `IosTabBar` with cyan active glow & `IosBadge` (glossy red notification bubble). + - **Modals**: `IosAlert` (translucent navy UIAlertView) and `IosActionSheet` (bottom linen sheet). + - **Pickers**: `IosPickerWheel` (3D rotating drum cylinder with cylindrical perspective and glass magnifying lens). + - **Indicators**: `IosProgressBar` and `IosActivityIndicator` (12-blade rotating gear spinner). +- 🔊 **Zero-Asset Tactile Audio & Haptics**: + - Embedded real-time PCM audio synthesizer (`SoundPlayer`) using Android's native `AudioTrack`. + - Generates authentic mechanical clicks, drum ratchet ticks, and switch snap latches on the fly with sub-5ms latency and **0 bytes of external audio files**. + - Toggleable or customizable via `LocalPwnappleSoundFeedback`. +- 📱 **Interactive Showcase App**: + - A pixel-perfect clone of the classic iOS 6 **Settings.app** with interactive sub-screens (Wi-Fi network chooser, Sounds ringer slider, About screen). + - Live interactive Component Catalog and Dialog Playground. + +--- + +## 📦 Installation + +Add the JitPack repository to your root `settings.gradle.kts`: + +```kotlin +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + maven { url = uri("https://jitpack.io") } + } +} +``` + +Add the dependency to your module's `build.gradle.kts`: + +```kotlin +dependencies { + implementation("com.github.Heretek-AI:pwnapple:1.0.0") +} +``` + +--- + +## 🚀 Quick Start + +### 1. Wrap Your App in `PwnappleTheme` + +```kotlin +import ai.heretek.pwnapple.theme.PwnappleTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + PwnappleTheme(soundFeedbackEnabled = true) { + MyAppContent() + } + } + } +} +``` + +### 2. Grouped Table (Settings Style) + +```kotlin +import ai.heretek.pwnapple.components.input.IosSwitch +import ai.heretek.pwnapple.components.table.IosCellAccessory +import ai.heretek.pwnapple.components.table.IosTableCell +import ai.heretek.pwnapple.components.table.IosTableSection +import ai.heretek.pwnapple.graphics.pinstripeBackground + +Column( + modifier = Modifier + .fillMaxSize() + .pinstripeBackground() +) { + IosTableSection( + headerText = "CONNECTIVITY", + footerText = "Turn on Airplane Mode to disable all wireless connections." + ) { + IosTableCell( + title = "Airplane Mode", + accessoryContent = { + IosSwitch( + checked = isAirplaneMode, + onCheckedChange = { isAirplaneMode = it } + ) + } + ) + IosTableCell( + title = "Wi-Fi", + detailText = "Heretek_5G", + accessory = IosCellAccessory.DisclosureIndicator, + onClick = { /* Navigate to Wi-Fi settings */ } + ) + } +} +``` + +### 3. Navigation Bar & Chevron Back Button + +```kotlin +import ai.heretek.pwnapple.components.navigation.IosBackButton +import ai.heretek.pwnapple.components.navigation.IosNavigationBar + +IosNavigationBar( + title = "General", + navigationIcon = { + IosBackButton(text = "Settings", onClick = { navController.popBackStack() }) + } +) +``` + +### 4. Skeuomorphic Buttons + +```kotlin +import ai.heretek.pwnapple.components.button.IosButton +import ai.heretek.pwnapple.components.button.IosButtonStyle + +// Standard Silver Button +IosButton( + text = "Save to Camera Roll", + onClick = { /* action */ }, + style = IosButtonStyle.Silver +) + +// Blue Action Button +IosButton( + text = "Confirm Purchase", + onClick = { /* action */ }, + style = IosButtonStyle.Blue +) + +// Destructive Red Button +IosButton( + text = "Delete Account", + onClick = { /* action */ }, + style = IosButtonStyle.Red +) +``` + +### 5. 3D Drum Wheel Picker (UIPickerView) + +```kotlin +import ai.heretek.pwnapple.components.picker.IosPickerWheel + +var selectedIndex by remember { mutableIntStateOf(0) } +val items = listOf("iPhone 3GS", "iPhone 4", "iPhone 4S", "iPhone 5") + +IosPickerWheel( + items = items, + selectedIndex = selectedIndex, + onSelectedIndexChange = { selectedIndex = it } +) +``` + +### 6. Modal Dialogs (UIAlertView & UIActionSheet) + +```kotlin +import ai.heretek.pwnapple.components.dialog.IosAlert +import ai.heretek.pwnapple.components.dialog.IosActionSheet + +if (showAlert) { + IosAlert( + title = "Cannot Connect to iTunes Store", + message = "Please verify your internet connection and try again.", + confirmButtonText = "OK", + onDismissRequest = { showAlert = false } + ) +} +``` + +--- + +## 🔊 Audio Feedback Customization + +Audio cues are generated on-the-fly using native PCM audio: +- To disable audio globally: + ```kotlin + PwnappleTheme(soundFeedbackEnabled = false) { ... } + ``` +- To toggle dynamically at runtime: + ```kotlin + val soundFeedback = LocalPwnappleSoundFeedback.current + soundFeedback.enabled = false // silences all clicks + ``` + +--- + +## 🛠️ Building & Running the Sample App + +1. Clone the repository: + ```bash + git clone https://github.com/Heretek-AI/pwnapple.git + cd pwnapple + ``` +2. Build the library and sample application: + ```bash + ./gradlew :pwnapple:assembleRelease :sample:assembleDebug + ``` +3. Run tests: + ```bash + ./gradlew test + ``` + +--- + +## 📄 License + +Pwnapple is open-source software licensed under the [MIT License](LICENSE). +Created and maintained by [Heretek AI](https://github.com/Heretek-AI). diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..932072e --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,6 @@ +plugins { + alias(libs.plugins.androidApplication) apply false + alias(libs.plugins.androidLibrary) apply false + alias(libs.plugins.jetbrainsKotlinAndroid) apply false + alias(libs.plugins.compose.compiler) apply false +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..c78fe95 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +org.gradle.java.home=/home/linuxbrew/.linuxbrew/opt/openjdk@21 +android.useAndroidX=true +android.nonTransitiveRClass=true +kotlin.code.style=official diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..28a26ff --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,36 @@ +[versions] +agp = "8.10.1" +kotlin = "2.0.0" +coreKtx = "1.16.0" +lifecycleRuntimeKtx = "2.8.7" +activityCompose = "1.10.1" +navigationCompose = "2.8.9" +composeBom = "2025.02.00" +junit = "4.13.2" +androidxJunit = "1.2.1" +espressoCore = "3.6.1" + +[libraries] +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } +androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core" } +androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxJunit" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } + +[plugins] +androidApplication = { id = "com.android.application", version.ref = "agp" } +androidLibrary = { id = "com.android.library", version.ref = "agp" } +jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +maven-publish = { id = "maven-publish" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..a4b76b9530d66f5e68d973ea569d8e19de379189 GIT binary patch literal 43583 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 yu?e%!_ptzGB|8GrCfu%p?(_ zQccdaaVK$5bz;*rnyK{_SQYM>;aES6Qs^lj9lEs6_J+%nIiuQC*fN;z8md>r_~Mfl zU%p5Dt_YT>gQqfr@`cR!$NWr~+`CZb%dn;WtzrAOI>P_JtsB76PYe*<%H(y>qx-`Kq!X_; z<{RpAqYhE=L1r*M)gNF3B8r(<%8mo*SR2hu zccLRZwGARt)Hlo1euqTyM>^!HK*!Q2P;4UYrysje@;(<|$&%vQekbn|0Ruu_Io(w4#%p6ld2Yp7tlA`Y$cciThP zKzNGIMPXX%&Ud0uQh!uQZz|FB`4KGD?3!ND?wQt6!n*f4EmCoJUh&b?;B{|lxs#F- z31~HQ`SF4x$&v00@(P+j1pAaj5!s`)b2RDBp*PB=2IB>oBF!*6vwr7Dp%zpAx*dPr zb@Zjq^XjN?O4QcZ*O+8>)|HlrR>oD*?WQl5ri3R#2?*W6iJ>>kH%KnnME&TT@ZzrHS$Q%LC?n|e>V+D+8D zYc4)QddFz7I8#}y#Wj6>4P%34dZH~OUDb?uP%-E zwjXM(?Sg~1!|wI(RVuxbu)-rH+O=igSho_pDCw(c6b=P zKk4ATlB?bj9+HHlh<_!&z0rx13K3ZrAR8W)!@Y}o`?a*JJsD+twZIv`W)@Y?Amu_u zz``@-e2X}27$i(2=9rvIu5uTUOVhzwu%mNazS|lZb&PT;XE2|B&W1>=B58#*!~D&) zfVmJGg8UdP*fx(>Cj^?yS^zH#o-$Q-*$SnK(ZVFkw+er=>N^7!)FtP3y~Xxnu^nzY zikgB>Nj0%;WOltWIob|}%lo?_C7<``a5hEkx&1ku$|)i>Rh6@3h*`slY=9U}(Ql_< zaNG*J8vb&@zpdhAvv`?{=zDedJ23TD&Zg__snRAH4eh~^oawdYi6A3w8<Ozh@Kw)#bdktM^GVb zrG08?0bG?|NG+w^&JvD*7LAbjED{_Zkc`3H!My>0u5Q}m!+6VokMLXxl`Mkd=g&Xx z-a>m*#G3SLlhbKB!)tnzfWOBV;u;ftU}S!NdD5+YtOjLg?X}dl>7m^gOpihrf1;PY zvll&>dIuUGs{Qnd- zwIR3oIrct8Va^Tm0t#(bJD7c$Z7DO9*7NnRZorrSm`b`cxz>OIC;jSE3DO8`hX955ui`s%||YQtt2 z5DNA&pG-V+4oI2s*x^>-$6J?p=I>C|9wZF8z;VjR??Icg?1w2v5Me+FgAeGGa8(3S z4vg*$>zC-WIVZtJ7}o9{D-7d>zCe|z#<9>CFve-OPAYsneTb^JH!Enaza#j}^mXy1 z+ULn^10+rWLF6j2>Ya@@Kq?26>AqK{A_| zQKb*~F1>sE*=d?A?W7N2j?L09_7n+HGi{VY;MoTGr_)G9)ot$p!-UY5zZ2Xtbm=t z@dpPSGwgH=QtIcEulQNI>S-#ifbnO5EWkI;$A|pxJd885oM+ zGZ0_0gDvG8q2xebj+fbCHYfAXuZStH2j~|d^sBAzo46(K8n59+T6rzBwK)^rfPT+B zyIFw)9YC-V^rhtK`!3jrhmW-sTmM+tPH+;nwjL#-SjQPUZ53L@A>y*rt(#M(qsiB2 zx6B)dI}6Wlsw%bJ8h|(lhkJVogQZA&n{?Vgs6gNSXzuZpEyu*xySy8ro07QZ7Vk1!3tJphN_5V7qOiyK8p z#@jcDD8nmtYi1^l8ml;AF<#IPK?!pqf9D4moYk>d99Im}Jtwj6c#+A;f)CQ*f-hZ< z=p_T86jog%!p)D&5g9taSwYi&eP z#JuEK%+NULWus;0w32-SYFku#i}d~+{Pkho&^{;RxzP&0!RCm3-9K6`>KZpnzS6?L z^H^V*s!8<>x8bomvD%rh>Zp3>Db%kyin;qtl+jAv8Oo~1g~mqGAC&Qi_wy|xEt2iz zWAJEfTV%cl2Cs<1L&DLRVVH05EDq`pH7Oh7sR`NNkL%wi}8n>IXcO40hp+J+sC!W?!krJf!GJNE8uj zg-y~Ns-<~D?yqbzVRB}G>0A^f0!^N7l=$m0OdZuqAOQqLc zX?AEGr1Ht+inZ-Qiwnl@Z0qukd__a!C*CKuGdy5#nD7VUBM^6OCpxCa2A(X;e0&V4 zM&WR8+wErQ7UIc6LY~Q9x%Sn*Tn>>P`^t&idaOEnOd(Ufw#>NoR^1QdhJ8s`h^|R_ zXX`c5*O~Xdvh%q;7L!_!ohf$NfEBmCde|#uVZvEo>OfEq%+Ns7&_f$OR9xsihRpBb z+cjk8LyDm@U{YN>+r46?nn{7Gh(;WhFw6GAxtcKD+YWV?uge>;+q#Xx4!GpRkVZYu zzsF}1)7$?%s9g9CH=Zs+B%M_)+~*j3L0&Q9u7!|+T`^O{xE6qvAP?XWv9_MrZKdo& z%IyU)$Q95AB4!#hT!_dA>4e@zjOBD*Y=XjtMm)V|+IXzjuM;(l+8aA5#Kaz_$rR6! zj>#&^DidYD$nUY(D$mH`9eb|dtV0b{S>H6FBfq>t5`;OxA4Nn{J(+XihF(stSche7$es&~N$epi&PDM_N`As;*9D^L==2Q7Z2zD+CiU(|+-kL*VG+&9!Yb3LgPy?A zm7Z&^qRG_JIxK7-FBzZI3Q<;{`DIxtc48k> zc|0dmX;Z=W$+)qE)~`yn6MdoJ4co;%!`ddy+FV538Y)j(vg}5*k(WK)KWZ3WaOG!8 z!syGn=s{H$odtpqFrT#JGM*utN7B((abXnpDM6w56nhw}OY}0TiTG1#f*VFZr+^-g zbP10`$LPq_;PvrA1XXlyx2uM^mrjTzX}w{yuLo-cOClE8MMk47T25G8M!9Z5ypOSV zAJUBGEg5L2fY)ZGJb^E34R2zJ?}Vf>{~gB!8=5Z) z9y$>5c)=;o0HeHHSuE4U)#vG&KF|I%-cF6f$~pdYJWk_dD}iOA>iA$O$+4%@>JU08 zS`ep)$XLPJ+n0_i@PkF#ri6T8?ZeAot$6JIYHm&P6EB=BiaNY|aA$W0I+nz*zkz_z zkEru!tj!QUffq%)8y0y`T&`fuus-1p>=^hnBiBqD^hXrPs`PY9tU3m0np~rISY09> z`P3s=-kt_cYcxWd{de@}TwSqg*xVhp;E9zCsnXo6z z?f&Sv^U7n4`xr=mXle94HzOdN!2kB~4=%)u&N!+2;z6UYKUDqi-s6AZ!haB;@&B`? z_TRX0%@suz^TRdCb?!vNJYPY8L_}&07uySH9%W^Tc&1pia6y1q#?*Drf}GjGbPjBS zbOPcUY#*$3sL2x4v_i*Y=N7E$mR}J%|GUI(>WEr+28+V z%v5{#e!UF*6~G&%;l*q*$V?&r$Pp^sE^i-0$+RH3ERUUdQ0>rAq2(2QAbG}$y{de( z>{qD~GGuOk559Y@%$?N^1ApVL_a704>8OD%8Y%8B;FCt%AoPu8*D1 zLB5X>b}Syz81pn;xnB}%0FnwazlWfUV)Z-~rZg6~b z6!9J$EcE&sEbzcy?CI~=boWA&eeIa%z(7SE^qgVLz??1Vbc1*aRvc%Mri)AJaAG!p z$X!_9Ds;Zz)f+;%s&dRcJt2==P{^j3bf0M=nJd&xwUGlUFn?H=2W(*2I2Gdu zv!gYCwM10aeus)`RIZSrCK=&oKaO_Ry~D1B5!y0R=%!i2*KfXGYX&gNv_u+n9wiR5 z*e$Zjju&ODRW3phN925%S(jL+bCHv6rZtc?!*`1TyYXT6%Ju=|X;6D@lq$8T zW{Y|e39ioPez(pBH%k)HzFITXHvnD6hw^lIoUMA;qAJ^CU?top1fo@s7xT13Fvn1H z6JWa-6+FJF#x>~+A;D~;VDs26>^oH0EI`IYT2iagy23?nyJ==i{g4%HrAf1-*v zK1)~@&(KkwR7TL}L(A@C_S0G;-GMDy=MJn2$FP5s<%wC)4jC5PXoxrQBFZ_k0P{{s@sz+gX`-!=T8rcB(=7vW}^K6oLWMmp(rwDh}b zwaGGd>yEy6fHv%jM$yJXo5oMAQ>c9j`**}F?MCry;T@47@r?&sKHgVe$MCqk#Z_3S z1GZI~nOEN*P~+UaFGnj{{Jo@16`(qVNtbU>O0Hf57-P>x8Jikp=`s8xWs^dAJ9lCQ z)GFm+=OV%AMVqVATtN@|vp61VVAHRn87}%PC^RAzJ%JngmZTasWBAWsoAqBU+8L8u z4A&Pe?fmTm0?mK-BL9t+{y7o(7jm+RpOhL9KnY#E&qu^}B6=K_dB}*VlSEiC9fn)+V=J;OnN)Ta5v66ic1rG+dGAJ1 z1%Zb_+!$=tQ~lxQrzv3x#CPb?CekEkA}0MYSgx$Jdd}q8+R=ma$|&1a#)TQ=l$1tQ z=tL9&_^vJ)Pk}EDO-va`UCT1m#Uty1{v^A3P~83_#v^ozH}6*9mIjIr;t3Uv%@VeW zGL6(CwCUp)Jq%G0bIG%?{_*Y#5IHf*5M@wPo6A{$Um++Co$wLC=J1aoG93&T7Ho}P z=mGEPP7GbvoG!uD$k(H3A$Z))+i{Hy?QHdk>3xSBXR0j!11O^mEe9RHmw!pvzv?Ua~2_l2Yh~_!s1qS`|0~0)YsbHSz8!mG)WiJE| z2f($6TQtt6L_f~ApQYQKSb=`053LgrQq7G@98#igV>y#i==-nEjQ!XNu9 z~;mE+gtj4IDDNQJ~JVk5Ux6&LCSFL!y=>79kE9=V}J7tD==Ga+IW zX)r7>VZ9dY=V&}DR))xUoV!u(Z|%3ciQi_2jl}3=$Agc(`RPb z8kEBpvY>1FGQ9W$n>Cq=DIpski};nE)`p3IUw1Oz0|wxll^)4dq3;CCY@RyJgFgc# zKouFh!`?Xuo{IMz^xi-h=StCis_M7yq$u) z?XHvw*HP0VgR+KR6wI)jEMX|ssqYvSf*_3W8zVTQzD?3>H!#>InzpSO)@SC8q*ii- z%%h}_#0{4JG;Jm`4zg};BPTGkYamx$Xo#O~lBirRY)q=5M45n{GCfV7h9qwyu1NxOMoP4)jjZMxmT|IQQh0U7C$EbnMN<3)Kk?fFHYq$d|ICu>KbY_hO zTZM+uKHe(cIZfEqyzyYSUBZa8;Fcut-GN!HSA9ius`ltNebF46ZX_BbZNU}}ZOm{M2&nANL9@0qvih15(|`S~z}m&h!u4x~(%MAO$jHRWNfuxWF#B)E&g3ghSQ9|> z(MFaLQj)NE0lowyjvg8z0#m6FIuKE9lDO~Glg}nSb7`~^&#(Lw{}GVOS>U)m8bF}x zVjbXljBm34Cs-yM6TVusr+3kYFjr28STT3g056y3cH5Tmge~ASxBj z%|yb>$eF;WgrcOZf569sDZOVwoo%8>XO>XQOX1OyN9I-SQgrm;U;+#3OI(zrWyow3 zk==|{lt2xrQ%FIXOTejR>;wv(Pb8u8}BUpx?yd(Abh6? zsoO3VYWkeLnF43&@*#MQ9-i-d0t*xN-UEyNKeyNMHw|A(k(_6QKO=nKMCxD(W(Yop zsRQ)QeL4X3Lxp^L%wzi2-WVSsf61dqliPUM7srDB?Wm6Lzn0&{*}|IsKQW;02(Y&| zaTKv|`U(pSzuvR6Rduu$wzK_W-Y-7>7s?G$)U}&uK;<>vU}^^ns@Z!p+9?St1s)dG zK%y6xkPyyS1$~&6v{kl?Md6gwM|>mt6Upm>oa8RLD^8T{0?HC!Z>;(Bob7el(DV6x zi`I)$&E&ngwFS@bi4^xFLAn`=fzTC;aimE^!cMI2n@Vo%Ae-ne`RF((&5y6xsjjAZ zVguVoQ?Z9uk$2ON;ersE%PU*xGO@T*;j1BO5#TuZKEf(mB7|g7pcEA=nYJ{s3vlbg zd4-DUlD{*6o%Gc^N!Nptgay>j6E5;3psI+C3Q!1ZIbeCubW%w4pq9)MSDyB{HLm|k zxv-{$$A*pS@csolri$Ge<4VZ}e~78JOL-EVyrbxKra^d{?|NnPp86!q>t<&IP07?Z z^>~IK^k#OEKgRH+LjllZXk7iA>2cfH6+(e&9ku5poo~6y{GC5>(bRK7hwjiurqAiZ zg*DmtgY}v83IjE&AbiWgMyFbaRUPZ{lYiz$U^&Zt2YjG<%m((&_JUbZcfJ22(>bi5 z!J?<7AySj0JZ&<-qXX;mcV!f~>G=sB0KnjWca4}vrtunD^1TrpfeS^4dvFr!65knK zZh`d;*VOkPs4*-9kL>$GP0`(M!j~B;#x?Ba~&s6CopvO86oM?-? zOw#dIRc;6A6T?B`Qp%^<U5 z19x(ywSH$_N+Io!6;e?`tWaM$`=Db!gzx|lQ${DG!zb1Zl&|{kX0y6xvO1o z220r<-oaS^^R2pEyY;=Qllqpmue|5yI~D|iI!IGt@iod{Opz@*ml^w2bNs)p`M(Io z|E;;m*Xpjd9l)4G#KaWfV(t8YUn@A;nK^#xgv=LtnArX|vWQVuw3}B${h+frU2>9^ z!l6)!Uo4`5k`<<;E(ido7M6lKTgWezNLq>U*=uz&s=cc$1%>VrAeOoUtA|T6gO4>UNqsdK=NF*8|~*sl&wI=x9-EGiq*aqV!(VVXA57 zw9*o6Ir8Lj1npUXvlevtn(_+^X5rzdR>#(}4YcB9O50q97%rW2me5_L=%ffYPUSRc z!vv?Kv>dH994Qi>U(a<0KF6NH5b16enCp+mw^Hb3Xs1^tThFpz!3QuN#}KBbww`(h z7GO)1olDqy6?T$()R7y%NYx*B0k_2IBiZ14&8|JPFxeMF{vW>HF-Vi3+ZOI=+qP}n zw(+!WcTd~4ZJX1!ZM&y!+uyt=&i!+~d(V%GjH;-NsEEv6nS1TERt|RHh!0>W4+4pp z1-*EzAM~i`+1f(VEHI8So`S`akPfPTfq*`l{Fz`hS%k#JS0cjT2mS0#QLGf=J?1`he3W*;m4)ce8*WFq1sdP=~$5RlH1EdWm|~dCvKOi4*I_96{^95p#B<(n!d?B z=o`0{t+&OMwKcxiBECznJcfH!fL(z3OvmxP#oWd48|mMjpE||zdiTBdWelj8&Qosv zZFp@&UgXuvJw5y=q6*28AtxZzo-UUpkRW%ne+Ylf!V-0+uQXBW=5S1o#6LXNtY5!I z%Rkz#(S8Pjz*P7bqB6L|M#Er{|QLae-Y{KA>`^} z@lPjeX>90X|34S-7}ZVXe{wEei1<{*e8T-Nbj8JmD4iwcE+Hg_zhkPVm#=@b$;)h6 z<<6y`nPa`f3I6`!28d@kdM{uJOgM%`EvlQ5B2bL)Sl=|y@YB3KeOzz=9cUW3clPAU z^sYc}xf9{4Oj?L5MOlYxR{+>w=vJjvbyO5}ptT(o6dR|ygO$)nVCvNGnq(6;bHlBd zl?w-|plD8spjDF03g5ip;W3Z z><0{BCq!Dw;h5~#1BuQilq*TwEu)qy50@+BE4bX28+7erX{BD4H)N+7U`AVEuREE8 z;X?~fyhF-x_sRfHIj~6f(+^@H)D=ngP;mwJjxhQUbUdzk8f94Ab%59-eRIq?ZKrwD z(BFI=)xrUlgu(b|hAysqK<}8bslmNNeD=#JW*}^~Nrswn^xw*nL@Tx!49bfJecV&KC2G4q5a!NSv)06A_5N3Y?veAz;Gv+@U3R% z)~UA8-0LvVE{}8LVDOHzp~2twReqf}ODIyXMM6=W>kL|OHcx9P%+aJGYi_Om)b!xe zF40Vntn0+VP>o<$AtP&JANjXBn7$}C@{+@3I@cqlwR2MdwGhVPxlTIcRVu@Ho-wO` z_~Or~IMG)A_`6-p)KPS@cT9mu9RGA>dVh5wY$NM9-^c@N=hcNaw4ITjm;iWSP^ZX| z)_XpaI61<+La+U&&%2a z0za$)-wZP@mwSELo#3!PGTt$uy0C(nTT@9NX*r3Ctw6J~7A(m#8fE)0RBd`TdKfAT zCf@$MAxjP`O(u9s@c0Fd@|}UQ6qp)O5Q5DPCeE6mSIh|Rj{$cAVIWsA=xPKVKxdhg zLzPZ`3CS+KIO;T}0Ip!fAUaNU>++ZJZRk@I(h<)RsJUhZ&Ru9*!4Ptn;gX^~4E8W^TSR&~3BAZc#HquXn)OW|TJ`CTahk+{qe`5+ixON^zA9IFd8)kc%*!AiLu z>`SFoZ5bW-%7}xZ>gpJcx_hpF$2l+533{gW{a7ce^B9sIdmLrI0)4yivZ^(Vh@-1q zFT!NQK$Iz^xu%|EOK=n>ug;(7J4OnS$;yWmq>A;hsD_0oAbLYhW^1Vdt9>;(JIYjf zdb+&f&D4@4AS?!*XpH>8egQvSVX`36jMd>$+RgI|pEg))^djhGSo&#lhS~9%NuWfX zDDH;3T*GzRT@5=7ibO>N-6_XPBYxno@mD_3I#rDD?iADxX`! zh*v8^i*JEMzyN#bGEBz7;UYXki*Xr(9xXax(_1qVW=Ml)kSuvK$coq2A(5ZGhs_pF z$*w}FbN6+QDseuB9=fdp_MTs)nQf!2SlROQ!gBJBCXD&@-VurqHj0wm@LWX-TDmS= z71M__vAok|@!qgi#H&H%Vg-((ZfxPAL8AI{x|VV!9)ZE}_l>iWk8UPTGHs*?u7RfP z5MC&=c6X;XlUzrz5q?(!eO@~* zoh2I*%J7dF!!_!vXoSIn5o|wj1#_>K*&CIn{qSaRc&iFVxt*^20ngCL;QonIS>I5^ zMw8HXm>W0PGd*}Ko)f|~dDd%;Wu_RWI_d;&2g6R3S63Uzjd7dn%Svu-OKpx*o|N>F zZg=-~qLb~VRLpv`k zWSdfHh@?dp=s_X`{yxOlxE$4iuyS;Z-x!*E6eqmEm*j2bE@=ZI0YZ5%Yj29!5+J$4h{s($nakA`xgbO8w zi=*r}PWz#lTL_DSAu1?f%-2OjD}NHXp4pXOsCW;DS@BC3h-q4_l`<))8WgzkdXg3! zs1WMt32kS2E#L0p_|x+x**TFV=gn`m9BWlzF{b%6j-odf4{7a4y4Uaef@YaeuPhU8 zHBvRqN^;$Jizy+ z=zW{E5<>2gp$pH{M@S*!sJVQU)b*J5*bX4h>5VJve#Q6ga}cQ&iL#=(u+KroWrxa%8&~p{WEUF0il=db;-$=A;&9M{Rq`ouZ5m%BHT6%st%saGsD6)fQgLN}x@d3q>FC;=f%O3Cyg=Ke@Gh`XW za@RajqOE9UB6eE=zhG%|dYS)IW)&y&Id2n7r)6p_)vlRP7NJL(x4UbhlcFXWT8?K=%s7;z?Vjts?y2+r|uk8Wt(DM*73^W%pAkZa1Jd zNoE)8FvQA>Z`eR5Z@Ig6kS5?0h;`Y&OL2D&xnnAUzQz{YSdh0k zB3exx%A2TyI)M*EM6htrxSlep!Kk(P(VP`$p0G~f$smld6W1r_Z+o?=IB@^weq>5VYsYZZR@` z&XJFxd5{|KPZmVOSxc@^%71C@;z}}WhbF9p!%yLj3j%YOlPL5s>7I3vj25 z@xmf=*z%Wb4;Va6SDk9cv|r*lhZ`(y_*M@>q;wrn)oQx%B(2A$9(74>;$zmQ!4fN; z>XurIk-7@wZys<+7XL@0Fhe-f%*=(weaQEdR9Eh6>Kl-EcI({qoZqyzziGwpg-GM#251sK_ z=3|kitS!j%;fpc@oWn65SEL73^N&t>Ix37xgs= zYG%eQDJc|rqHFia0!_sm7`@lvcv)gfy(+KXA@E{3t1DaZ$DijWAcA)E0@X?2ziJ{v z&KOYZ|DdkM{}t+@{@*6ge}m%xfjIxi%qh`=^2Rwz@w0cCvZ&Tc#UmCDbVwABrON^x zEBK43FO@weA8s7zggCOWhMvGGE`baZ62cC)VHyy!5Zbt%ieH+XN|OLbAFPZWyC6)p z4P3%8sq9HdS3=ih^0OOlqTPbKuzQ?lBEI{w^ReUO{V?@`ARsL|S*%yOS=Z%sF)>-y z(LAQdhgAcuF6LQjRYfdbD1g4o%tV4EiK&ElLB&^VZHbrV1K>tHTO{#XTo>)2UMm`2 z^t4s;vnMQgf-njU-RVBRw0P0-m#d-u`(kq7NL&2T)TjI_@iKuPAK-@oH(J8?%(e!0Ir$yG32@CGUPn5w4)+9@8c&pGx z+K3GKESI4*`tYlmMHt@br;jBWTei&(a=iYslc^c#RU3Q&sYp zSG){)V<(g7+8W!Wxeb5zJb4XE{I|&Y4UrFWr%LHkdQ;~XU zgy^dH-Z3lmY+0G~?DrC_S4@=>0oM8Isw%g(id10gWkoz2Q%7W$bFk@mIzTCcIB(K8 zc<5h&ZzCdT=9n-D>&a8vl+=ZF*`uTvQviG_bLde*k>{^)&0o*b05x$MO3gVLUx`xZ z43j+>!u?XV)Yp@MmG%Y`+COH2?nQcMrQ%k~6#O%PeD_WvFO~Kct za4XoCM_X!c5vhRkIdV=xUB3xI2NNStK*8_Zl!cFjOvp-AY=D;5{uXj}GV{LK1~IE2 z|KffUiBaStRr;10R~K2VVtf{TzM7FaPm;Y(zQjILn+tIPSrJh&EMf6evaBKIvi42-WYU9Vhj~3< zZSM-B;E`g_o8_XTM9IzEL=9Lb^SPhe(f(-`Yh=X6O7+6ALXnTcUFpI>ekl6v)ZQeNCg2 z^H|{SKXHU*%nBQ@I3It0m^h+6tvI@FS=MYS$ZpBaG7j#V@P2ZuYySbp@hA# ze(kc;P4i_-_UDP?%<6>%tTRih6VBgScKU^BV6Aoeg6Uh(W^#J^V$Xo^4#Ekp ztqQVK^g9gKMTHvV7nb64UU7p~!B?>Y0oFH5T7#BSW#YfSB@5PtE~#SCCg3p^o=NkMk$<8- z6PT*yIKGrvne7+y3}_!AC8NNeI?iTY(&nakN>>U-zT0wzZf-RuyZk^X9H-DT_*wk= z;&0}6LsGtfVa1q)CEUPlx#(ED@-?H<1_FrHU#z5^P3lEB|qsxEyn%FOpjx z3S?~gvoXy~L(Q{Jh6*i~=f%9kM1>RGjBzQh_SaIDfSU_9!<>*Pm>l)cJD@wlyxpBV z4Fmhc2q=R_wHCEK69<*wG%}mgD1=FHi4h!98B-*vMu4ZGW~%IrYSLGU{^TuseqVgV zLP<%wirIL`VLyJv9XG_p8w@Q4HzNt-o;U@Au{7%Ji;53!7V8Rv0^Lu^Vf*sL>R(;c zQG_ZuFl)Mh-xEIkGu}?_(HwkB2jS;HdPLSxVU&Jxy9*XRG~^HY(f0g8Q}iqnVmgjI zfd=``2&8GsycjR?M%(zMjn;tn9agcq;&rR!Hp z$B*gzHsQ~aXw8c|a(L^LW(|`yGc!qOnV(ZjU_Q-4z1&0;jG&vAKuNG=F|H?@m5^N@ zq{E!1n;)kNTJ>|Hb2ODt-7U~-MOIFo%9I)_@7fnX+eMMNh>)V$IXesJpBn|uo8f~#aOFytCT zf9&%MCLf8mp4kwHTcojWmM3LU=#|{3L>E}SKwOd?%{HogCZ_Z1BSA}P#O(%H$;z7XyJ^sjGX;j5 zrzp>|Ud;*&VAU3x#f{CKwY7Vc{%TKKqmB@oTHA9;>?!nvMA;8+Jh=cambHz#J18x~ zs!dF>$*AnsQ{{82r5Aw&^7eRCdvcgyxH?*DV5(I$qXh^zS>us*I66_MbL8y4d3ULj z{S(ipo+T3Ag!+5`NU2sc+@*m{_X|&p#O-SAqF&g_n7ObB82~$p%fXA5GLHMC+#qqL zdt`sJC&6C2)=juQ_!NeD>U8lDVpAOkW*khf7MCcs$A(wiIl#B9HM%~GtQ^}yBPjT@ z+E=|A!Z?A(rwzZ;T}o6pOVqHzTr*i;Wrc%&36kc@jXq~+w8kVrs;%=IFdACoLAcCAmhFNpbP8;s`zG|HC2Gv?I~w4ITy=g$`0qMQdkijLSOtX6xW%Z9Nw<;M- zMN`c7=$QxN00DiSjbVt9Mi6-pjv*j(_8PyV-il8Q-&TwBwH1gz1uoxs6~uU}PrgWB zIAE_I-a1EqlIaGQNbcp@iI8W1sm9fBBNOk(k&iLBe%MCo#?xI$%ZmGA?=)M9D=0t7 zc)Q0LnI)kCy{`jCGy9lYX%mUsDWwsY`;jE(;Us@gmWPqjmXL+Hu#^;k%eT>{nMtzj zsV`Iy6leTA8-PndszF;N^X@CJrTw5IIm!GPeu)H2#FQitR{1p;MasQVAG3*+=9FYK zw*k!HT(YQorfQj+1*mCV458(T5=fH`um$gS38hw(OqVMyunQ;rW5aPbF##A3fGH6h z@W)i9Uff?qz`YbK4c}JzQpuxuE3pcQO)%xBRZp{zJ^-*|oryTxJ-rR+MXJ)!f=+pp z10H|DdGd2exhi+hftcYbM0_}C0ZI-2vh+$fU1acsB-YXid7O|=9L!3e@$H*6?G*Zp z%qFB(sgl=FcC=E4CYGp4CN>=M8#5r!RU!u+FJVlH6=gI5xHVD&k;Ta*M28BsxfMV~ zLz+@6TxnfLhF@5=yQo^1&S}cmTN@m!7*c6z;}~*!hNBjuE>NLVl2EwN!F+)0$R1S! zR|lF%n!9fkZ@gPW|x|B={V6x3`=jS*$Pu0+5OWf?wnIy>Y1MbbGSncpKO0qE(qO=ts z!~@&!N`10S593pVQu4FzpOh!tvg}p%zCU(aV5=~K#bKi zHdJ1>tQSrhW%KOky;iW+O_n;`l9~omqM%sdxdLtI`TrJzN6BQz+7xOl*rM>xVI2~# z)7FJ^Dc{DC<%~VS?@WXzuOG$YPLC;>#vUJ^MmtbSL`_yXtNKa$Hk+l-c!aC7gn(Cg ze?YPYZ(2Jw{SF6MiO5(%_pTo7j@&DHNW`|lD`~{iH+_eSTS&OC*2WTT*a`?|9w1dh zh1nh@$a}T#WE5$7Od~NvSEU)T(W$p$s5fe^GpG+7fdJ9=enRT9$wEk+ZaB>G3$KQO zgq?-rZZnIv!p#>Ty~}c*Lb_jxJg$eGM*XwHUwuQ|o^}b3^T6Bxx{!?va8aC@-xK*H ztJBFvFfsSWu89%@b^l3-B~O!CXs)I6Y}y#0C0U0R0WG zybjroj$io0j}3%P7zADXOwHwafT#uu*zfM!oD$6aJx7+WL%t-@6^rD_a_M?S^>c;z zMK580bZXo1f*L$CuMeM4Mp!;P@}b~$cd(s5*q~FP+NHSq;nw3fbWyH)i2)-;gQl{S zZO!T}A}fC}vUdskGSq&{`oxt~0i?0xhr6I47_tBc`fqaSrMOzR4>0H^;A zF)hX1nfHs)%Zb-(YGX;=#2R6C{BG;k=?FfP?9{_uFLri~-~AJ;jw({4MU7e*d)?P@ zXX*GkNY9ItFjhwgAIWq7Y!ksbMzfqpG)IrqKx9q{zu%Mdl+{Dis#p9q`02pr1LG8R z@As?eG!>IoROgS!@J*to<27coFc1zpkh?w=)h9CbYe%^Q!Ui46Y*HO0mr% zEff-*$ndMNw}H2a5@BsGj5oFfd!T(F&0$<{GO!Qdd?McKkorh=5{EIjDTHU`So>8V zBA-fqVLb2;u7UhDV1xMI?y>fe3~4urv3%PX)lDw+HYa;HFkaLqi4c~VtCm&Ca+9C~ zge+67hp#R9`+Euq59WhHX&7~RlXn=--m8$iZ~~1C8cv^2(qO#X0?vl91gzUKBeR1J z^p4!!&7)3#@@X&2aF2-)1Ffcc^F8r|RtdL2X%HgN&XU-KH2SLCbpw?J5xJ*!F-ypZ zMG%AJ!Pr&}`LW?E!K~=(NJxuSVTRCGJ$2a*Ao=uUDSys!OFYu!Vs2IT;xQ6EubLIl z+?+nMGeQQhh~??0!s4iQ#gm3!BpMpnY?04kK375e((Uc7B3RMj;wE?BCoQGu=UlZt!EZ1Q*auI)dj3Jj{Ujgt zW5hd~-HWBLI_3HuO) zNrb^XzPsTIb=*a69wAAA3J6AAZZ1VsYbIG}a`=d6?PjM)3EPaDpW2YP$|GrBX{q*! z$KBHNif)OKMBCFP5>!1d=DK>8u+Upm-{hj5o|Wn$vh1&K!lVfDB&47lw$tJ?d5|=B z^(_9=(1T3Fte)z^>|3**n}mIX;mMN5v2F#l(q*CvU{Ga`@VMp#%rQkDBy7kYbmb-q z<5!4iuB#Q_lLZ8}h|hPODI^U6`gzLJre9u3k3c#%86IKI*^H-@I48Bi*@avYm4v!n0+v zWu{M{&F8#p9cx+gF0yTB_<2QUrjMPo9*7^-uP#~gGW~y3nfPAoV%amgr>PSyVAd@l)}8#X zR5zV6t*uKJZL}?NYvPVK6J0v4iVpwiN|>+t3aYiZSp;m0!(1`bHO}TEtWR1tY%BPB z(W!0DmXbZAsT$iC13p4f>u*ZAy@JoLAkJhzFf1#4;#1deO8#8d&89}en&z!W&A3++^1(;>0SB1*54d@y&9Pn;^IAf3GiXbfT`_>{R+Xv; zQvgL>+0#8-laO!j#-WB~(I>l0NCMt_;@Gp_f0#^c)t?&#Xh1-7RR0@zPyBz!U#0Av zT?}n({(p?p7!4S2ZBw)#KdCG)uPnZe+U|0{BW!m)9 zi_9$F?m<`2!`JNFv+w8MK_K)qJ^aO@7-Ig>cM4-r0bi=>?B_2mFNJ}aE3<+QCzRr*NA!QjHw# z`1OsvcoD0?%jq{*7b!l|L1+Tw0TTAM4XMq7*ntc-Ived>Sj_ZtS|uVdpfg1_I9knY z2{GM_j5sDC7(W&}#s{jqbybqJWyn?{PW*&cQIU|*v8YGOKKlGl@?c#TCnmnAkAzV- zmK={|1G90zz=YUvC}+fMqts0d4vgA%t6Jhjv?d;(Z}(Ep8fTZfHA9``fdUHkA+z3+ zhh{ohP%Bj?T~{i0sYCQ}uC#5BwN`skI7`|c%kqkyWIQ;!ysvA8H`b-t()n6>GJj6xlYDu~8qX{AFo$Cm3d|XFL=4uvc?Keb zzb0ZmMoXca6Mob>JqkNuoP>B2Z>D`Q(TvrG6m`j}-1rGP!g|qoL=$FVQYxJQjFn33lODt3Wb1j8VR zlR++vIT6^DtYxAv_hxupbLLN3e0%A%a+hWTKDV3!Fjr^cWJ{scsAdfhpI)`Bms^M6 zQG$waKgFr=c|p9Piug=fcJvZ1ThMnNhQvBAg-8~b1?6wL*WyqXhtj^g(Ke}mEfZVM zJuLNTUVh#WsE*a6uqiz`b#9ZYg3+2%=C(6AvZGc=u&<6??!slB1a9K)=VL zY9EL^mfyKnD zSJyYBc_>G;5RRnrNgzJz#Rkn3S1`mZgO`(r5;Hw6MveN(URf_XS-r58Cn80K)ArH4 z#Rrd~LG1W&@ttw85cjp8xV&>$b%nSXH_*W}7Ch2pg$$c0BdEo-HWRTZcxngIBJad> z;C>b{jIXjb_9Jis?NZJsdm^EG}e*pR&DAy0EaSGi3XWTa(>C%tz1n$u?5Fb z1qtl?;_yjYo)(gB^iQq?=jusF%kywm?CJP~zEHi0NbZ);$(H$w(Hy@{i>$wcVRD_X|w-~(0Z9BJyh zhNh;+eQ9BEIs;tPz%jSVnfCP!3L&9YtEP;svoj_bNzeGSQIAjd zBss@A;)R^WAu-37RQrM%{DfBNRx>v!G31Z}8-El9IOJlb_MSoMu2}GDYycNaf>uny z+8xykD-7ONCM!APry_Lw6-yT>5!tR}W;W`C)1>pxSs5o1z#j7%m=&=7O4hz+Lsqm` z*>{+xsabZPr&X=}G@obTb{nPTkccJX8w3CG7X+1+t{JcMabv~UNv+G?txRqXib~c^Mo}`q{$`;EBNJ;#F*{gvS12kV?AZ%O0SFB$^ zn+}!HbmEj}w{Vq(G)OGAzH}R~kS^;(-s&=ectz8vN!_)Yl$$U@HNTI-pV`LSj7Opu zTZ5zZ)-S_{GcEQPIQXLQ#oMS`HPu{`SQiAZ)m1at*Hy%3xma|>o`h%E%8BEbi9p0r zVjcsh<{NBKQ4eKlXU|}@XJ#@uQw*$4BxKn6#W~I4T<^f99~(=}a`&3(ur8R9t+|AQ zWkQx7l}wa48-jO@ft2h+7qn%SJtL%~890FG0s5g*kNbL3I&@brh&f6)TlM`K^(bhr zJWM6N6x3flOw$@|C@kPi7yP&SP?bzP-E|HSXQXG>7gk|R9BTj`e=4de9C6+H7H7n# z#GJeVs1mtHhLDmVO?LkYRQc`DVOJ_vdl8VUihO-j#t=0T3%Fc1f9F73ufJz*adn*p zc%&vi(4NqHu^R>sAT_0EDjVR8bc%wTz#$;%NU-kbDyL_dg0%TFafZwZ?5KZpcuaO54Z9hX zD$u>q!-9`U6-D`E#`W~fIfiIF5_m6{fvM)b1NG3xf4Auw;Go~Fu7cth#DlUn{@~yu z=B;RT*dp?bO}o%4x7k9v{r=Y@^YQ^UUm(Qmliw8brO^=NP+UOohLYiaEB3^DB56&V zK?4jV61B|1Uj_5fBKW;8LdwOFZKWp)g{B%7g1~DgO&N& z#lisxf?R~Z@?3E$Mms$$JK8oe@X`5m98V*aV6Ua}8Xs2#A!{x?IP|N(%nxsH?^c{& z@vY&R1QmQs83BW28qAmJfS7MYi=h(YK??@EhjL-t*5W!p z^gYX!Q6-vBqcv~ruw@oMaU&qp0Fb(dbVzm5xJN%0o_^@fWq$oa3X?9s%+b)x4w-q5Koe(@j6Ez7V@~NRFvd zfBH~)U5!ix3isg`6be__wBJp=1@yfsCMw1C@y+9WYD9_C%{Q~7^0AF2KFryfLlUP# zwrtJEcH)jm48!6tUcxiurAMaiD04C&tPe6DI0#aoqz#Bt0_7_*X*TsF7u*zv(iEfA z;$@?XVu~oX#1YXtceQL{dSneL&*nDug^OW$DSLF0M1Im|sSX8R26&)<0Fbh^*l6!5wfSu8MpMoh=2l z^^0Sr$UpZp*9oqa23fcCfm7`ya2<4wzJ`Axt7e4jJrRFVf?nY~2&tRL* zd;6_njcz01c>$IvN=?K}9ie%Z(BO@JG2J}fT#BJQ+f5LFSgup7i!xWRKw6)iITjZU z%l6hPZia>R!`aZjwCp}I zg)%20;}f+&@t;(%5;RHL>K_&7MH^S+7<|(SZH!u zznW|jz$uA`P9@ZWtJgv$EFp>)K&Gt+4C6#*khZQXS*S~6N%JDT$r`aJDs9|uXWdbg zBwho$phWx}x!qy8&}6y5Vr$G{yGSE*r$^r{}pw zVTZKvikRZ`J_IJrjc=X1uw?estdwm&bEahku&D04HD+0Bm~q#YGS6gp!KLf$A{%Qd z&&yX@Hp>~(wU{|(#U&Bf92+1i&Q*-S+=y=3pSZy$#8Uc$#7oiJUuO{cE6=tsPhwPe| zxQpK>`Dbka`V)$}e6_OXKLB%i76~4N*zA?X+PrhH<&)}prET;kel24kW%+9))G^JI zsq7L{P}^#QsZViX%KgxBvEugr>ZmFqe^oAg?{EI=&_O#e)F3V#rc z8$4}0Zr19qd3tE4#$3_f=Bbx9oV6VO!d3(R===i-7p=Vj`520w0D3W6lQfY48}!D* z&)lZMG;~er2qBoI2gsX+Ts-hnpS~NYRDtPd^FPzn!^&yxRy#CSz(b&E*tL|jIkq|l zf%>)7Dtu>jCf`-7R#*GhGn4FkYf;B$+9IxmqH|lf6$4irg{0ept__%)V*R_OK=T06 zyT_m-o@Kp6U{l5h>W1hGq*X#8*y@<;vsOFqEjTQXFEotR+{3}ODDnj;o0@!bB5x=N z394FojuGOtVKBlVRLtHp%EJv_G5q=AgF)SKyRN5=cGBjDWv4LDn$IL`*=~J7u&Dy5 zrMc83y+w^F&{?X(KOOAl-sWZDb{9X9#jrQtmrEXD?;h-}SYT7yM(X_6qksM=K_a;Z z3u0qT0TtaNvDER_8x*rxXw&C^|h{P1qxK|@pS7vdlZ#P z7PdB7MmC2}%sdzAxt>;WM1s0??`1983O4nFK|hVAbHcZ3x{PzytQLkCVk7hA!Lo` zEJH?4qw|}WH{dc4z%aB=0XqsFW?^p=X}4xnCJXK%c#ItOSjdSO`UXJyuc8bh^Cf}8 z@Ht|vXd^6{Fgai8*tmyRGmD_s_nv~r^Fy7j`Bu`6=G)5H$i7Q7lvQnmea&TGvJp9a|qOrUymZ$6G|Ly z#zOCg++$3iB$!6!>215A4!iryregKuUT344X)jQb3|9qY>c0LO{6Vby05n~VFzd?q zgGZv&FGlkiH*`fTurp>B8v&nSxNz)=5IF$=@rgND4d`!AaaX;_lK~)-U8la_Wa8i?NJC@BURO*sUW)E9oyv3RG^YGfN%BmxzjlT)bp*$<| zX3tt?EAy<&K+bhIuMs-g#=d1}N_?isY)6Ay$mDOKRh z4v1asEGWoAp=srraLW^h&_Uw|6O+r;wns=uwYm=JN4Q!quD8SQRSeEcGh|Eb5Jg8m zOT}u;N|x@aq)=&;wufCc^#)5U^VcZw;d_wwaoh9$p@Xrc{DD6GZUqZ ziC6OT^zSq@-lhbgR8B+e;7_Giv;DK5gn^$bs<6~SUadiosfewWDJu`XsBfOd1|p=q zE>m=zF}!lObA%ePey~gqU8S6h-^J2Y?>7)L2+%8kV}Gp=h`Xm_}rlm)SyUS=`=S7msKu zC|T!gPiI1rWGb1z$Md?0YJQ;%>uPLOXf1Z>N~`~JHJ!^@D5kSXQ4ugnFZ>^`zH8CAiZmp z6Ms|#2gcGsQ{{u7+Nb9sA?U>(0e$5V1|WVwY`Kn)rsnnZ4=1u=7u!4WexZD^IQ1Jk zfF#NLe>W$3m&C^ULjdw+5|)-BSHwpegdyt9NYC{3@QtMfd8GrIWDu`gd0nv-3LpGCh@wgBaG z176tikL!_NXM+Bv#7q^cyn9$XSeZR6#!B4JE@GVH zoobHZN_*RF#@_SVYKkQ_igme-Y5U}cV(hkR#k1c{bQNMji zU7aE`?dHyx=1`kOYZo_8U7?3-7vHOp`Qe%Z*i+FX!s?6huNp0iCEW-Z7E&jRWmUW_ z67j>)Ew!yq)hhG4o?^z}HWH-e=es#xJUhDRc4B51M4~E-l5VZ!&zQq`gWe`?}#b~7w1LH4Xa-UCT5LXkXQWheBa2YJYbyQ zl1pXR%b(KCXMO0OsXgl0P0Og<{(@&z1aokU-Pq`eQq*JYgt8xdFQ6S z6Z3IFSua8W&M#`~*L#r>Jfd6*BzJ?JFdBR#bDv$_0N!_5vnmo@!>vULcDm`MFU823 zpG9pqjqz^FE5zMDoGqhs5OMmC{Y3iVcl>F}5Rs24Y5B^mYQ;1T&ks@pIApHOdrzXF z-SdX}Hf{X;TaSxG_T$0~#RhqKISGKNK47}0*x&nRIPtmdwxc&QT3$8&!3fWu1eZ_P zJveQj^hJL#Sn!*4k`3}(d(aasl&7G0j0-*_2xtAnoX1@9+h zO#c>YQg60Z;o{Bi=3i7S`Ic+ZE>K{(u|#)9y}q*j8uKQ1^>+(BI}m%1v3$=4ojGBc zm+o1*!T&b}-lVvZqIUBc8V}QyFEgm#oyIuC{8WqUNV{Toz`oxhYpP!_p2oHHh5P@iB*NVo~2=GQm+8Yrkm2Xjc_VyHg1c0>+o~@>*Qzo zHVBJS>$$}$_4EniTI;b1WShX<5-p#TPB&!;lP!lBVBbLOOxh6FuYloD%m;n{r|;MU3!q4AVkua~fieeWu2 zQAQ$ue(IklX6+V;F1vCu-&V?I3d42FgWgsb_e^29ol}HYft?{SLf>DrmOp9o!t>I^ zY7fBCk+E8n_|apgM|-;^=#B?6RnFKlN`oR)`e$+;D=yO-(U^jV;rft^G_zl`n7qnM zL z*-Y4Phq+ZI1$j$F-f;`CD#|`-T~OM5Q>x}a>B~Gb3-+9i>Lfr|Ca6S^8g*{*?_5!x zH_N!SoRP=gX1?)q%>QTY!r77e2j9W(I!uAz{T`NdNmPBBUzi2{`XMB^zJGGwFWeA9 z{fk33#*9SO0)DjROug+(M)I-pKA!CX;IY(#gE!UxXVsa)X!UftIN98{pt#4MJHOhY zM$_l}-TJlxY?LS6Nuz1T<44m<4i^8k@D$zuCPrkmz@sdv+{ciyFJG2Zwy&%c7;atIeTdh!a(R^QXnu1Oq1b42*OQFWnyQ zWeQrdvP|w_idy53Wa<{QH^lFmEd+VlJkyiC>6B#s)F;w-{c;aKIm;Kp50HnA-o3lY z9B~F$gJ@yYE#g#X&3ADx&tO+P_@mnQTz9gv30_sTsaGXkfNYXY{$(>*PEN3QL>I!k zp)KibPhrfX3%Z$H6SY`rXGYS~143wZrG2;=FLj50+VM6soI~up_>fU(2Wl@{BRsMi zO%sL3x?2l1cXTF)k&moNsHfQrQ+wu(gBt{sk#CU=UhrvJIncy@tJX5klLjgMn>~h= zg|FR&;@eh|C7`>s_9c~0-{IAPV){l|Ts`i=)AW;d9&KPc3fMeoTS%8@V~D8*h;&(^>yjT84MM}=%#LS7shLAuuj(0VAYoozhWjq z4LEr?wUe2^WGwdTIgWBkDUJa>YP@5d9^Rs$kCXmMRxuF*YMVrn?0NFyPl}>`&dqZb z<5eqR=ZG3>n2{6v6BvJ`YBZeeTtB88TAY(x0a58EWyuf>+^|x8Qa6wA|1Nb_p|nA zWWa}|z8a)--Wj`LqyFk_a3gN2>5{Rl_wbW?#by7&i*^hRknK%jwIH6=dQ8*-_{*x0j^DUfMX0`|K@6C<|1cgZ~D(e5vBFFm;HTZF(!vT8=T$K+|F)x3kqzBV4-=p1V(lzi(s7jdu0>LD#N=$Lk#3HkG!a zIF<7>%B7sRNzJ66KrFV76J<2bdYhxll0y2^_rdG=I%AgW4~)1Nvz=$1UkE^J%BxLo z+lUci`UcU062os*=`-j4IfSQA{w@y|3}Vk?i;&SSdh8n+$iHA#%ERL{;EpXl6u&8@ zzg}?hkEOUOJt?ZL=pWZFJ19mI1@P=$U5*Im1e_8Z${JsM>Ov?nh8Z zP5QvI!{Jy@&BP48%P2{Jr_VgzW;P@7)M9n|lDT|Ep#}7C$&ud&6>C^5ZiwKIg2McPU(4jhM!BD@@L(Gd*Nu$ji(ljZ<{FIeW_1Mmf;76{LU z-ywN~=uNN)Xi6$<12A9y)K%X|(W0p|&>>4OXB?IiYr||WKDOJPxiSe01NSV-h24^L z_>m$;|C+q!Mj**-qQ$L-*++en(g|hw;M!^%_h-iDjFHLo-n3JpB;p?+o2;`*jpvJU zLY^lt)Un4joij^^)O(CKs@7E%*!w>!HA4Q?0}oBJ7Nr8NQ7QmY^4~jvf0-`%waOLn zdNjAPaC0_7c|RVhw)+71NWjRi!y>C+Bl;Z`NiL^zn2*0kmj5gyhCLCxts*cWCdRI| zjsd=sT5BVJc^$GxP~YF$-U{-?kW6r@^vHXB%{CqYzU@1>dzf#3SYedJG-Rm6^RB7s zGM5PR(yKPKR)>?~vpUIeTP7A1sc8-knnJk*9)3t^e%izbdm>Y=W{$wm(cy1RB-19i za#828DMBY+ps#7Y8^6t)=Ea@%Nkt)O6JCx|ybC;Ap}Z@Zw~*}3P>MZLPb4Enxz9Wf zssobT^(R@KuShj8>@!1M7tm|2%-pYYDxz-5`rCbaTCG5{;Uxm z*g=+H1X8{NUvFGzz~wXa%Eo};I;~`37*WrRU&K0dPSB$yk(Z*@K&+mFal^?c zurbqB-+|Kb5|sznT;?Pj!+kgFY1#Dr;_%A(GIQC{3ct|{*Bji%FNa6c-thbpBkA;U zURV!Dr&X{0J}iht#-Qp2=xzuh(fM>zRoiGrYl5ttw2#r34gC41CCOC31m~^UPTK@s z6;A@)7O7_%C)>bnAXerYuAHdE93>j2N}H${zEc6&SbZ|-fiG*-qtGuy-qDelH(|u$ zorf8_T6Zqe#Ub!+e3oSyrskt_HyW_^5lrWt#30l)tHk|j$@YyEkXUOV;6B51L;M@=NIWZXU;GrAa(LGxO%|im%7F<-6N;en0Cr zLH>l*y?pMwt`1*cH~LdBPFY_l;~`N!Clyfr;7w<^X;&(ZiVdF1S5e(+Q%60zgh)s4 zn2yj$+mE=miVERP(g8}G4<85^-5f@qxh2ec?n+$A_`?qN=iyT1?U@t?V6DM~BIlBB z>u~eXm-aE>R0sQy!-I4xtCNi!!qh?R1!kKf6BoH2GG{L4%PAz0{Sh6xpuyI%*~u)s z%rLuFl)uQUCBQAtMyN;%)zFMx4loh7uTfKeB2Xif`lN?2gq6NhWhfz0u5WP9J>=V2 zo{mLtSy&BA!mSzs&CrKWq^y40JF5a&GSXIi2= z{EYb59J4}VwikL4P=>+mc6{($FNE@e=VUwG+KV21;<@lrN`mnz5jYGASyvz7BOG_6(p^eTxD-4O#lROgon;R35=|nj#eHIfJBYPWG>H>`dHKCDZ3`R{-?HO0mE~(5_WYcFmp8sU?wr*UkAQiNDGc6T zA%}GOLXlOWqL?WwfHO8MB#8M8*~Y*gz;1rWWoVSXP&IbKxbQ8+s%4Jnt?kDsq7btI zCDr0PZ)b;B%!lu&CT#RJzm{l{2fq|BcY85`w~3LSK<><@(2EdzFLt9Y_`;WXL6x`0 zDoQ?=?I@Hbr;*VVll1Gmd8*%tiXggMK81a+T(5Gx6;eNb8=uYn z5BG-0g>pP21NPn>$ntBh>`*})Fl|38oC^9Qz>~MAazH%3Q~Qb!ALMf$srexgPZ2@&c~+hxRi1;}+)-06)!#Mq<6GhP z-Q?qmgo${aFBApb5p}$1OJKTClfi8%PpnczyVKkoHw7Ml9e7ikrF0d~UB}i3vizos zXW4DN$SiEV9{faLt5bHy2a>33K%7Td-n5C*N;f&ZqAg#2hIqEb(y<&f4u5BWJ>2^4 z414GosL=Aom#m&=x_v<0-fp1r%oVJ{T-(xnomNJ(Dryv zh?vj+%=II_nV+@NR+(!fZZVM&(W6{6%9cm+o+Z6}KqzLw{(>E86uA1`_K$HqINlb1 zKelh3-jr2I9V?ych`{hta9wQ2c9=MM`2cC{m6^MhlL2{DLv7C^j z$xXBCnDl_;l|bPGMX@*tV)B!c|4oZyftUlP*?$YU9C_eAsuVHJ58?)zpbr30P*C`T z7y#ao`uE-SOG(Pi+`$=e^mle~)pRrdwL5)N;o{gpW21of(QE#U6w%*C~`v-z0QqBML!!5EeYA5IQB0 z^l01c;L6E(iytN!LhL}wfwP7W9PNAkb+)Cst?qg#$n;z41O4&v+8-zPs+XNb-q zIeeBCh#ivnFLUCwfS;p{LC0O7tm+Sf9Jn)~b%uwP{%69;QC)Ok0t%*a5M+=;y8j=v z#!*pp$9@!x;UMIs4~hP#pnfVc!%-D<+wsG@R2+J&%73lK|2G!EQC)O05TCV=&3g)C!lT=czLpZ@Sa%TYuoE?v8T8`V;e$#Zf2_Nj6nvBgh1)2 GZ~q4|mN%#X literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..c1d5e01 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..6f8794d --- /dev/null +++ b/gradlew @@ -0,0 +1,253 @@ +#!/bin/sh +if [ ! -d "$JAVA_HOME" ]; then export JAVA_HOME=/home/linuxbrew/.linuxbrew/opt/openjdk@21; fi + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 0000000..cb62cca --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,6 @@ +jdk: + - openjdk21 +before_install: + - ./gradlew -v +install: + - ./gradlew :pwnapple:publishToMavenLocal diff --git a/pwnapple/build.gradle.kts b/pwnapple/build.gradle.kts new file mode 100644 index 0000000..31d23db --- /dev/null +++ b/pwnapple/build.gradle.kts @@ -0,0 +1,98 @@ +plugins { + alias(libs.plugins.androidLibrary) + alias(libs.plugins.jetbrainsKotlinAndroid) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.maven.publish) +} + +android { + namespace = "ai.heretek.pwnapple" + compileSdk = 35 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + kotlinOptions { + jvmTarget = "21" + } + + buildFeatures { + compose = true + } + + publishing { + singleVariant("release") { + withSourcesJar() + withJavadocJar() + } + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material.icons.core) + implementation(libs.androidx.compose.material.icons.extended) + + testImplementation(libs.junit) +} + +afterEvaluate { + publishing { + publications { + create("release") { + from(components["release"]) + groupId = "ai.heretek.pwnapple" + artifactId = "pwnapple" + version = "1.0.0" + + pom { + name.set("Pwnapple") + description.set("Authentic iOS 6 skeuomorphic UI library for Android built with Jetpack Compose") + url.set("https://github.com/Heretek-AI/pwnapple") + licenses { + license { + name.set("The MIT License") + url.set("https://opensource.org/licenses/MIT") + } + } + developers { + developer { + id.set("heretek-ai") + name.set("Heretek AI") + email.set("dev@heretek.ai") + } + } + scm { + connection.set("scm:git:git://github.com/Heretek-AI/pwnapple.git") + developerConnection.set("scm:git:ssh://github.com/Heretek-AI/pwnapple.git") + url.set("https://github.com/Heretek-AI/pwnapple") + } + } + } + } + } +} diff --git a/pwnapple/consumer-rules.pro b/pwnapple/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/pwnapple/proguard-rules.pro b/pwnapple/proguard-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/pwnapple/src/main/AndroidManifest.xml b/pwnapple/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9a40236 --- /dev/null +++ b/pwnapple/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/audio/SoundPlayer.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/audio/SoundPlayer.kt new file mode 100644 index 0000000..3e4cd26 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/audio/SoundPlayer.kt @@ -0,0 +1,129 @@ +package ai.heretek.pwnapple.audio + +import android.media.AudioAttributes +import android.media.AudioFormat +import android.media.AudioTrack +import java.util.concurrent.Executors +import kotlin.math.PI +import kotlin.math.exp +import kotlin.math.sin + +/** + * Synthesizes and plays low-latency, authentic skeuomorphic tactile audio cues + * directly via PCM AudioTrack without needing external audio asset files. + */ +object SoundPlayer { + private const val SAMPLE_RATE = 44100 + private val executor = Executors.newSingleThreadExecutor() + + private val clickBuffer: ShortArray by lazy { generateButtonClick() } + private val tickBuffer: ShortArray by lazy { generateWheelTick() } + private val latchBuffer: ShortArray by lazy { generateSwitchLatch() } + + fun playButtonClick() { + playSample(clickBuffer) + } + + fun playWheelTick() { + playSample(tickBuffer) + } + + fun playSwitchLatch() { + playSample(latchBuffer) + } + + private fun playSample(buffer: ShortArray) { + executor.execute { + try { + val track = AudioTrack.Builder() + .setAudioAttributes( + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build() + ) + .setAudioFormat( + AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setSampleRate(SAMPLE_RATE) + .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) + .build() + ) + .setBufferSizeInBytes(buffer.size * 2) + .setTransferMode(AudioTrack.MODE_STATIC) + .build() + + track.write(buffer, 0, buffer.size) + track.play() + // Sleep until playback completes then release track + Thread.sleep((buffer.size * 1000L / SAMPLE_RATE) + 10) + track.stop() + track.release() + } catch (_: Throwable) { + // Silently ignore if audio subsystem is unavailable + } + } + } + + /** + * Crisp 22ms mechanical tactile click with fast attack and exponential decay. + */ + private fun generateButtonClick(): ShortArray { + val durationMs = 22 + val numSamples = (SAMPLE_RATE * durationMs) / 1000 + val buffer = ShortArray(numSamples) + val f1 = 1750.0 + val f2 = 820.0 + + for (i in 0 until numSamples) { + val t = i.toDouble() / SAMPLE_RATE + val env = exp(-t * 220.0) + val wave = 0.7 * sin(2.0 * PI * f1 * t) + 0.3 * sin(2.0 * PI * f2 * t) + buffer[i] = (wave * env * 24000.0).toInt().coerceIn(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()).toShort() + } + return buffer + } + + /** + * 10ms high-pitch mechanical ratchet tick for picker drum wheels. + */ + private fun generateWheelTick(): ShortArray { + val durationMs = 12 + val numSamples = (SAMPLE_RATE * durationMs) / 1000 + val buffer = ShortArray(numSamples) + val freq = 3100.0 + + for (i in 0 until numSamples) { + val t = i.toDouble() / SAMPLE_RATE + val env = exp(-t * 450.0) + val wave = sin(2.0 * PI * freq * t) + buffer[i] = (wave * env * 20000.0).toInt().coerceIn(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()).toShort() + } + return buffer + } + + /** + * 32ms dual-impulse snap for the classic spring-loaded sliding switch. + */ + private fun generateSwitchLatch(): ShortArray { + val durationMs = 32 + val numSamples = (SAMPLE_RATE * durationMs) / 1000 + val buffer = ShortArray(numSamples) + + for (i in 0 until numSamples) { + val t = i.toDouble() / SAMPLE_RATE + // Phase 1: onset low thump + val env1 = if (t < 0.012) exp(-t * 300.0) else 0.0 + val wave1 = sin(2.0 * PI * 550.0 * t) + + // Phase 2: sharp metallic snap 7ms later + val t2 = t - 0.007 + val env2 = if (t2 > 0.0) exp(-t2 * 280.0) else 0.0 + val wave2 = sin(2.0 * PI * 2200.0 * t2) + + val combined = (wave1 * env1 * 0.4) + (wave2 * env2 * 0.7) + buffer[i] = (combined * 25000.0).toInt().coerceIn(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()).toShort() + } + return buffer + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/components/button/IosButton.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/components/button/IosButton.kt new file mode 100644 index 0000000..6f51d96 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/components/button/IosButton.kt @@ -0,0 +1,119 @@ +package ai.heretek.pwnapple.components.button + +import ai.heretek.pwnapple.graphics.glossHighlight +import ai.heretek.pwnapple.graphics.innerShadow +import ai.heretek.pwnapple.graphics.skeuoBevel +import ai.heretek.pwnapple.theme.PwnappleTheme +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +enum class IosButtonStyle { + Silver, + Blue, + Red, + Black +} + +enum class IosButtonSize { + Normal, + Small +} + +/** + * Authentic iOS 6 glossy beveled button with tactile pressed states, + * specular reflection, and click audio. + */ +@Composable +fun IosButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + style: IosButtonStyle = IosButtonStyle.Silver, + size: IosButtonSize = IosButtonSize.Normal, + enabled: Boolean = true, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + val colors = PwnappleTheme.colors + val typography = PwnappleTheme.typography + val sound = PwnappleTheme.sound + val isPressed by interactionSource.collectIsPressedAsState() + + val cornerRadius: Dp = if (size == IosButtonSize.Small) 4.5.dp else 6.dp + val shape = remember(cornerRadius) { RoundedCornerShape(cornerRadius) } + + val brush = when (style) { + IosButtonStyle.Silver -> colors.buttonSilverBrush(isPressed) + IosButtonStyle.Blue -> colors.buttonBlueBrush(isPressed) + IosButtonStyle.Red -> colors.buttonRedBrush(isPressed) + IosButtonStyle.Black -> if (!isPressed) colors.navBarBlackBrush() else colors.navBarBlackBrush() + } + + val borderColor = when (style) { + IosButtonStyle.Silver -> colors.buttonSilverBorder + IosButtonStyle.Blue -> colors.buttonBlueBorder + IosButtonStyle.Red -> colors.buttonRedBorder + IosButtonStyle.Black -> Color.Black + } + + val textStyle: TextStyle = when (style) { + IosButtonStyle.Silver -> typography.buttonSilverText + else -> typography.buttonColorText + } + + val minHeight: Dp = if (size == IosButtonSize.Small) 30.dp else 42.dp + val horizontalPadding: Dp = if (size == IosButtonSize.Small) 10.dp else 18.dp + + Box( + modifier = modifier + .defaultMinSize(minHeight = minHeight) + .clip(shape) + .background(brush) + .skeuoBevel( + cornerRadius = cornerRadius, + borderColor = borderColor, + highlightColor = if (isPressed) Color.Transparent else Color.White.copy(alpha = 0.4f), + shadowColor = if (isPressed) Color.Black.copy(alpha = 0.4f) else Color.Black.copy(alpha = 0.2f) + ) + .then( + if (isPressed) { + Modifier.innerShadow(cornerRadius = cornerRadius, shadowColor = Color(0x66000000), shadowHeight = 3.dp) + } else { + Modifier.glossHighlight(cornerRadius = cornerRadius, glossAlpha = 0.42f) + } + ) + .clickable( + enabled = enabled, + interactionSource = interactionSource, + indication = null + ) { + sound.playButtonClick() + onClick() + } + .padding(horizontal = horizontalPadding, vertical = 6.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = text, + style = textStyle, + modifier = Modifier.offset(y = if (isPressed) 1.dp else 0.dp) + ) + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/components/button/IosSegmentedControl.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/components/button/IosSegmentedControl.kt new file mode 100644 index 0000000..7bdfd41 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/components/button/IosSegmentedControl.kt @@ -0,0 +1,147 @@ +package ai.heretek.pwnapple.components.button + +import ai.heretek.pwnapple.graphics.glossHighlight +import ai.heretek.pwnapple.graphics.innerShadow +import ai.heretek.pwnapple.theme.PwnappleTheme +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * Connected segmented control with active/inactive skeuomorphic gradient styling. + */ +@Composable +fun IosSegmentedControl( + items: List, + selectedIndex: Int, + onItemSelected: (Int) -> Unit, + modifier: Modifier = Modifier, + height: androidx.compose.ui.unit.Dp = 30.dp +) { + val sound = PwnappleTheme.sound + val outerShape = remember { RoundedCornerShape(4.dp) } + + Row( + modifier = modifier + .height(height) + .clip(outerShape) + .border(1.dp, Color(0xFF6B7E96), outerShape) + ) { + items.forEachIndexed { index, title -> + val isSelected = index == selectedIndex + + val segmentShape = when { + items.size == 1 -> outerShape + index == 0 -> RoundedCornerShape(topStart = 4.dp, bottomStart = 4.dp) + index == items.lastIndex -> RoundedCornerShape(topEnd = 4.dp, bottomEnd = 4.dp) + else -> RoundedCornerShape(0.dp) + } + + val brush = if (isSelected) { + Brush.verticalGradient( + listOf( + Color(0xFF33537E), + Color(0xFF4C73A6), + Color(0xFF5B84B8) + ) + ) + } else { + Brush.verticalGradient( + listOf( + Color(0xFFFBFBFB), + Color(0xFFE2E2E2), + Color(0xFFD0D0D0) + ) + ) + } + + val textStyle = if (isSelected) { + TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 13.sp, + color = Color.White, + shadow = Shadow( + color = Color.Black.copy(alpha = 0.6f), + offset = Offset(0f, -1.5f), + blurRadius = 0f + ) + ) + } else { + TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 13.sp, + color = Color(0xFF333333), + shadow = Shadow( + color = Color.White.copy(alpha = 0.8f), + offset = Offset(0f, 1.5f), + blurRadius = 0f + ) + ) + } + + Box( + modifier = Modifier + .weight(1f) + .clip(segmentShape) + .background(brush) + .then( + if (isSelected) { + Modifier.innerShadow(cornerRadius = 0.dp, shadowColor = Color(0x66000000), shadowHeight = 3.dp) + } else { + Modifier.glossHighlight(cornerRadius = 0.dp, glossAlpha = 0.35f) + } + ) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + if (index != selectedIndex) { + sound.playButtonClick() + onItemSelected(index) + } + } + .padding(horizontal = 8.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = title, + style = textStyle + ) + } + + // Divider line between segments + if (index < items.lastIndex) { + Box( + modifier = Modifier + .height(height) + .background(Color(0xFF6B7E96)) + .defaultMinSize(minWidth = 1.dp) + ) + } + } + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/components/dialog/IosActionSheet.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/components/dialog/IosActionSheet.kt new file mode 100644 index 0000000..5fa3539 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/components/dialog/IosActionSheet.kt @@ -0,0 +1,134 @@ +package ai.heretek.pwnapple.components.dialog + +import ai.heretek.pwnapple.components.button.IosButton +import ai.heretek.pwnapple.components.button.IosButtonStyle +import ai.heretek.pwnapple.graphics.darkLinenBackground +import ai.heretek.pwnapple.graphics.glossHighlight +import ai.heretek.pwnapple.theme.PwnappleTheme +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * Skeuomorphic iOS 6 bottom action sheet with dark translucent container, + * optional title, action buttons, destructive button, and cancel button. + */ +@Composable +fun IosActionSheet( + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + title: String? = null, + destructiveText: String? = null, + onDestructiveClick: (() -> Unit)? = null, + actionButtons: List Unit>> = emptyList(), + cancelText: String = "Cancel", + onCancelClick: () -> Unit = onDismissRequest +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.45f)) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { onDismissRequest() }, + contentAlignment = Alignment.BottomCenter + ) { + Box( + modifier = modifier + .fillMaxWidth() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) {} // consume clicks inside sheet + .background(Color(0xE61E242B)) + .darkLinenBackground() + .drawBehind { + // Top 1px highlight line + drawLine( + color = Color.White.copy(alpha = 0.25f), + start = Offset(0f, 0.5f), + end = Offset(size.width, 0.5f), + strokeWidth = 1f + ) + } + .padding(16.dp) + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + if (!title.isNullOrEmpty()) { + Text( + text = title, + style = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 14.sp, + color = Color(0xFFC0CAD5), + shadow = Shadow( + color = Color.Black.copy(alpha = 0.8f), + offset = Offset(0f, -1f), + blurRadius = 0f + ) + ), + textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = 12.dp) + ) + } + + // Destructive button (Red) + if (destructiveText != null && onDestructiveClick != null) { + IosButton( + text = destructiveText, + onClick = { onDestructiveClick(); onDismissRequest() }, + style = IosButtonStyle.Red, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(10.dp)) + } + + // Standard action buttons (Silver) + actionButtons.forEach { (btnText, action) -> + IosButton( + text = btnText, + onClick = { action(); onDismissRequest() }, + style = IosButtonStyle.Silver, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(10.dp)) + } + + // Cancel button (Black/Dark) + IosButton( + text = cancelText, + onClick = { onCancelClick(); onDismissRequest() }, + style = IosButtonStyle.Black, + modifier = Modifier.fillMaxWidth() + ) + } + } + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/components/dialog/IosAlert.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/components/dialog/IosAlert.kt new file mode 100644 index 0000000..86e2919 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/components/dialog/IosAlert.kt @@ -0,0 +1,162 @@ +package ai.heretek.pwnapple.components.dialog + +import ai.heretek.pwnapple.components.button.IosButton +import ai.heretek.pwnapple.components.button.IosButtonStyle +import ai.heretek.pwnapple.graphics.glossHighlight +import ai.heretek.pwnapple.graphics.skeuoBevel +import ai.heretek.pwnapple.theme.PwnappleTheme +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog + +/** + * The iconic iOS 6 UIAlertView with deep navy translucent gradient, + * silver beveled trim, embossed title, and glossy action buttons. + */ +@Composable +fun IosAlert( + title: String, + message: String, + onDismissRequest: () -> Unit, + confirmButtonText: String = "OK", + onConfirm: () -> Unit = onDismissRequest, + dismissButtonText: String? = null, + onDismiss: (() -> Unit)? = null +) { + val colors = PwnappleTheme.colors + val shape = remember { RoundedCornerShape(12.dp) } + + Dialog(onDismissRequest = onDismissRequest) { + Box( + modifier = Modifier + .width(280.dp) + .drawBehind { + // Outer dialog drop shadow + drawRoundRect( + color = Color.Black.copy(alpha = 0.6f), + topLeft = Offset(0f, 4f), + size = size, + cornerRadius = CornerRadius(12.dp.toPx(), 12.dp.toPx()) + ) + } + .clip(shape) + .background( + Brush.verticalGradient( + listOf( + colors.alertBackgroundTop.copy(alpha = 0.95f), + colors.alertBackgroundBottom.copy(alpha = 0.95f) + ) + ) + ) + .skeuoBevel( + cornerRadius = 12.dp, + borderColor = colors.alertBorder, + borderWidth = 1.5.dp, + highlightColor = Color.White.copy(alpha = 0.4f), + shadowColor = Color.Black.copy(alpha = 0.5f) + ) + .glossHighlight(cornerRadius = 12.dp, glossAlpha = 0.32f) + .padding(16.dp) + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + // Alert Title + Text( + text = title, + style = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 17.sp, + color = Color.White, + shadow = Shadow( + color = Color.Black.copy(alpha = 0.8f), + offset = Offset(0f, -1.5f), + blurRadius = 0f + ) + ), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Alert Message + Text( + text = message, + style = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + color = Color(0xFFD6E2F0), + shadow = Shadow( + color = Color.Black.copy(alpha = 0.6f), + offset = Offset(0f, -1f), + blurRadius = 0f + ) + ), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Buttons + if (dismissButtonText != null) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + IosButton( + text = dismissButtonText, + onClick = { onDismiss?.invoke(); onDismissRequest() }, + style = IosButtonStyle.Silver, + modifier = Modifier.weight(1f) + ) + IosButton( + text = confirmButtonText, + onClick = { onConfirm(); onDismissRequest() }, + style = IosButtonStyle.Blue, + modifier = Modifier.weight(1f) + ) + } + } else { + IosButton( + text = confirmButtonText, + onClick = { onConfirm(); onDismissRequest() }, + style = IosButtonStyle.Blue, + modifier = Modifier.fillMaxWidth() + ) + } + } + } + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/components/indicator/IosActivityIndicator.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/components/indicator/IosActivityIndicator.kt new file mode 100644 index 0000000..297c777 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/components/indicator/IosActivityIndicator.kt @@ -0,0 +1,65 @@ +package ai.heretek.pwnapple.components.indicator + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.rotate +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlin.math.roundToInt + +/** + * Authentic 12-blade iOS 6 activity indicator (spinner). + */ +@Composable +fun IosActivityIndicator( + modifier: Modifier = Modifier, + size: Dp = 24.dp, + baseColor: Color = Color(0xFF5C5C5C) +) { + val infiniteTransition = rememberInfiniteTransition(label = "spinnerTransition") + val step by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 12f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 1000, easing = LinearEasing), + repeatMode = RepeatMode.Restart + ), + label = "spinnerStep" + ) + + val currentStep = step.toInt() % 12 + + Canvas(modifier = modifier.size(size)) { + val bladeWidth = this.size.width * 0.09f + val bladeHeight = this.size.height * 0.28f + val radius = this.size.width / 2f + + for (i in 0 until 12) { + val alphaIndex = (i - currentStep + 12) % 12 + // Blades fade from 1.0 down to 0.15 + val alpha = (1f - (alphaIndex / 12f) * 0.85f).coerceIn(0.15f, 1f) + + rotate(degrees = i * 30f, pivot = Offset(radius, radius)) { + drawRoundRect( + color = baseColor.copy(alpha = alpha), + topLeft = Offset(radius - bladeWidth / 2f, this.size.height * 0.05f), + size = Size(bladeWidth, bladeHeight), + cornerRadius = CornerRadius(bladeWidth / 2f, bladeWidth / 2f) + ) + } + } + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/components/indicator/IosProgressBar.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/components/indicator/IosProgressBar.kt new file mode 100644 index 0000000..c5c9765 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/components/indicator/IosProgressBar.kt @@ -0,0 +1,61 @@ +package ai.heretek.pwnapple.components.indicator + +import ai.heretek.pwnapple.graphics.glossHighlight +import ai.heretek.pwnapple.graphics.innerShadow +import ai.heretek.pwnapple.theme.PwnappleTheme +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +/** + * Skeuomorphic iOS 6 progress bar with glossy cyan-blue progress and recessed track. + */ +@Composable +fun IosProgressBar( + progress: Float, // 0f..1f + modifier: Modifier = Modifier +) { + val colors = PwnappleTheme.colors + val clamped = progress.coerceIn(0f, 1f) + val shape = remember { RoundedCornerShape(5.dp) } + + Box( + modifier = modifier + .fillMaxWidth() + .height(10.dp) + .clip(shape) + .background( + Brush.verticalGradient( + listOf(Color(0xFFC0C0C0), Color(0xFFE2E2E2)) + ) + ) + .border(0.75.dp, Color(0xFF9E9E9E), shape) + .innerShadow(cornerRadius = 5.dp, shadowColor = Color(0x66000000), shadowHeight = 2.dp) + ) { + if (clamped > 0f) { + Box( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth(clamped) + .clip(shape) + .background( + Brush.verticalGradient( + listOf(colors.switchOnTop, colors.switchOnBottom) + ) + ) + .glossHighlight(cornerRadius = 5.dp, glossAlpha = 0.35f) + ) + } + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/components/input/IosSlider.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/components/input/IosSlider.kt new file mode 100644 index 0000000..b30fcbe --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/components/input/IosSlider.kt @@ -0,0 +1,157 @@ +package ai.heretek.pwnapple.components.input + +import ai.heretek.pwnapple.graphics.glossHighlight +import ai.heretek.pwnapple.graphics.innerShadow +import ai.heretek.pwnapple.theme.PwnappleTheme +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import kotlin.math.roundToInt + +/** + * Skeuomorphic slider with filled cyan glossy progress bar, + * inset unfilled track, and metallic thumb disc with grip grooves. + */ +@Composable +fun IosSlider( + value: Float, + onValueChange: (Float) -> Unit, + modifier: Modifier = Modifier, + valueRange: ClosedFloatingPointRange = 0f..1f, + enabled: Boolean = true +) { + val colors = PwnappleTheme.colors + var trackWidthPx by remember { mutableFloatStateOf(0f) } + val thumbSize = 26.dp + val trackHeight = 8.dp + val trackShape = remember { RoundedCornerShape(4.dp) } + + val normalized = ((value - valueRange.start) / (valueRange.endInclusive - valueRange.start)).coerceIn(0f, 1f) + + Box( + modifier = modifier + .fillMaxWidth() + .height(36.dp) + .onSizeChanged { trackWidthPx = it.width.toFloat() } + .pointerInput(enabled) { + if (!enabled) return@pointerInput + detectTapGestures { offset -> + val newNorm = (offset.x / trackWidthPx).coerceIn(0f, 1f) + val newValue = valueRange.start + newNorm * (valueRange.endInclusive - valueRange.start) + onValueChange(newValue) + } + } + .pointerInput(enabled) { + if (!enabled) return@pointerInput + detectDragGestures { change, _ -> + val newNorm = (change.position.x / trackWidthPx).coerceIn(0f, 1f) + val newValue = valueRange.start + newNorm * (valueRange.endInclusive - valueRange.start) + onValueChange(newValue) + } + }, + contentAlignment = Alignment.CenterStart + ) { + // Unfilled background track + Box( + modifier = Modifier + .fillMaxWidth() + .height(trackHeight) + .clip(trackShape) + .background( + Brush.verticalGradient( + listOf(Color(0xFFC0C0C0), Color(0xFFE2E2E2)) + ) + ) + .border(0.75.dp, Color(0xFF9E9E9E), trackShape) + .innerShadow(cornerRadius = 4.dp, shadowColor = Color(0x66000000), shadowHeight = 2.dp) + ) + + // Filled active progress track + if (trackWidthPx > 0f) { + val fillWidth = (trackWidthPx * normalized) + Box( + modifier = Modifier + .size( + width = with(androidx.compose.ui.platform.LocalDensity.current) { fillWidth.toDp() }, + height = trackHeight + ) + .clip(trackShape) + .background( + Brush.verticalGradient( + listOf(colors.switchOnTop, colors.switchOnBottom) + ) + ) + .glossHighlight(cornerRadius = 4.dp, glossAlpha = 0.35f) + ) + } + + // Metallic circular thumb + val thumbOffsetPx = if (trackWidthPx > 0f) { + val maxTravel = trackWidthPx - with(androidx.compose.ui.platform.LocalDensity.current) { thumbSize.toPx() } + (maxTravel * normalized).coerceAtLeast(0f) + } else 0f + + Box( + modifier = Modifier + .offset { IntOffset(thumbOffsetPx.roundToInt(), 0) } + .size(thumbSize) + .drawBehind { + // Outer drop shadow + drawCircle( + color = Color.Black.copy(alpha = 0.35f), + radius = size.minDimension / 2f, + center = center + Offset(0f, 1.5f) + ) + } + .clip(CircleShape) + .background( + Brush.verticalGradient( + listOf( + Color(0xFFFFFFFF), + Color(0xFFEDEDED), + Color(0xFFDCDCDC) + ) + ) + ) + .border(1.dp, Color(0xFFA6A6A6), CircleShape) + .glossHighlight(cornerRadius = 13.dp, glossAlpha = 0.45f) + .drawBehind { + // Center metallic grip lines + val cx = size.width / 2f + val cy = size.height / 2f + for (dy in listOf(-3f, 0f, 3f)) { + drawLine( + color = Color(0xFF8B8B8B), + start = Offset(cx - 4f, cy + dy), + end = Offset(cx + 4f, cy + dy), + strokeWidth = 1f + ) + } + } + ) + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/components/input/IosSwitch.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/components/input/IosSwitch.kt new file mode 100644 index 0000000..0add128 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/components/input/IosSwitch.kt @@ -0,0 +1,169 @@ +package ai.heretek.pwnapple.components.input + +import ai.heretek.pwnapple.graphics.glossHighlight +import ai.heretek.pwnapple.graphics.innerShadow +import ai.heretek.pwnapple.theme.PwnappleTheme +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlin.math.roundToInt + +/** + * The iconic iOS 6 sliding switch with glossy cyan "ON" and silver "OFF" tracks, + * beveled circular metal thumb, embossed labels, and tactile latch audio. + */ +@Composable +fun IosSwitch( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true +) { + val sound = PwnappleTheme.sound + val colors = PwnappleTheme.colors + + val trackWidth = 76.dp + val trackHeight = 28.dp + val thumbSize = 28.dp + val cornerRadius = 14.dp + val trackShape = remember { RoundedCornerShape(cornerRadius) } + + // Progress: 0.0 (OFF) -> 1.0 (ON) + val progress by animateFloatAsState( + targetValue = if (checked) 1f else 0f, + animationSpec = spring(dampingRatio = 0.85f, stiffness = 600f), + label = "switchThumbPosition" + ) + + Box( + modifier = modifier + .width(trackWidth) + .height(trackHeight) + .clip(trackShape) + .border(1.dp, Color(0xFF949494), trackShape) + .clickable( + enabled = enabled, + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + sound.playSwitchLatch() + onCheckedChange(!checked) + } + ) { + // Track layer + Box( + modifier = Modifier + .fillMaxSize() + .background( + if (checked) colors.switchOnBrush() else colors.switchOffBrush() + ) + .innerShadow(cornerRadius = cornerRadius, shadowColor = Color(0x66000000), shadowHeight = 4.dp) + .glossHighlight(cornerRadius = cornerRadius, glossAlpha = 0.25f) + ) { + // "ON" text when checked + if (progress > 0.4f) { + Text( + text = "ON", + style = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 13.sp, + color = Color.White.copy(alpha = progress), + shadow = Shadow( + color = Color.Black.copy(alpha = 0.45f * progress), + offset = Offset(0f, -1.5f), + blurRadius = 0f + ) + ), + modifier = Modifier + .align(Alignment.CenterStart) + .padding(start = 12.dp) + ) + } + + // "OFF" text when unchecked + if (progress < 0.6f) { + Text( + text = "OFF", + style = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 13.sp, + color = Color(0xFF7D7D7D).copy(alpha = 1f - progress), + shadow = Shadow( + color = Color.White.copy(alpha = 0.9f * (1f - progress)), + offset = Offset(0f, 1.5f), + blurRadius = 0f + ) + ), + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(end = 10.dp) + ) + } + } + + // Circular Thumb Knob + val maxOffsetPx = with(androidx.compose.ui.platform.LocalDensity.current) { + (trackWidth - thumbSize).toPx() + } + + Box( + modifier = Modifier + .offset { IntOffset(x = (maxOffsetPx * progress).roundToInt(), y = 0) } + .size(thumbSize) + .drawBehind { + // Drop shadow on knob + drawCircle( + color = Color.Black.copy(alpha = 0.35f), + radius = size.minDimension / 2f, + center = center + Offset(if (checked) -2f else 2f, 1.5f) + ) + } + .clip(CircleShape) + .background( + Brush.verticalGradient( + listOf( + colors.switchKnobTop, + Color(0xFFEDEDED), + colors.switchKnobBottom + ) + ) + ) + .border(1.dp, Color(0xFFB0B0B0), CircleShape) + .glossHighlight(cornerRadius = cornerRadius, glossAlpha = 0.45f) + ) + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/components/input/IosTextField.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/components/input/IosTextField.kt new file mode 100644 index 0000000..f01e536 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/components/input/IosTextField.kt @@ -0,0 +1,122 @@ +package ai.heretek.pwnapple.components.input + +import ai.heretek.pwnapple.graphics.innerShadow +import ai.heretek.pwnapple.theme.PwnappleTheme +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * Inset skeuomorphic text field with inner shadow, clear button, and placeholder. + */ +@Composable +fun IosTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + placeholder: String = "", + leadingIcon: @Composable (() -> Unit)? = null, + singleLine: Boolean = true, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default +) { + val shape = remember { RoundedCornerShape(5.dp) } + + Box( + modifier = modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 34.dp) + .clip(shape) + .background(Color.White) + .border(1.dp, Color(0xFFA5A5A5), shape) + .innerShadow(cornerRadius = 5.dp, shadowColor = Color(0x40000000), shadowHeight = 3.dp) + .padding(horizontal = 8.dp, vertical = 6.dp), + contentAlignment = Alignment.CenterStart + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + if (leadingIcon != null) { + Box(modifier = Modifier.padding(end = 6.dp)) { + leadingIcon() + } + } + + Box(modifier = Modifier.weight(1f)) { + if (value.isEmpty() && placeholder.isNotEmpty()) { + Text( + text = placeholder, + style = TextStyle( + fontFamily = FontFamily.SansSerif, + fontSize = 15.sp, + color = Color(0xFF9E9E9E) + ) + ) + } + + BasicTextField( + value = value, + onValueChange = onValueChange, + textStyle = TextStyle( + fontFamily = FontFamily.SansSerif, + fontSize = 15.sp, + color = Color.Black + ), + cursorBrush = SolidColor(Color(0xFF0272EC)), + singleLine = singleLine, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + modifier = Modifier.fillMaxWidth() + ) + } + + if (value.isNotEmpty()) { + Box( + modifier = Modifier + .size(18.dp) + .clip(CircleShape) + .background(Color(0xFF8E8E93)) + .clickable { onValueChange("") }, + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Clear text", + tint = Color.White, + modifier = Modifier.size(12.dp) + ) + } + } + } + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/components/navigation/IosBackButton.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/components/navigation/IosBackButton.kt new file mode 100644 index 0000000..931e012 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/components/navigation/IosBackButton.kt @@ -0,0 +1,95 @@ +package ai.heretek.pwnapple.components.navigation + +import ai.heretek.pwnapple.graphics.ArrowBackShape +import ai.heretek.pwnapple.graphics.glossHighlight +import ai.heretek.pwnapple.theme.PwnappleTheme +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * The iconic iOS 6 chevron back button with arrow point, + * glossy gradient fill, and embossed title text. + */ +@Composable +fun IosBackButton( + text: String = "Back", + onClick: () -> Unit, + modifier: Modifier = Modifier, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + val isPressed by interactionSource.collectIsPressedAsState() + val sound = PwnappleTheme.sound + val shape = remember { ArrowBackShape(arrowWidth = 13.dp, cornerRadius = 4.5.dp) } + + val brush = if (!isPressed) { + Brush.verticalGradient( + listOf( + Color(0xFF7A94B5), + Color(0xFF567298), + Color(0xFF425E85) + ) + ) + } else { + Brush.verticalGradient( + listOf( + Color(0xFF334A6A), + Color(0xFF486386) + ) + ) + } + + Box( + modifier = modifier + .height(30.dp) + .clip(shape) + .background(brush) + .border(1.dp, Color(0xFF263952), shape) + .glossHighlight(cornerRadius = 4.dp, glossAlpha = 0.35f) + .clickable( + interactionSource = interactionSource, + indication = null + ) { + sound.playButtonClick() + onClick() + } + .padding(start = 14.dp, end = 10.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = text, + style = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 12.sp, + color = Color.White, + shadow = Shadow( + color = Color.Black.copy(alpha = 0.65f), + offset = Offset(0f, -1.5f), + blurRadius = 0f + ) + ) + ) + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/components/navigation/IosNavigationBar.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/components/navigation/IosNavigationBar.kt new file mode 100644 index 0000000..ed32c59 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/components/navigation/IosNavigationBar.kt @@ -0,0 +1,114 @@ +package ai.heretek.pwnapple.components.navigation + +import ai.heretek.pwnapple.graphics.glossHighlight +import ai.heretek.pwnapple.theme.PwnappleTheme +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp + +enum class IosNavBarStyle { + DefaultBlue, + BlackTranslucent, + BlackOpaque +} + +/** + * Standard iOS 6 navigation bar with glossy gradient, embossed center title, + * specular highlight line, and bottom hairline border. + */ +@Composable +fun IosNavigationBar( + title: String, + modifier: Modifier = Modifier, + style: IosNavBarStyle = IosNavBarStyle.DefaultBlue, + navigationIcon: @Composable (() -> Unit)? = null, + actions: @Composable RowScope.() -> Unit = {} +) { + val colors = PwnappleTheme.colors + val typography = PwnappleTheme.typography + + val brush = when (style) { + IosNavBarStyle.DefaultBlue -> colors.navBarBlueBrush() + IosNavBarStyle.BlackTranslucent -> Brush.verticalGradient( + listOf(colors.navBarBlackTop.copy(alpha = 0.88f), colors.navBarBlackBottom.copy(alpha = 0.88f)) + ) + IosNavBarStyle.BlackOpaque -> colors.navBarBlackBrush() + } + + val bottomBorderColor = when (style) { + IosNavBarStyle.DefaultBlue -> Color(0xFF2A374A) + else -> Color(0xFF000000) + } + + Box( + modifier = modifier + .fillMaxWidth() + .height(44.dp) + .background(brush) + .glossHighlight(cornerRadius = 0.dp, glossAlpha = 0.28f) + .drawBehind { + // Top 1px specular line + drawLine( + color = Color.White.copy(alpha = 0.35f), + start = Offset(0f, 0.5f), + end = Offset(size.width, 0.5f), + strokeWidth = 1f + ) + // Bottom 1px hairline border + drawLine( + color = bottomBorderColor, + start = Offset(0f, size.height - 0.5f), + end = Offset(size.width, size.height - 0.5f), + strokeWidth = 1f + ) + // Bottom drop shadow + drawLine( + color = Color.Black.copy(alpha = 0.3f), + start = Offset(0f, size.height + 0.5f), + end = Offset(size.width, size.height + 0.5f), + strokeWidth = 1.5f + ) + } + .padding(horizontal = 8.dp), + contentAlignment = Alignment.Center + ) { + // Left navigation icon + if (navigationIcon != null) { + Box( + modifier = Modifier.align(Alignment.CenterStart) + ) { + navigationIcon() + } + } + + // Embossed center title + Text( + text = title, + style = typography.navTitle, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(horizontal = 48.dp) + ) + + // Right actions + Row( + modifier = Modifier.align(Alignment.CenterEnd), + verticalAlignment = Alignment.CenterVertically, + content = actions + ) + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/components/picker/IosPickerWheel.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/components/picker/IosPickerWheel.kt new file mode 100644 index 0000000..4548b41 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/components/picker/IosPickerWheel.kt @@ -0,0 +1,221 @@ +package ai.heretek.pwnapple.components.picker + +import ai.heretek.pwnapple.theme.PwnappleTheme +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlin.math.abs +import kotlin.math.cos +import kotlin.math.roundToInt + +/** + * The iconic 3D drum cylinder wheel picker (UIPickerView) with cylindrical + * perspective curvature, glass lens overlay, and ratchet audio ticks. + */ +@Composable +fun IosPickerWheel( + items: List, + selectedIndex: Int, + onSelectedIndexChange: (Int) -> Unit, + modifier: Modifier = Modifier, + visibleItemsCount: Int = 5, + itemHeight: androidx.compose.ui.unit.Dp = 38.dp +) { + if (items.isEmpty()) return + + val sound = PwnappleTheme.sound + val itemHeightPx = with(androidx.compose.ui.platform.LocalDensity.current) { itemHeight.toPx() } + val totalHeight = itemHeight * visibleItemsCount + + var currentOffsetPx by remember(selectedIndex) { + mutableFloatStateOf(-selectedIndex * itemHeightPx) + } + + var lastTickIndex by remember { mutableIntStateOf(selectedIndex) } + + val draggableState = rememberDraggableState { delta -> + val maxOffset = 0f + val minOffset = -(items.size - 1) * itemHeightPx + currentOffsetPx = (currentOffsetPx + delta).coerceIn(minOffset, maxOffset) + + val activeIndex = (-currentOffsetPx / itemHeightPx).roundToInt().coerceIn(0, items.lastIndex) + if (activeIndex != lastTickIndex) { + sound.playWheelTick() + lastTickIndex = activeIndex + } + } + + val shape = remember { RoundedCornerShape(6.dp) } + + Box( + modifier = modifier + .fillMaxWidth() + .height(totalHeight) + .clip(shape) + .background( + Brush.verticalGradient( + listOf( + Color(0xFFE3E5E8), + Color(0xFFF7F8F9), + Color(0xFFFFFFFF), + Color(0xFFF7F8F9), + Color(0xFFE3E5E8) + ) + ) + ) + .border(1.dp, Color(0xFF9E9E9E), shape) + .draggable( + state = draggableState, + orientation = Orientation.Vertical, + onDragStopped = { + val targetIndex = (-currentOffsetPx / itemHeightPx).roundToInt().coerceIn(0, items.lastIndex) + currentOffsetPx = -targetIndex * itemHeightPx + onSelectedIndexChange(targetIndex) + } + ), + contentAlignment = Alignment.Center + ) { + // Render visible items with 3D cylindrical projection + items.forEachIndexed { index, text -> + val itemCenterY = (index * itemHeightPx) + currentOffsetPx + (totalHeight.value * 1.5f) + val diffFromCenterPx = (index * itemHeightPx) + currentOffsetPx + val normalizedDiff = (diffFromCenterPx / itemHeightPx) + + if (abs(normalizedDiff) <= (visibleItemsCount / 2f + 1f)) { + val scale = (1f - (abs(normalizedDiff) * 0.10f)).coerceIn(0.75f, 1f) + val alpha = (1f - (abs(normalizedDiff) * 0.28f)).coerceIn(0.15f, 1f) + val rotationX = (-normalizedDiff * 22f).coerceIn(-65f, 65f) + + val isCenter = abs(normalizedDiff) < 0.5f + + Box( + modifier = Modifier + .fillMaxWidth() + .height(itemHeight) + .offset { IntOffset(0, diffFromCenterPx.roundToInt()) } + .graphicsLayer { + this.scaleX = scale + this.scaleY = scale + this.alpha = alpha + this.rotationX = rotationX + cameraDistance = 12f * density + }, + contentAlignment = Alignment.Center + ) { + Text( + text = text, + style = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = if (isCenter) FontWeight.Bold else FontWeight.Medium, + fontSize = 20.sp, + color = if (isCenter) Color.Black else Color(0xFF4A4A4A), + shadow = if (isCenter) { + Shadow( + color = Color.White.copy(alpha = 0.9f), + offset = Offset(0f, 1.5f), + blurRadius = 0f + ) + } else null + ), + textAlign = TextAlign.Center + ) + } + } + } + + // Central Glass Magnifying Lens Overlay Bar + Box( + modifier = Modifier + .fillMaxWidth() + .height(itemHeight) + .drawWithContent { + drawContent() + // Top glass reflection line + drawLine( + color = Color(0x664A7EBB), + start = Offset(0f, 0.5f), + end = Offset(size.width, 0.5f), + strokeWidth = 1.5f + ) + // Bottom glass reflection line + drawLine( + color = Color(0x664A7EBB), + start = Offset(0f, size.height - 0.5f), + end = Offset(size.width, size.height - 0.5f), + strokeWidth = 1.5f + ) + } + .background( + Brush.verticalGradient( + listOf( + Color(0x1A2A6EB5), + Color(0x082A6EB5), + Color(0x1A2A6EB5) + ) + ) + ) + ) + + // Top & Bottom cylindrical shadow vignette + Box( + modifier = Modifier + .fillMaxSize() + .drawWithContent { + drawContent() + val vHeight = size.height * 0.35f + // Top shadow vignette + drawRect( + brush = Brush.verticalGradient( + listOf(Color(0x77000000), Color.Transparent), + startY = 0f, + endY = vHeight + ), + size = Size(size.width, vHeight) + ) + // Bottom shadow vignette + drawRect( + brush = Brush.verticalGradient( + listOf(Color.Transparent, Color(0x77000000)), + startY = size.height - vHeight, + endY = size.height + ), + topLeft = Offset(0f, size.height - vHeight), + size = Size(size.width, vHeight) + ) + } + ) + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/components/table/IosGroupedTable.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/components/table/IosGroupedTable.kt new file mode 100644 index 0000000..bb3fd32 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/components/table/IosGroupedTable.kt @@ -0,0 +1,35 @@ +package ai.heretek.pwnapple.components.table + +import ai.heretek.pwnapple.theme.PwnappleTheme +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp + +/** + * Grouped rounded table card with 10dp radius and 1px border. + */ +@Composable +fun IosGroupedTable( + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit +) { + val colors = PwnappleTheme.colors + val shape = remember { RoundedCornerShape(10.dp) } + + Column( + modifier = modifier + .fillMaxWidth() + .clip(shape) + .background(colors.cardBackground) + .border(1.dp, colors.cardBorder, shape), + content = content + ) +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/components/table/IosTableCell.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/components/table/IosTableCell.kt new file mode 100644 index 0000000..3862995 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/components/table/IosTableCell.kt @@ -0,0 +1,201 @@ +package ai.heretek.pwnapple.components.table + +import ai.heretek.pwnapple.theme.PwnappleTheme +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +enum class IosCellAccessory { + None, + DisclosureIndicator, + DetailDisclosureButton, + Checkmark +} + +/** + * Standard iOS 6 table cell with leading icon, title, detail label, + * disclosure chevron, and interactive blue pressed state. + */ +@Composable +fun IosTableCell( + title: String, + modifier: Modifier = Modifier, + subtitle: String? = null, + detailText: String? = null, + leadingContent: @Composable (() -> Unit)? = null, + accessory: IosCellAccessory = IosCellAccessory.None, + accessoryContent: @Composable (() -> Unit)? = null, + showDivider: Boolean = true, + onClick: (() -> Unit)? = null, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + val colors = PwnappleTheme.colors + val typography = PwnappleTheme.typography + val sound = PwnappleTheme.sound + val isPressed by interactionSource.collectIsPressedAsState() + + val isInteractive = onClick != null + val indentPx = if (leadingContent != null) 48.dp else 14.dp + + val pressedBrush = Brush.verticalGradient( + listOf( + Color(0xFF0A85F6), + Color(0xFF015FE6) + ) + ) + + Box( + modifier = modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 44.dp) + .then( + if (isPressed && isInteractive) { + Modifier.background(pressedBrush) + } else { + Modifier.background(colors.cardBackground) + } + ) + .then( + if (isInteractive) { + Modifier.clickable( + interactionSource = interactionSource, + indication = null + ) { + sound.playButtonClick() + onClick?.invoke() + } + } else Modifier + ) + .drawBehind { + if (showDivider) { + val startX = indentPx.toPx() + drawLine( + color = colors.cellDivider, + start = Offset(startX, size.height - 0.5f), + end = Offset(size.width, size.height - 0.5f), + strokeWidth = 1f + ) + } + } + .padding(horizontal = 14.dp, vertical = 10.dp), + contentAlignment = Alignment.CenterStart + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + // Leading icon/content + if (leadingContent != null) { + Box( + modifier = Modifier.padding(end = 12.dp), + contentAlignment = Alignment.Center + ) { + leadingContent() + } + } + + // Title & optional subtitle + androidx.compose.foundation.layout.Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = title, + style = typography.cellTitle.copy( + color = if (isPressed && isInteractive) Color.White else colors.bodyText + ) + ) + if (!subtitle.isNullOrEmpty()) { + Text( + text = subtitle, + style = typography.cellSubtitle.copy( + color = if (isPressed && isInteractive) Color.White.copy(alpha = 0.85f) else colors.tertiaryText + ), + modifier = Modifier.padding(top = 2.dp) + ) + } + } + + // Detail text on right + if (!detailText.isNullOrEmpty()) { + Text( + text = detailText, + style = typography.cellSubtitle.copy( + color = if (isPressed && isInteractive) Color.White else colors.secondaryText + ), + modifier = Modifier.padding(end = 6.dp) + ) + } + + // Custom accessory or built-in accessories + if (accessoryContent != null) { + accessoryContent() + } else { + when (accessory) { + IosCellAccessory.DisclosureIndicator -> { + Icon( + imageVector = Icons.Default.ChevronRight, + contentDescription = "Navigate", + tint = if (isPressed && isInteractive) Color.White else Color(0xFF7F7F7F), + modifier = Modifier.size(20.dp) + ) + } + IosCellAccessory.Checkmark -> { + Icon( + imageVector = Icons.Default.Check, + contentDescription = "Selected", + tint = if (isPressed && isInteractive) Color.White else Color(0xFF2E63B8), + modifier = Modifier.size(20.dp) + ) + } + IosCellAccessory.DetailDisclosureButton -> { + Box( + modifier = Modifier + .size(22.dp) + .background( + Brush.verticalGradient( + listOf(Color(0xFF5A89C7), Color(0xFF2C5CA0)) + ), + shape = androidx.compose.foundation.shape.CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.ChevronRight, + contentDescription = "Detail", + tint = Color.White, + modifier = Modifier.size(16.dp) + ) + } + } + IosCellAccessory.None -> {} + } + } + } + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/components/table/IosTableSection.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/components/table/IosTableSection.kt new file mode 100644 index 0000000..d0b5888 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/components/table/IosTableSection.kt @@ -0,0 +1,49 @@ +package ai.heretek.pwnapple.components.table + +import ai.heretek.pwnapple.theme.PwnappleTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +/** + * A section wrapper in an iOS 6 grouped table with letterpress debossed + * header and footer captions. + */ +@Composable +fun IosTableSection( + modifier: Modifier = Modifier, + headerText: String? = null, + footerText: String? = null, + content: @Composable ColumnScope.() -> Unit +) { + val typography = PwnappleTheme.typography + + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 10.dp, vertical = 6.dp) + ) { + if (!headerText.isNullOrEmpty()) { + Text( + text = headerText, + style = typography.sectionHeader, + modifier = Modifier.padding(start = 12.dp, top = 10.dp, bottom = 6.dp) + ) + } + + IosGroupedTable(content = content) + + if (!footerText.isNullOrEmpty()) { + Text( + text = footerText, + style = typography.sectionFooter, + modifier = Modifier.padding(start = 12.dp, top = 6.dp, bottom = 8.dp) + ) + } + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/components/tabs/IosBadge.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/components/tabs/IosBadge.kt new file mode 100644 index 0000000..5febf45 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/components/tabs/IosBadge.kt @@ -0,0 +1,64 @@ +package ai.heretek.pwnapple.components.tabs + +import ai.heretek.pwnapple.graphics.glossHighlight +import ai.heretek.pwnapple.theme.PwnappleTheme +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +/** + * Authentic glossy red notification badge with white rim and drop shadow. + */ +@Composable +fun IosBadge( + count: String, + modifier: Modifier = Modifier +) { + val typography = PwnappleTheme.typography + val shape = remember { RoundedCornerShape(10.dp) } + + Box( + modifier = modifier + .drawBehind { + drawRoundRect( + color = Color.Black.copy(alpha = 0.45f), + topLeft = Offset(0f, 1.5f), + size = size, + cornerRadius = androidx.compose.ui.geometry.CornerRadius(10.dp.toPx(), 10.dp.toPx()) + ) + } + .clip(shape) + .background( + Brush.verticalGradient( + listOf( + Color(0xFFEE2D2D), + Color(0xFFBA0F0F) + ) + ) + ) + .border(1.5.dp, Color.White, shape) + .glossHighlight(cornerRadius = 10.dp, glossAlpha = 0.5f) + .defaultMinSize(minWidth = 18.dp, minHeight = 18.dp) + .padding(horizontal = 5.dp, vertical = 1.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = count, + style = typography.badgeText + ) + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/components/tabs/IosTabBar.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/components/tabs/IosTabBar.kt new file mode 100644 index 0000000..ce2e158 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/components/tabs/IosTabBar.kt @@ -0,0 +1,144 @@ +package ai.heretek.pwnapple.components.tabs + +import ai.heretek.pwnapple.theme.PwnappleTheme +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +data class IosTabItem( + val title: String, + val icon: ImageVector, + val badge: String? = null +) + +/** + * Skeuomorphic dark matte bottom tab bar with blue glow active indicators. + */ +@Composable +fun IosTabBar( + items: List, + selectedIndex: Int, + onTabSelected: (Int) -> Unit, + modifier: Modifier = Modifier +) { + val sound = PwnappleTheme.sound + val colors = PwnappleTheme.colors + val typography = PwnappleTheme.typography + + Box( + modifier = modifier + .fillMaxWidth() + .height(49.dp) + .background( + Brush.verticalGradient( + listOf( + Color(0xFF222222), + Color(0xFF141414), + Color(0xFF080808) + ) + ) + ) + .drawBehind { + // Top 1px border + drawLine( + color = Color.Black, + start = Offset(0f, 0f), + end = Offset(size.width, 0f), + strokeWidth = 1f + ) + // Inner highlight line + drawLine( + color = Color.White.copy(alpha = 0.15f), + start = Offset(0f, 1f), + end = Offset(size.width, 1f), + strokeWidth = 1f + ) + } + ) { + Row( + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + horizontalArrangement = Arrangement.SpaceAround, + verticalAlignment = Alignment.CenterVertically + ) { + items.forEachIndexed { index, item -> + val isSelected = index == selectedIndex + val tintColor = if (isSelected) colors.tabBarActiveGlow else colors.tabBarInactive + + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + if (index != selectedIndex) { + sound.playButtonClick() + onTabSelected(index) + } + }, + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Box(contentAlignment = Alignment.TopEnd) { + Icon( + imageVector = item.icon, + contentDescription = item.title, + tint = tintColor, + modifier = Modifier.size(24.dp) + ) + if (item.badge != null) { + IosBadge( + count = item.badge, + modifier = Modifier.offset(x = 10.dp, y = (-4).dp) + ) + } + } + + Text( + text = item.title, + style = typography.tabBarLabel.copy( + color = tintColor, + shadow = if (isSelected) { + Shadow( + color = Color(0x992EABFE), + offset = Offset(0f, 0f), + blurRadius = 4f + ) + } else null + ), + modifier = Modifier.padding(top = 2.dp) + ) + } + } + } + } + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/graphics/ArrowBackShape.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/graphics/ArrowBackShape.kt new file mode 100644 index 0000000..2caa9c1 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/graphics/ArrowBackShape.kt @@ -0,0 +1,53 @@ +package ai.heretek.pwnapple.graphics + +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp + +/** + * Shape producing the iconic iOS 6 chevron back-button outline: + * a pointed left arrow head merging seamlessly into a rounded rectangle. + */ +class ArrowBackShape( + private val arrowWidth: Dp = 14.dp, + private val cornerRadius: Dp = 4.dp +) : Shape { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + val path = Path() + val w = size.width + val h = size.height + val aw = with(density) { arrowWidth.toPx() }.coerceAtMost(w * 0.45f) + val r = with(density) { cornerRadius.toPx() }.coerceAtMost(h / 2f) + + // Start at left tip midpoint + path.moveTo(0f, h / 2f) + // Slant up to top edge + path.lineTo(aw, 0f) + // Horizontal top edge + path.lineTo(w - r, 0f) + // Top right corner arc + path.quadraticTo(w, 0f, w, r) + // Right edge + path.lineTo(w, h - r) + // Bottom right corner arc + path.quadraticTo(w, h, w - r, h) + // Horizontal bottom edge + path.lineTo(aw, h) + // Slant back down-left to tip midpoint + path.lineTo(0f, h / 2f) + path.close() + + return Outline.Generic(path) + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/graphics/BevelModifiers.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/graphics/BevelModifiers.kt new file mode 100644 index 0000000..1ccf637 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/graphics/BevelModifiers.kt @@ -0,0 +1,54 @@ +package ai.heretek.pwnapple.graphics + +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Draws a 3D beveled skeuomorphic border with an outer stroke, + * top inner specular highlight line, and bottom inner shadow. + */ +fun Modifier.skeuoBevel( + cornerRadius: Dp = 4.dp, + borderColor: Color, + borderWidth: Dp = 1.dp, + highlightColor: Color = Color.White.copy(alpha = 0.35f), + shadowColor: Color = Color.Black.copy(alpha = 0.25f) +): Modifier = this.drawWithContent { + drawContent() + val width = size.width + val height = size.height + val rPx = cornerRadius.toPx() + val bWidthPx = borderWidth.toPx() + + // Outer border stroke + drawRoundRect( + color = borderColor, + topLeft = Offset(bWidthPx / 2f, bWidthPx / 2f), + size = Size(width - bWidthPx, height - bWidthPx), + cornerRadius = CornerRadius(rPx, rPx), + style = Stroke(width = bWidthPx) + ) + + // Inner top highlight line + drawLine( + color = highlightColor, + start = Offset(rPx, bWidthPx + 0.5f), + end = Offset(width - rPx, bWidthPx + 0.5f), + strokeWidth = 1f + ) + + // Inner bottom shadow line + drawLine( + color = shadowColor, + start = Offset(rPx, height - bWidthPx - 0.5f), + end = Offset(width - rPx, height - bWidthPx - 0.5f), + strokeWidth = 1f + ) +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/graphics/GlossModifiers.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/graphics/GlossModifiers.kt new file mode 100644 index 0000000..ace4485 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/graphics/GlossModifiers.kt @@ -0,0 +1,109 @@ +package ai.heretek.pwnapple.graphics + +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Draws the iconic iOS 6 curved specular reflection over the top half of a component. + */ +fun Modifier.glossHighlight( + cornerRadius: Dp = 4.dp, + glossAlpha: Float = 0.45f +): Modifier = this.drawWithContent { + drawContent() + val width = size.width + val height = size.height + val rPx = cornerRadius.toPx() + + val path = Path().apply { + addRoundRect( + androidx.compose.ui.geometry.RoundRect( + left = 0f, + top = 0f, + right = width, + bottom = height, + topLeftCornerRadius = CornerRadius(rPx, rPx), + topRightCornerRadius = CornerRadius(rPx, rPx), + bottomLeftCornerRadius = CornerRadius(rPx, rPx), + bottomRightCornerRadius = CornerRadius(rPx, rPx) + ) + ) + } + + clipPath(path) { + // Linear top gloss layer (top 50%) + val glossHeight = height * 0.50f + drawRect( + brush = Brush.verticalGradient( + colors = listOf( + Color.White.copy(alpha = glossAlpha), + Color.White.copy(alpha = glossAlpha * 0.15f) + ), + startY = 0f, + endY = glossHeight + ), + topLeft = Offset.Zero, + size = Size(width, glossHeight) + ) + + // Specular highlight hairline on very top rim + drawLine( + color = Color.White.copy(alpha = glossAlpha * 0.9f), + start = Offset(rPx, 1f), + end = Offset(width - rPx, 1f), + strokeWidth = 1.5f + ) + } +} + +/** + * Renders an inner recessed shadow for inset controls (text fields, switch grooves, pressed buttons). + */ +fun Modifier.innerShadow( + cornerRadius: Dp = 4.dp, + shadowColor: Color = Color(0x55000000), + shadowHeight: Dp = 4.dp +): Modifier = this.drawWithContent { + drawContent() + val width = size.width + val height = size.height + val rPx = cornerRadius.toPx() + val sHeight = shadowHeight.toPx() + + val path = Path().apply { + addRoundRect( + androidx.compose.ui.geometry.RoundRect( + left = 0f, + top = 0f, + right = width, + bottom = height, + topLeftCornerRadius = CornerRadius(rPx, rPx), + topRightCornerRadius = CornerRadius(rPx, rPx), + bottomLeftCornerRadius = CornerRadius(rPx, rPx), + bottomRightCornerRadius = CornerRadius(rPx, rPx) + ) + ) + } + + clipPath(path) { + // Inset shadow gradient on top edge + drawRect( + brush = Brush.verticalGradient( + colors = listOf(shadowColor, Color.Transparent), + startY = 0f, + endY = sHeight + ), + topLeft = Offset.Zero, + size = Size(width, sHeight) + ) + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/graphics/Textures.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/graphics/Textures.kt new file mode 100644 index 0000000..b9503fd --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/graphics/Textures.kt @@ -0,0 +1,67 @@ +package ai.heretek.pwnapple.graphics + +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Draws the classic vertical alternating pinstripe pattern from iOS 6 Settings.app. + */ +fun Modifier.pinstripeBackground( + baseColor: Color = Color(0xFFC5CCD4), + stripeColor: Color = Color(0xFFCBD2D8), + stripeWidth: Dp = 2.dp +): Modifier = this.drawBehind { + drawRect(color = baseColor) + val width = size.width + val height = size.height + val sWidthPx = stripeWidth.toPx() + val period = sWidthPx * 2 + + var x = 0f + while (x < width) { + drawLine( + color = stripeColor, + start = Offset(x + (sWidthPx / 2f), 0f), + end = Offset(x + (sWidthPx / 2f), height), + strokeWidth = sWidthPx + ) + x += period + } +} + +/** + * Draws the dark woven linen texture seen in the iOS 6 notification center, + * multitasking bar, and folder backgrounds. + */ +fun Modifier.darkLinenBackground( + baseColor: Color = Color(0xFF23272B), + crossColor: Color = Color(0xFF2C3238), + gridStep: Dp = 4.dp +): Modifier = this.drawBehind { + drawRect(color = baseColor) + val width = size.width + val height = size.height + val stepPx = gridStep.toPx() + + // Diagonal weave lines + var offset = -height + while (offset < width) { + drawLine( + color = crossColor.copy(alpha = 0.5f), + start = Offset(offset, 0f), + end = Offset(offset + height, height), + strokeWidth = 1f + ) + drawLine( + color = crossColor.copy(alpha = 0.5f), + start = Offset(offset + height, 0f), + end = Offset(offset, height), + strokeWidth = 1f + ) + offset += stepPx + } +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/theme/PwnappleAudio.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/theme/PwnappleAudio.kt new file mode 100644 index 0000000..3ab9dd1 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/theme/PwnappleAudio.kt @@ -0,0 +1,22 @@ +package ai.heretek.pwnapple.theme + +import ai.heretek.pwnapple.audio.SoundPlayer +import androidx.compose.runtime.compositionLocalOf + +class PwnappleAudioFeedback( + var enabled: Boolean = true +) { + fun playButtonClick() { + if (enabled) SoundPlayer.playButtonClick() + } + + fun playWheelTick() { + if (enabled) SoundPlayer.playWheelTick() + } + + fun playSwitchLatch() { + if (enabled) SoundPlayer.playSwitchLatch() + } +} + +val LocalPwnappleSoundFeedback = compositionLocalOf { PwnappleAudioFeedback() } diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/theme/PwnappleColors.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/theme/PwnappleColors.kt new file mode 100644 index 0000000..32f8c01 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/theme/PwnappleColors.kt @@ -0,0 +1,132 @@ +package ai.heretek.pwnapple.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color + +@Immutable +data class PwnappleColors( + // Canvas & Backgrounds + val canvasPinstripeBase: Color = Color(0xFFC5CCD4), + val canvasPinstripeStripe: Color = Color(0xFFCBD2D8), + val canvasDarkLinenBase: Color = Color(0xFF262B30), + val canvasDarkLinenStripe: Color = Color(0xFF30363C), + + // Grouped Table Cards + val cardBackground: Color = Color(0xFFFFFFFF), + val cardBorder: Color = Color(0xFFABABAB), + val cellDivider: Color = Color(0xFFE2E2E2), + val cellPressed: Color = Color(0xFF0272EC), + val cellPressedText: Color = Color(0xFFFFFFFF), + + // Navigation Bar + val navBarBlueTop: Color = Color(0xFF8A9EB9), + val navBarBlueBottom: Color = Color(0xFF5C7496), + val navBarBlackTop: Color = Color(0xFF4C4C4C), + val navBarBlackBottom: Color = Color(0xFF141414), + val navBarShadow: Color = Color(0xFF2A3442), + + // Standard Buttons (Silver) + val buttonSilverTop: Color = Color(0xFFFCFCFC), + val buttonSilverMid: Color = Color(0xFFE9E9E9), + val buttonSilverBottom: Color = Color(0xFFD4D4D4), + val buttonSilverBorder: Color = Color(0xFFA5A5A5), + val buttonSilverText: Color = Color(0xFF222222), + + // Action Buttons (Gloss Blue) + val buttonBlueTop: Color = Color(0xFF6E9BD7), + val buttonBlueMid: Color = Color(0xFF3F75C1), + val buttonBlueBottom: Color = Color(0xFF2B5FAA), + val buttonBlueBorder: Color = Color(0xFF1E4378), + val buttonBlueText: Color = Color(0xFFFFFFFF), + + // Destructive Buttons (Gloss Red) + val buttonRedTop: Color = Color(0xFFEE5353), + val buttonRedMid: Color = Color(0xFFDA2A2A), + val buttonRedBottom: Color = Color(0xFFB81818), + val buttonRedBorder: Color = Color(0xFF851010), + val buttonRedText: Color = Color(0xFFFFFFFF), + + // Switch & Slider (Cyan / Blue) + val switchOnTop: Color = Color(0xFF2D8CE6), + val switchOnBottom: Color = Color(0xFF0A62BD), + val switchOffTop: Color = Color(0xFFD6D6D6), + val switchOffBottom: Color = Color(0xFFF2F2F2), + val switchKnobTop: Color = Color(0xFFFFFFFF), + val switchKnobBottom: Color = Color(0xFFE4E4E4), + val switchKnobBorder: Color = Color(0xFFB0B0B0), + + // Text & Headers + val headerText: Color = Color(0xFF4C566C), + val bodyText: Color = Color(0xFF000000), + val secondaryText: Color = Color(0xFF385487), + val tertiaryText: Color = Color(0xFF7F7F7F), + + // Tab Bar + val tabBarBackground: Color = Color(0xFF121212), + val tabBarBorder: Color = Color(0xFF000000), + val tabBarActiveGlow: Color = Color(0xFF2EABFE), + val tabBarInactive: Color = Color(0xFF919191), + + // Badges & Indicators + val badgeRedTop: Color = Color(0xFFEE2828), + val badgeRedBottom: Color = Color(0xFFB21010), + val badgeBorder: Color = Color(0xFFFFFFFF), + + // Alert & Action Sheet + val alertBackgroundTop: Color = Color(0xFF233654), + val alertBackgroundBottom: Color = Color(0xFF111E33), + val alertBorder: Color = Color(0xFF8B9FB8) +) { + fun navBarBlueBrush(): Brush = Brush.verticalGradient( + listOf(navBarBlueTop, navBarBlueBottom) + ) + + fun navBarBlackBrush(): Brush = Brush.verticalGradient( + listOf(navBarBlackTop, navBarBlackBottom) + ) + + fun buttonSilverBrush(isPressed: Boolean = false): Brush { + return if (!isPressed) { + Brush.verticalGradient( + listOf(buttonSilverTop, buttonSilverMid, buttonSilverBottom) + ) + } else { + Brush.verticalGradient( + listOf(buttonSilverBottom, buttonSilverMid, buttonSilverTop) + ) + } + } + + fun buttonBlueBrush(isPressed: Boolean = false): Brush { + return if (!isPressed) { + Brush.verticalGradient( + listOf(buttonBlueTop, buttonBlueMid, buttonBlueBottom) + ) + } else { + Brush.verticalGradient( + listOf(buttonBlueBottom, buttonBlueMid, buttonBlueTop) + ) + } + } + + fun buttonRedBrush(isPressed: Boolean = false): Brush { + return if (!isPressed) { + Brush.verticalGradient( + listOf(buttonRedTop, buttonRedMid, buttonRedBottom) + ) + } else { + Brush.verticalGradient( + listOf(buttonRedBottom, buttonRedMid, buttonRedTop) + ) + } + } + + fun switchOnBrush(): Brush = Brush.verticalGradient( + listOf(switchOnTop, switchOnBottom) + ) + + fun switchOffBrush(): Brush = Brush.verticalGradient( + listOf(switchOffTop, switchOffBottom) + ) +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/theme/PwnappleTheme.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/theme/PwnappleTheme.kt new file mode 100644 index 0000000..cd37bfa --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/theme/PwnappleTheme.kt @@ -0,0 +1,46 @@ +package ai.heretek.pwnapple.theme + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf + +val LocalPwnappleColors = staticCompositionLocalOf { PwnappleColors() } +val LocalPwnappleTypography = staticCompositionLocalOf { PwnappleTypography() } + +@Composable +fun PwnappleTheme( + colors: PwnappleColors = PwnappleTheme.colors, + typography: PwnappleTypography = PwnappleTheme.typography, + soundFeedbackEnabled: Boolean = true, + content: @Composable () -> Unit +) { + val soundFeedback = remember(soundFeedbackEnabled) { + PwnappleAudioFeedback(enabled = soundFeedbackEnabled) + } + + CompositionLocalProvider( + LocalPwnappleColors provides colors, + LocalPwnappleTypography provides typography, + LocalPwnappleSoundFeedback provides soundFeedback, + content = content + ) +} + +object PwnappleTheme { + val colors: PwnappleColors + @Composable + @ReadOnlyComposable + get() = LocalPwnappleColors.current + + val typography: PwnappleTypography + @Composable + @ReadOnlyComposable + get() = LocalPwnappleTypography.current + + val sound: PwnappleAudioFeedback + @Composable + @ReadOnlyComposable + get() = LocalPwnappleSoundFeedback.current +} diff --git a/pwnapple/src/main/java/ai/heretek/pwnapple/theme/PwnappleTypography.kt b/pwnapple/src/main/java/ai/heretek/pwnapple/theme/PwnappleTypography.kt new file mode 100644 index 0000000..0401163 --- /dev/null +++ b/pwnapple/src/main/java/ai/heretek/pwnapple/theme/PwnappleTypography.kt @@ -0,0 +1,109 @@ +package ai.heretek.pwnapple.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +@Immutable +data class PwnappleTypography( + val navTitle: TextStyle = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 19.sp, + color = Color.White, + shadow = Shadow( + color = Color.Black.copy(alpha = 0.65f), + offset = Offset(0f, -2f), + blurRadius = 0f + ) + ), + val sectionHeader: TextStyle = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 17.sp, + color = Color(0xFF4C566C), + shadow = Shadow( + color = Color.White.copy(alpha = 0.9f), + offset = Offset(0f, 2f), + blurRadius = 0f + ) + ), + val sectionFooter: TextStyle = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + color = Color(0xFF4C566C), + shadow = Shadow( + color = Color.White.copy(alpha = 0.9f), + offset = Offset(0f, 2f), + blurRadius = 0f + ) + ), + val cellTitle: TextStyle = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 17.sp, + color = Color.Black + ), + val cellSubtitle: TextStyle = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 15.sp, + color = Color(0xFF385487) + ), + val buttonSilverText: TextStyle = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + color = Color(0xFF222222), + shadow = Shadow( + color = Color.White.copy(alpha = 0.8f), + offset = Offset(0f, 2f), + blurRadius = 0f + ) + ), + val buttonColorText: TextStyle = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + color = Color.White, + shadow = Shadow( + color = Color.Black.copy(alpha = 0.6f), + offset = Offset(0f, -2f), + blurRadius = 0f + ) + ), + val tabBarLabel: TextStyle = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 11.sp, + color = Color(0xFF919191) + ), + val badgeText: TextStyle = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 12.sp, + color = Color.White, + shadow = Shadow( + color = Color.Black.copy(alpha = 0.6f), + offset = Offset(0f, -1.5f), + blurRadius = 0f + ) + ), + val switchLabel: TextStyle = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 14.sp, + color = Color.White, + shadow = Shadow( + color = Color.Black.copy(alpha = 0.4f), + offset = Offset(0f, -1.5f), + blurRadius = 0f + ) + ) +) diff --git a/pwnapple/src/main/res/values/strings.xml b/pwnapple/src/main/res/values/strings.xml new file mode 100644 index 0000000..d2a64a7 --- /dev/null +++ b/pwnapple/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Pwnapple + diff --git a/pwnapple/src/test/java/ai/heretek/pwnapple/PwnappleCoreTest.kt b/pwnapple/src/test/java/ai/heretek/pwnapple/PwnappleCoreTest.kt new file mode 100644 index 0000000..9ef1b95 --- /dev/null +++ b/pwnapple/src/test/java/ai/heretek/pwnapple/PwnappleCoreTest.kt @@ -0,0 +1,39 @@ +package ai.heretek.pwnapple + +import ai.heretek.pwnapple.graphics.ArrowBackShape +import ai.heretek.pwnapple.theme.PwnappleColors +import ai.heretek.pwnapple.theme.PwnappleTypography +import androidx.compose.ui.unit.dp +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class PwnappleCoreTest { + + @Test + fun testColorsInitialization() { + val colors = PwnappleColors() + assertNotNull(colors.navBarBlueTop) + assertNotNull(colors.buttonSilverTop) + assertNotNull(colors.switchOnTop) + assertNotNull(colors.cardBackground) + assertNotNull(colors.navBarBlueBrush()) + assertNotNull(colors.buttonSilverBrush(false)) + assertNotNull(colors.buttonSilverBrush(true)) + } + + @Test + fun testTypographyDefaults() { + val typography = PwnappleTypography() + assertNotNull(typography.navTitle.shadow) + assertNotNull(typography.sectionHeader.shadow) + assertNotNull(typography.buttonSilverText.shadow) + assertTrue(typography.navTitle.fontSize.value > 15f) + } + + @Test + fun testArrowBackShapeCreation() { + val shape = ArrowBackShape(arrowWidth = 14.dp, cornerRadius = 4.dp) + assertNotNull(shape) + } +} diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts new file mode 100644 index 0000000..68a0905 --- /dev/null +++ b/sample/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + alias(libs.plugins.androidApplication) + alias(libs.plugins.jetbrainsKotlinAndroid) + alias(libs.plugins.compose.compiler) +} + +android { + namespace = "ai.heretek.pwnapple.sample" + compileSdk = 35 + + defaultConfig { + applicationId = "ai.heretek.pwnapple.sample" + minSdk = 24 + targetSdk = 35 + versionCode = 1 + versionName = "1.0.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + kotlinOptions { + jvmTarget = "21" + } + + buildFeatures { + compose = true + } +} + +dependencies { + implementation(project(":pwnapple")) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.navigation.compose) + + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material.icons.core) + implementation(libs.androidx.compose.material.icons.extended) + + debugImplementation(libs.androidx.compose.ui.tooling) +} diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5249ee8 --- /dev/null +++ b/sample/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/sample/src/main/java/ai/heretek/pwnapple/sample/MainActivity.kt b/sample/src/main/java/ai/heretek/pwnapple/sample/MainActivity.kt new file mode 100644 index 0000000..20bc39b --- /dev/null +++ b/sample/src/main/java/ai/heretek/pwnapple/sample/MainActivity.kt @@ -0,0 +1,74 @@ +package ai.heretek.pwnapple.sample + +import ai.heretek.pwnapple.components.tabs.IosTabBar +import ai.heretek.pwnapple.components.tabs.IosTabItem +import ai.heretek.pwnapple.sample.screens.AlertsScreen +import ai.heretek.pwnapple.sample.screens.ControlsScreen +import ai.heretek.pwnapple.sample.screens.PickersScreen +import ai.heretek.pwnapple.sample.screens.SettingsScreen +import ai.heretek.pwnapple.theme.PwnappleTheme +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DateRange +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Widgets +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + PwnappleTheme { + PwnappleShowcaseApp() + } + } + } +} + +@Composable +fun PwnappleShowcaseApp() { + var selectedTab by remember { mutableIntStateOf(0) } + + val tabItems = remember { + listOf( + IosTabItem(title = "Settings", icon = Icons.Default.Settings), + IosTabItem(title = "Controls", icon = Icons.Default.Widgets), + IosTabItem(title = "Pickers", icon = Icons.Default.DateRange), + IosTabItem(title = "Alerts", icon = Icons.Default.Notifications, badge = "1") + ) + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFFC5CCD4)) + ) { + Box(modifier = Modifier.weight(1f)) { + when (selectedTab) { + 0 -> SettingsScreen() + 1 -> ControlsScreen() + 2 -> PickersScreen() + 3 -> AlertsScreen() + } + } + + IosTabBar( + items = tabItems, + selectedIndex = selectedTab, + onTabSelected = { selectedTab = it } + ) + } +} diff --git a/sample/src/main/java/ai/heretek/pwnapple/sample/screens/AlertsScreen.kt b/sample/src/main/java/ai/heretek/pwnapple/sample/screens/AlertsScreen.kt new file mode 100644 index 0000000..4537d6c --- /dev/null +++ b/sample/src/main/java/ai/heretek/pwnapple/sample/screens/AlertsScreen.kt @@ -0,0 +1,122 @@ +package ai.heretek.pwnapple.sample.screens + +import ai.heretek.pwnapple.components.button.IosButton +import ai.heretek.pwnapple.components.button.IosButtonStyle +import ai.heretek.pwnapple.components.dialog.IosActionSheet +import ai.heretek.pwnapple.components.dialog.IosAlert +import ai.heretek.pwnapple.components.navigation.IosNavigationBar +import ai.heretek.pwnapple.components.table.IosTableSection +import ai.heretek.pwnapple.graphics.pinstripeBackground +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun AlertsScreen() { + var showSimpleAlert by remember { mutableStateOf(false) } + var showChoiceAlert by remember { mutableStateOf(false) } + var showActionSheet by remember { mutableStateOf(false) } + val scrollState = rememberScrollState() + + Box(modifier = Modifier.fillMaxSize()) { + Column(modifier = Modifier.fillMaxSize()) { + IosNavigationBar(title = "Alerts & Sheets") + + Column( + modifier = Modifier + .fillMaxSize() + .pinstripeBackground() + .verticalScroll(scrollState) + .padding(vertical = 12.dp) + ) { + IosTableSection( + headerText = "MODAL DIALOGS", + footerText = "Classic translucent deep blue alerts with silver bevel and 3D specular sheen." + ) { + Column( + modifier = Modifier.padding(14.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + IosButton( + text = "Show Single-Button Alert", + onClick = { showSimpleAlert = true }, + style = IosButtonStyle.Blue, + modifier = Modifier.fillMaxWidth() + ) + + IosButton( + text = "Show Two-Button Alert", + onClick = { showChoiceAlert = true }, + style = IosButtonStyle.Silver, + modifier = Modifier.fillMaxWidth() + ) + } + } + + IosTableSection( + headerText = "ACTION SHEETS", + footerText = "Dark linen textured bottom sheet with destructive and cancel styling." + ) { + Column(modifier = Modifier.padding(14.dp)) { + IosButton( + text = "Trigger Action Sheet", + onClick = { showActionSheet = true }, + style = IosButtonStyle.Red, + modifier = Modifier.fillMaxWidth() + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + } + } + + // Active Dialogs / Overlays + if (showSimpleAlert) { + IosAlert( + title = "Cannot Connect to iTunes Store", + message = "An error occurred while connecting to the iTunes Store. Please check your network connection and try again.", + onDismissRequest = { showSimpleAlert = false }, + confirmButtonText = "OK" + ) + } + + if (showChoiceAlert) { + IosAlert( + title = "Restore Network Settings", + message = "This will reset all network settings, returning them to factory defaults. Continue?", + onDismissRequest = { showChoiceAlert = false }, + dismissButtonText = "Cancel", + confirmButtonText = "Reset" + ) + } + + if (showActionSheet) { + IosActionSheet( + title = "Are you sure you want to proceed?", + destructiveText = "Delete All Content", + onDestructiveClick = { showActionSheet = false }, + actionButtons = listOf( + "Back Up to iCloud" to { showActionSheet = false }, + "Archive Locally" to { showActionSheet = false } + ), + cancelText = "Cancel", + onDismissRequest = { showActionSheet = false } + ) + } + } +} diff --git a/sample/src/main/java/ai/heretek/pwnapple/sample/screens/ControlsScreen.kt b/sample/src/main/java/ai/heretek/pwnapple/sample/screens/ControlsScreen.kt new file mode 100644 index 0000000..5a47f1f --- /dev/null +++ b/sample/src/main/java/ai/heretek/pwnapple/sample/screens/ControlsScreen.kt @@ -0,0 +1,178 @@ +package ai.heretek.pwnapple.sample.screens + +import ai.heretek.pwnapple.components.button.IosButton +import ai.heretek.pwnapple.components.button.IosButtonSize +import ai.heretek.pwnapple.components.button.IosButtonStyle +import ai.heretek.pwnapple.components.button.IosSegmentedControl +import ai.heretek.pwnapple.components.indicator.IosActivityIndicator +import ai.heretek.pwnapple.components.indicator.IosProgressBar +import ai.heretek.pwnapple.components.input.IosSlider +import ai.heretek.pwnapple.components.input.IosTextField +import ai.heretek.pwnapple.components.navigation.IosNavigationBar +import ai.heretek.pwnapple.components.table.IosGroupedTable +import ai.heretek.pwnapple.components.table.IosTableSection +import ai.heretek.pwnapple.graphics.pinstripeBackground +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun ControlsScreen() { + val scrollState = rememberScrollState() + var selectedSegment by remember { mutableIntStateOf(0) } + var textInput by remember { mutableStateOf("Steve Jobs") } + var searchInput by remember { mutableStateOf("") } + var progressVal by remember { mutableFloatStateOf(0.65f) } + + Column(modifier = Modifier.fillMaxSize()) { + IosNavigationBar(title = "Controls") + + Column( + modifier = Modifier + .fillMaxSize() + .pinstripeBackground() + .verticalScroll(scrollState) + .padding(vertical = 12.dp) + ) { + // Segmented Control Section + IosTableSection(headerText = "SEGMENTED CONTROLS") { + Box(modifier = Modifier.padding(14.dp)) { + IosSegmentedControl( + items = listOf("Map", "Satellite", "Hybrid"), + selectedIndex = selectedSegment, + onItemSelected = { selectedSegment = it } + ) + } + } + + // Buttons Section + IosTableSection(headerText = "SKEUOMORPHIC BUTTONS") { + Column( + modifier = Modifier.padding(14.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + IosButton( + text = "Standard Silver Button", + onClick = {}, + style = IosButtonStyle.Silver, + modifier = Modifier.fillMaxWidth() + ) + IosButton( + text = "Action Blue Button", + onClick = {}, + style = IosButtonStyle.Blue, + modifier = Modifier.fillMaxWidth() + ) + IosButton( + text = "Destructive Red Button", + onClick = {}, + style = IosButtonStyle.Red, + modifier = Modifier.fillMaxWidth() + ) + IosButton( + text = "Dark Slate Button", + onClick = {}, + style = IosButtonStyle.Black, + modifier = Modifier.fillMaxWidth() + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + IosButton( + text = "Small Silver", + onClick = {}, + style = IosButtonStyle.Silver, + size = IosButtonSize.Small, + modifier = Modifier.weight(1f) + ) + IosButton( + text = "Small Blue", + onClick = {}, + style = IosButtonStyle.Blue, + size = IosButtonSize.Small, + modifier = Modifier.weight(1f) + ) + } + } + } + + // Text Inputs Section + IosTableSection(headerText = "TEXT FIELDS") { + Column( + modifier = Modifier.padding(14.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + IosTextField( + value = textInput, + onValueChange = { textInput = it }, + placeholder = "Name" + ) + + IosTextField( + value = searchInput, + onValueChange = { searchInput = it }, + placeholder = "Search Contacts", + leadingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = "Search", + tint = Color(0xFF7F7F7F) + ) + } + ) + } + } + + // Progress & Indicators + IosTableSection(headerText = "PROGRESS & SPINNERS") { + Column( + modifier = Modifier.padding(14.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + IosProgressBar(progress = progressVal) + + IosSlider( + value = progressVal, + onValueChange = { progressVal = it } + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + IosActivityIndicator(size = 22.dp) + IosActivityIndicator(size = 32.dp) + IosActivityIndicator(size = 42.dp) + } + } + } + + Spacer(modifier = Modifier.height(24.dp)) + } + } +} diff --git a/sample/src/main/java/ai/heretek/pwnapple/sample/screens/PickersScreen.kt b/sample/src/main/java/ai/heretek/pwnapple/sample/screens/PickersScreen.kt new file mode 100644 index 0000000..1a0f23f --- /dev/null +++ b/sample/src/main/java/ai/heretek/pwnapple/sample/screens/PickersScreen.kt @@ -0,0 +1,92 @@ +package ai.heretek.pwnapple.sample.screens + +import ai.heretek.pwnapple.components.navigation.IosNavigationBar +import ai.heretek.pwnapple.components.picker.IosPickerWheel +import ai.heretek.pwnapple.components.table.IosTableCell +import ai.heretek.pwnapple.components.table.IosTableSection +import ai.heretek.pwnapple.graphics.pinstripeBackground +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun PickersScreen() { + val devices = listOf( + "iPhone 3GS", + "iPhone 4", + "iPhone 4S", + "iPhone 5", + "iPod touch (4th gen)", + "iPod touch (5th gen)", + "iPad (3rd gen)", + "iPad (4th gen)", + "iPad mini" + ) + + val months = listOf( + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ) + + var selectedDeviceIndex by remember { mutableIntStateOf(3) } + var selectedMonthIndex by remember { mutableIntStateOf(8) } + val scrollState = rememberScrollState() + + Column(modifier = Modifier.fillMaxSize()) { + IosNavigationBar(title = "Pickers") + + Column( + modifier = Modifier + .fillMaxSize() + .pinstripeBackground() + .verticalScroll(scrollState) + .padding(vertical = 12.dp) + ) { + IosTableSection(headerText = "SELECTED VALUE") { + IosTableCell( + title = "Target Device", + detailText = devices[selectedDeviceIndex] + ) + IosTableCell( + title = "Release Month", + detailText = months[selectedMonthIndex], + showDivider = false + ) + } + + IosTableSection(headerText = "DEVICE DRUM WHEEL") { + Box(modifier = Modifier.padding(14.dp)) { + IosPickerWheel( + items = devices, + selectedIndex = selectedDeviceIndex, + onSelectedIndexChange = { selectedDeviceIndex = it } + ) + } + } + + IosTableSection(headerText = "MONTH DRUM WHEEL") { + Box(modifier = Modifier.padding(14.dp)) { + IosPickerWheel( + items = months, + selectedIndex = selectedMonthIndex, + onSelectedIndexChange = { selectedMonthIndex = it } + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + } + } +} diff --git a/sample/src/main/java/ai/heretek/pwnapple/sample/screens/SettingsScreen.kt b/sample/src/main/java/ai/heretek/pwnapple/sample/screens/SettingsScreen.kt new file mode 100644 index 0000000..49f6459 --- /dev/null +++ b/sample/src/main/java/ai/heretek/pwnapple/sample/screens/SettingsScreen.kt @@ -0,0 +1,327 @@ +package ai.heretek.pwnapple.sample.screens + +import ai.heretek.pwnapple.components.input.IosSlider +import ai.heretek.pwnapple.components.input.IosSwitch +import ai.heretek.pwnapple.components.navigation.IosBackButton +import ai.heretek.pwnapple.components.navigation.IosNavigationBar +import ai.heretek.pwnapple.components.table.IosCellAccessory +import ai.heretek.pwnapple.components.table.IosTableCell +import ai.heretek.pwnapple.components.table.IosTableSection +import ai.heretek.pwnapple.graphics.pinstripeBackground +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.VolumeUp +import androidx.compose.material.icons.filled.AirplanemodeActive +import androidx.compose.material.icons.filled.Bluetooth +import androidx.compose.material.icons.filled.DoNotDisturb +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Wallpaper +import androidx.compose.material.icons.filled.Wifi +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +enum class SettingsSubscreen { + Main, + Wifi, + Sounds, + About +} + +@Composable +fun SettingsScreen() { + var subscreen by remember { mutableStateOf(SettingsSubscreen.Main) } + + when (subscreen) { + SettingsSubscreen.Main -> MainSettings( + onNavigateWifi = { subscreen = SettingsSubscreen.Wifi }, + onNavigateSounds = { subscreen = SettingsSubscreen.Sounds }, + onNavigateAbout = { subscreen = SettingsSubscreen.About } + ) + SettingsSubscreen.Wifi -> WifiSubscreen(onBack = { subscreen = SettingsSubscreen.Main }) + SettingsSubscreen.Sounds -> SoundsSubscreen(onBack = { subscreen = SettingsSubscreen.Main }) + SettingsSubscreen.About -> AboutSubscreen(onBack = { subscreen = SettingsSubscreen.Main }) + } +} + +@Composable +private fun MainSettings( + onNavigateWifi: () -> Unit, + onNavigateSounds: () -> Unit, + onNavigateAbout: () -> Unit +) { + var airplaneMode by remember { mutableStateOf(false) } + var doNotDisturb by remember { mutableStateOf(false) } + val scrollState = rememberScrollState() + + Column(modifier = Modifier.fillMaxSize()) { + IosNavigationBar(title = "Settings") + + Column( + modifier = Modifier + .fillMaxSize() + .pinstripeBackground() + .verticalScroll(scrollState) + .padding(vertical = 12.dp) + ) { + // Connectivity Group + IosTableSection { + IosTableCell( + title = "Airplane Mode", + leadingContent = { SettingsIcon(Icons.Default.AirplanemodeActive, Color(0xFFF28F16)) }, + accessoryContent = { + IosSwitch( + checked = airplaneMode, + onCheckedChange = { airplaneMode = it } + ) + } + ) + IosTableCell( + title = "Wi-Fi", + detailText = "Heretek_5G", + leadingContent = { SettingsIcon(Icons.Default.Wifi, Color(0xFF007AFE)) }, + accessory = IosCellAccessory.DisclosureIndicator, + onClick = onNavigateWifi + ) + IosTableCell( + title = "Bluetooth", + detailText = "On", + leadingContent = { SettingsIcon(Icons.Default.Bluetooth, Color(0xFF007AFE)) }, + accessory = IosCellAccessory.DisclosureIndicator, + onClick = {} + ) + IosTableCell( + title = "Do Not Disturb", + leadingContent = { SettingsIcon(Icons.Default.DoNotDisturb, Color(0xFF5756CE)) }, + accessoryContent = { + IosSwitch( + checked = doNotDisturb, + onCheckedChange = { doNotDisturb = it } + ) + }, + showDivider = false + ) + } + + // General & Preferences Group + IosTableSection( + footerText = "When Do Not Disturb is enabled, calls and alerts that arrive while locked will be silenced." + ) { + IosTableCell( + title = "Notifications", + leadingContent = { SettingsIcon(Icons.Default.Notifications, Color(0xFFFD3B30)) }, + accessory = IosCellAccessory.DisclosureIndicator, + onClick = {} + ) + IosTableCell( + title = "General", + leadingContent = { SettingsIcon(Icons.Default.Settings, Color(0xFF8E8E93)) }, + accessory = IosCellAccessory.DisclosureIndicator, + onClick = onNavigateAbout + ) + IosTableCell( + title = "Sounds", + leadingContent = { SettingsIcon(Icons.AutoMirrored.Filled.VolumeUp, Color(0xFFFD3B30)) }, + accessory = IosCellAccessory.DisclosureIndicator, + onClick = onNavigateSounds + ) + IosTableCell( + title = "Brightness & Wallpaper", + leadingContent = { SettingsIcon(Icons.Default.Wallpaper, Color(0xFF007AFE)) }, + accessory = IosCellAccessory.DisclosureIndicator, + showDivider = false, + onClick = {} + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + } + } +} + +@Composable +private fun WifiSubscreen(onBack: () -> Unit) { + var wifiEnabled by remember { mutableStateOf(true) } + var selectedNetwork by remember { mutableStateOf("Heretek_5G") } + val networks = listOf("Heretek_5G", "Cupertino_Guest", "Pwnapple_Lab", "Infinite_Loop") + + Column(modifier = Modifier.fillMaxSize()) { + IosNavigationBar( + title = "Wi-Fi", + navigationIcon = { IosBackButton(text = "Settings", onClick = onBack) } + ) + + Column( + modifier = Modifier + .fillMaxSize() + .pinstripeBackground() + .padding(vertical = 12.dp) + ) { + IosTableSection { + IosTableCell( + title = "Wi-Fi", + accessoryContent = { + IosSwitch( + checked = wifiEnabled, + onCheckedChange = { wifiEnabled = it } + ) + }, + showDivider = false + ) + } + + if (wifiEnabled) { + IosTableSection(headerText = "CHOOSE A NETWORK...") { + networks.forEachIndexed { index, net -> + val isCurrent = net == selectedNetwork + IosTableCell( + title = net, + accessory = if (isCurrent) IosCellAccessory.Checkmark else IosCellAccessory.None, + showDivider = index < networks.lastIndex, + onClick = { selectedNetwork = net } + ) + } + } + } + } + } +} + +@Composable +private fun SoundsSubscreen(onBack: () -> Unit) { + var ringerVolume by remember { mutableFloatStateOf(0.75f) } + var vibrateOnRing by remember { mutableStateOf(true) } + var vibrateOnSilent by remember { mutableStateOf(true) } + + Column(modifier = Modifier.fillMaxSize()) { + IosNavigationBar( + title = "Sounds", + navigationIcon = { IosBackButton(text = "Settings", onClick = onBack) } + ) + + Column( + modifier = Modifier + .fillMaxSize() + .pinstripeBackground() + .padding(vertical = 12.dp) + ) { + IosTableSection(headerText = "VIBRATE") { + IosTableCell( + title = "Vibrate on Ring", + accessoryContent = { + IosSwitch(checked = vibrateOnRing, onCheckedChange = { vibrateOnRing = it }) + } + ) + IosTableCell( + title = "Vibrate on Silent", + accessoryContent = { + IosSwitch(checked = vibrateOnSilent, onCheckedChange = { vibrateOnSilent = it }) + }, + showDivider = false + ) + } + + IosTableSection( + headerText = "RINGER AND ALERTS", + footerText = "Adjust ringer volume and notification sound levels." + ) { + Box(modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp)) { + IosSlider( + value = ringerVolume, + onValueChange = { ringerVolume = it } + ) + } + } + + IosTableSection(headerText = "SOUNDS") { + IosTableCell( + title = "Ringtone", + detailText = "Marimba", + accessory = IosCellAccessory.DisclosureIndicator, + onClick = {} + ) + IosTableCell( + title = "Text Tone", + detailText = "Tri-tone", + accessory = IosCellAccessory.DisclosureIndicator, + showDivider = false, + onClick = {} + ) + } + } + } +} + +@Composable +private fun AboutSubscreen(onBack: () -> Unit) { + Column(modifier = Modifier.fillMaxSize()) { + IosNavigationBar( + title = "About", + navigationIcon = { IosBackButton(text = "General", onClick = onBack) } + ) + + Column( + modifier = Modifier + .fillMaxSize() + .pinstripeBackground() + .padding(vertical = 12.dp) + ) { + IosTableSection { + IosTableCell(title = "Name", detailText = "Pwnapple Phone") + IosTableCell(title = "Network", detailText = "Heretek Wireless") + IosTableCell(title = "Songs", detailText = "1,337") + IosTableCell(title = "Videos", detailText = "42") + IosTableCell(title = "Photos", detailText = "856") + IosTableCell(title = "Capacity", detailText = "64 GB") + IosTableCell(title = "Available", detailText = "48.2 GB") + IosTableCell(title = "Version", detailText = "6.1.6 (10B500)") + IosTableCell(title = "Model", detailText = "MD298LL/A", showDivider = false) + } + } + } +} + +@Composable +private fun SettingsIcon(icon: ImageVector, tint: Color) { + Box( + modifier = Modifier + .size(28.dp) + .clip(RoundedCornerShape(6.dp)) + .background( + Brush.verticalGradient( + listOf(tint, tint.copy(alpha = 0.82f)) + ) + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(18.dp) + ) + } +} diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml new file mode 100644 index 0000000..d2a64a7 --- /dev/null +++ b/sample/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Pwnapple + diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..8b26d4e --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,24 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "pwnapple" +include(":pwnapple") +include(":sample")