From 5cdc1c1142910c784fede6974c175f7011844ae7 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 17 May 2026 17:43:16 +0200 Subject: [PATCH] Add Calorie AI Android app --- .forgejo/workflows/android.yml | 51 ++ .gitignore | 6 + README.md | 37 ++ app/build.gradle | 16 + app/src/main/AndroidManifest.xml | 18 + .../com/danvics/calorieai/MainActivity.java | 569 ++++++++++++++++++ app/src/main/res/values/styles.xml | 10 + build.gradle | 3 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 32944 bytes settings.gradle | 18 + 10 files changed, 728 insertions(+) create mode 100644 .forgejo/workflows/android.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/build.gradle create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/danvics/calorieai/MainActivity.java create mode 100644 app/src/main/res/values/styles.xml create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 settings.gradle diff --git a/.forgejo/workflows/android.yml b/.forgejo/workflows/android.yml new file mode 100644 index 0000000..0bf0bee --- /dev/null +++ b/.forgejo/workflows/android.yml @@ -0,0 +1,51 @@ +name: Android CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + env: + ANDROID_HOME: /opt/android-sdk + ANDROID_SDK_ROOT: /opt/android-sdk + GRADLE_VERSION: 8.10.2 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Install build dependencies + run: | + apt-get update + apt-get install -y --no-install-recommends ca-certificates curl unzip openjdk-17-jdk + + - name: Install Android SDK + run: | + mkdir -p "$ANDROID_HOME/cmdline-tools" + curl -fsSL https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -o /tmp/android-tools.zip + unzip -q /tmp/android-tools.zip -d /tmp/android-tools + mv /tmp/android-tools/cmdline-tools "$ANDROID_HOME/cmdline-tools/latest" + yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses >/dev/null + "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" \ + "platform-tools" \ + "platforms;android-35" \ + "build-tools;35.0.0" + + - name: Install Gradle + run: | + curl -fsSL "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" -o /tmp/gradle.zip + unzip -q /tmp/gradle.zip -d /opt + ln -s "/opt/gradle-${GRADLE_VERSION}/bin/gradle" /usr/local/bin/gradle + + - name: Build debug APK + run: gradle --no-daemon :app:assembleDebug + + - name: Upload debug APK + uses: actions/upload-artifact@v4 + with: + name: calorie-ai-debug-apk + path: app/build/outputs/apk/debug/app-debug.apk diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e9ebd5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.gradle/ +build/ +app/build/ +local.properties +*.iml +.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..adf8400 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# Calorie AI Android + +Native Android calorie tracker with optional meal images and admin-configurable AI models. + +## Features + +- Add meals by description, portion estimate, and optional image. +- Analyze meals through OpenAI-compatible chat completions. +- Uses a vision model for image interpretation when an image is attached. +- Uses a tasking model to estimate calories, macros, and notes. +- Stores daily meal entries locally on device. +- Admin settings control API base URL, API key, vision model, and tasking model. The two model fields can contain the same model name. + +## Build + +Open this folder in Android Studio and run the `app` configuration, or build with Gradle if available: + +```bash +gradle :app:assembleDebug +``` + +Forgejo Actions builds the debug APK on every push to `main` and uploads it as the `calorie-ai-debug-apk` artifact. + +## AI Endpoint + +The app expects an OpenAI-compatible endpoint at: + +```text +{API base URL}/chat/completions +``` + +Example base URLs: + +- `https://api.openai.com/v1` +- An emulator host-loopback URL ending in `:11434/v1` for a local OpenAI-compatible service + +Default admin PIN is `admin`. Change it in the Admin AI Settings panel after first launch. diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..528c031 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,16 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'com.danvics.calorieai' + compileSdk 35 + + defaultConfig { + applicationId 'com.danvics.calorieai' + minSdk 26 + targetSdk 35 + versionCode 1 + versionName '1.0' + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..da1c1ca --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/app/src/main/java/com/danvics/calorieai/MainActivity.java b/app/src/main/java/com/danvics/calorieai/MainActivity.java new file mode 100644 index 0000000..d01d1d9 --- /dev/null +++ b/app/src/main/java/com/danvics/calorieai/MainActivity.java @@ -0,0 +1,569 @@ +package com.danvics.calorieai; + +import android.app.Activity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.GradientDrawable; +import android.net.Uri; +import android.os.Bundle; +import android.provider.OpenableColumns; +import android.text.InputType; +import android.util.Base64; +import android.view.Gravity; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; +import android.database.Cursor; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +public class MainActivity extends Activity { + private static final int PICK_IMAGE_REQUEST = 11; + + private SharedPreferences prefs; + private LinearLayout root; + private EditText descriptionInput; + private EditText measureInput; + private TextView selectedImageText; + private TextView statusText; + private Button analyzeButton; + private Uri selectedImageUri; + private String selectedImageDataUri; + private String selectedImageName; + private boolean adminUnlocked; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + prefs = getSharedPreferences("calorie_ai", MODE_PRIVATE); + adminUnlocked = prefs.getBoolean("admin_unlocked", false); + buildScreen(); + } + + private void buildScreen() { + ScrollView scrollView = new ScrollView(this); + scrollView.setBackgroundColor(Color.rgb(248, 244, 236)); + + root = new LinearLayout(this); + root.setOrientation(LinearLayout.VERTICAL); + root.setPadding(dp(18), dp(24), dp(18), dp(24)); + scrollView.addView(root); + + TextView title = text("Calorie AI", 30, true); + title.setTextColor(Color.rgb(38, 55, 45)); + root.addView(title); + + TextView subtitle = text("Track what you eat each day. Add a description, portion estimate, and optional image for AI calorie analysis.", 15, false); + subtitle.setTextColor(Color.rgb(91, 99, 92)); + subtitle.setPadding(0, dp(6), 0, dp(18)); + root.addView(subtitle); + + addTodaySummary(); + addMealForm(); + addEntries(); + addAdminSettings(); + + setContentView(scrollView); + } + + private void addTodaySummary() { + JSONArray todayEntries = entriesForDate(todayKey()); + int calories = 0; + int protein = 0; + int carbs = 0; + int fat = 0; + + for (int i = 0; i < todayEntries.length(); i++) { + JSONObject entry = todayEntries.optJSONObject(i); + if (entry == null) continue; + calories += entry.optInt("calories"); + protein += entry.optInt("proteinGrams"); + carbs += entry.optInt("carbsGrams"); + fat += entry.optInt("fatGrams"); + } + + LinearLayout card = card(); + TextView heading = text("Today", 18, true); + card.addView(heading); + card.addView(text(todayKey() + " | " + todayEntries.length() + " meals", 13, false)); + + TextView totals = text(calories + " kcal", 34, true); + totals.setTextColor(Color.rgb(47, 125, 89)); + totals.setPadding(0, dp(8), 0, dp(2)); + card.addView(totals); + card.addView(text("Protein " + protein + "g | Carbs " + carbs + "g | Fat " + fat + "g", 14, false)); + root.addView(card); + } + + private void addMealForm() { + LinearLayout card = card(); + card.addView(text("Add Meal", 20, true)); + + descriptionInput = input("What did you eat? Example: chicken rice bowl with avocado"); + descriptionInput.setMinLines(3); + descriptionInput.setGravity(Gravity.TOP); + card.addView(descriptionInput); + + measureInput = input("Portion or measure. Example: 450g, 1 large bowl, 2 slices"); + card.addView(measureInput); + + Button imageButton = button("Choose optional image"); + imageButton.setOnClickListener(v -> pickImage()); + card.addView(imageButton); + + selectedImageText = text("No image selected", 13, false); + selectedImageText.setTextColor(Color.rgb(91, 99, 92)); + selectedImageText.setPadding(0, dp(6), 0, dp(6)); + card.addView(selectedImageText); + + analyzeButton = button("Analyze and save meal"); + analyzeButton.setOnClickListener(v -> analyzeMeal()); + card.addView(analyzeButton); + + statusText = text("", 13, false); + statusText.setPadding(0, dp(8), 0, 0); + card.addView(statusText); + + root.addView(card); + } + + private void addEntries() { + LinearLayout card = card(); + card.addView(text("Today's Meals", 20, true)); + JSONArray todayEntries = entriesForDate(todayKey()); + + if (todayEntries.length() == 0) { + TextView empty = text("No meals logged today.", 14, false); + empty.setTextColor(Color.rgb(91, 99, 92)); + card.addView(empty); + root.addView(card); + return; + } + + for (int i = todayEntries.length() - 1; i >= 0; i--) { + JSONObject entry = todayEntries.optJSONObject(i); + if (entry == null) continue; + TextView meal = text(entry.optString("time") + " " + entry.optString("mealName", "Meal"), 16, true); + meal.setPadding(0, dp(12), 0, dp(2)); + card.addView(meal); + card.addView(text(entry.optInt("calories") + " kcal | P " + entry.optInt("proteinGrams") + + "g | C " + entry.optInt("carbsGrams") + "g | F " + entry.optInt("fatGrams") + "g", 14, false)); + card.addView(text(entry.optString("description"), 13, false)); + String notes = entry.optString("notes"); + if (!notes.isEmpty()) { + TextView note = text(notes, 13, false); + note.setTextColor(Color.rgb(91, 99, 92)); + card.addView(note); + } + } + root.addView(card); + } + + private void addAdminSettings() { + LinearLayout card = card(); + card.addView(text("Admin AI Settings", 20, true)); + + if (!adminUnlocked) { + EditText pin = input("Admin PIN"); + pin.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + card.addView(pin); + + Button unlock = button("Unlock settings"); + unlock.setOnClickListener(v -> { + String expected = prefs.getString("admin_pin", "admin"); + if (expected.equals(pin.getText().toString())) { + adminUnlocked = true; + prefs.edit().putBoolean("admin_unlocked", true).apply(); + buildScreen(); + } else { + pin.setError("Incorrect PIN"); + } + }); + card.addView(unlock); + card.addView(text("Default PIN: admin", 12, false)); + root.addView(card); + return; + } + + EditText baseUrl = input("API base URL, e.g. https://api.openai.com/v1"); + baseUrl.setText(prefs.getString("api_base_url", "")); + card.addView(baseUrl); + + EditText apiKey = input("API key. Leave blank for local endpoints that do not need auth"); + apiKey.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + apiKey.setText(prefs.getString("api_key", "")); + card.addView(apiKey); + + EditText visionModel = input("Vision model"); + visionModel.setText(prefs.getString("vision_model", "gpt-4o-mini")); + card.addView(visionModel); + + EditText taskModel = input("Tasking model"); + taskModel.setText(prefs.getString("task_model", "gpt-4o-mini")); + card.addView(taskModel); + + EditText newPin = input("New admin PIN, optional"); + newPin.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + card.addView(newPin); + + Button save = button("Save AI settings"); + save.setOnClickListener(v -> { + SharedPreferences.Editor editor = prefs.edit() + .putString("api_base_url", baseUrl.getText().toString().trim()) + .putString("api_key", apiKey.getText().toString().trim()) + .putString("vision_model", visionModel.getText().toString().trim()) + .putString("task_model", taskModel.getText().toString().trim()); + String pin = newPin.getText().toString().trim(); + if (!pin.isEmpty()) editor.putString("admin_pin", pin); + editor.apply(); + save.setText("Saved"); + }); + card.addView(save); + + Button lock = button("Lock admin settings"); + lock.setOnClickListener(v -> { + adminUnlocked = false; + prefs.edit().putBoolean("admin_unlocked", false).apply(); + buildScreen(); + }); + card.addView(lock); + + TextView help = text("The vision model reads attached meal photos. The tasking model estimates calories, macros, and notes from the full meal context. Both fields may use the same model.", 12, false); + help.setTextColor(Color.rgb(91, 99, 92)); + card.addView(help); + root.addView(card); + } + + private void pickImage() { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("image/*"); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + startActivityForResult(intent, PICK_IMAGE_REQUEST); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode != PICK_IMAGE_REQUEST || resultCode != RESULT_OK || data == null) return; + selectedImageUri = data.getData(); + if (selectedImageUri == null) return; + try { + getContentResolver().takePersistableUriPermission(selectedImageUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + } catch (SecurityException ignored) { + // Some document providers grant one-shot read access only; analysis still works in-session. + } + selectedImageName = displayName(selectedImageUri); + selectedImageDataUri = null; + selectedImageText.setText("Selected: " + selectedImageName); + } + + private void analyzeMeal() { + String description = descriptionInput.getText().toString().trim(); + String measure = measureInput.getText().toString().trim(); + String baseUrl = prefs.getString("api_base_url", "").trim(); + String taskModel = prefs.getString("task_model", "").trim(); + + if (description.isEmpty() && selectedImageUri == null) { + status("Describe the meal or attach an image.", true); + return; + } + if (baseUrl.isEmpty() || taskModel.isEmpty()) { + status("Admin must set API base URL and tasking model first.", true); + return; + } + + analyzeButton.setEnabled(false); + status("Analyzing meal...", false); + + new Thread(() -> { + try { + String vision = ""; + if (selectedImageUri != null) { + if (selectedImageDataUri == null) selectedImageDataUri = readImageAsDataUri(selectedImageUri); + String visionModel = prefs.getString("vision_model", "").trim(); + if (!visionModel.isEmpty()) { + vision = requestVisionDescription(baseUrl, visionModel, description, selectedImageDataUri); + } + } + + JSONObject estimate = requestTaskEstimate(baseUrl, taskModel, description, measure, vision); + saveEntry(description, measure, vision, estimate, selectedImageUri != null); + + runOnUiThread(() -> { + selectedImageUri = null; + selectedImageDataUri = null; + selectedImageName = null; + buildScreen(); + }); + } catch (Exception e) { + runOnUiThread(() -> { + analyzeButton.setEnabled(true); + status("AI analysis failed: " + e.getMessage(), true); + }); + } + }).start(); + } + + private String requestVisionDescription(String baseUrl, String model, String description, String imageDataUri) throws Exception { + JSONArray content = new JSONArray(); + content.put(new JSONObject() + .put("type", "text") + .put("text", "Describe this food photo for calorie estimation. Include likely foods, portion sizes, visible quantities, sauces, and uncertainty. User description: " + description)); + content.put(new JSONObject() + .put("type", "image_url") + .put("image_url", new JSONObject().put("url", imageDataUri))); + + JSONArray messages = new JSONArray(); + messages.put(new JSONObject().put("role", "user").put("content", content)); + + JSONObject body = new JSONObject() + .put("model", model) + .put("messages", messages) + .put("temperature", 0.2); + + return chatCompletion(baseUrl, body); + } + + private JSONObject requestTaskEstimate(String baseUrl, String model, String description, String measure, String vision) throws Exception { + String prompt = "You are a nutrition logging assistant. Estimate calories and macros for one meal. " + + "Return only JSON with keys mealName, calories, proteinGrams, carbsGrams, fatGrams, notes. " + + "Use integer grams and calories. Mention uncertainty in notes.\n\n" + + "User description: " + description + "\n" + + "User measure: " + measure + "\n" + + "Vision description: " + vision; + + JSONArray messages = new JSONArray(); + messages.put(new JSONObject().put("role", "system").put("content", "Return strict JSON only.")); + messages.put(new JSONObject().put("role", "user").put("content", prompt)); + + JSONObject body = new JSONObject() + .put("model", model) + .put("messages", messages) + .put("temperature", 0.1); + + String content = chatCompletion(baseUrl, body); + return parseEstimate(content); + } + + private String chatCompletion(String baseUrl, JSONObject body) throws Exception { + String urlString = baseUrl.replaceAll("/+$", "") + "/chat/completions"; + HttpURLConnection connection = (HttpURLConnection) new URL(urlString).openConnection(); + connection.setRequestMethod("POST"); + connection.setConnectTimeout(20000); + connection.setReadTimeout(60000); + connection.setRequestProperty("Content-Type", "application/json"); + + String apiKey = prefs.getString("api_key", "").trim(); + if (!apiKey.isEmpty()) connection.setRequestProperty("Authorization", "Bearer " + apiKey); + + connection.setDoOutput(true); + try (OutputStream output = connection.getOutputStream()) { + output.write(body.toString().getBytes(StandardCharsets.UTF_8)); + } + + int code = connection.getResponseCode(); + InputStream stream = code >= 200 && code < 300 ? connection.getInputStream() : connection.getErrorStream(); + String response = readText(stream); + if (code < 200 || code >= 300) throw new IllegalStateException("HTTP " + code + ": " + response); + + JSONObject json = new JSONObject(response); + return json.getJSONArray("choices") + .getJSONObject(0) + .getJSONObject("message") + .getString("content") + .trim(); + } + + private JSONObject parseEstimate(String content) throws JSONException { + String cleaned = content.trim(); + if (cleaned.startsWith("```")) { + cleaned = cleaned.replaceFirst("^```json", "").replaceFirst("^```", ""); + cleaned = cleaned.replaceFirst("```$", "").trim(); + } + int start = cleaned.indexOf('{'); + int end = cleaned.lastIndexOf('}'); + if (start >= 0 && end > start) cleaned = cleaned.substring(start, end + 1); + + JSONObject estimate = new JSONObject(cleaned); + return new JSONObject() + .put("mealName", estimate.optString("mealName", "Meal")) + .put("calories", Math.max(0, estimate.optInt("calories"))) + .put("proteinGrams", Math.max(0, estimate.optInt("proteinGrams"))) + .put("carbsGrams", Math.max(0, estimate.optInt("carbsGrams"))) + .put("fatGrams", Math.max(0, estimate.optInt("fatGrams"))) + .put("notes", estimate.optString("notes", "")) + .put("raw", content); + } + + private void saveEntry(String description, String measure, String vision, JSONObject estimate, boolean hasImage) throws JSONException { + JSONArray entries = allEntries(); + JSONObject entry = new JSONObject() + .put("date", todayKey()) + .put("time", LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm"))) + .put("description", description) + .put("measure", measure) + .put("imageIncluded", hasImage) + .put("visionDescription", vision) + .put("mealName", estimate.optString("mealName", "Meal")) + .put("calories", estimate.optInt("calories")) + .put("proteinGrams", estimate.optInt("proteinGrams")) + .put("carbsGrams", estimate.optInt("carbsGrams")) + .put("fatGrams", estimate.optInt("fatGrams")) + .put("notes", estimate.optString("notes")) + .put("rawAi", estimate.optString("raw")); + entries.put(entry); + prefs.edit().putString("entries", entries.toString()).apply(); + } + + private JSONArray allEntries() { + try { + return new JSONArray(prefs.getString("entries", "[]")); + } catch (JSONException e) { + return new JSONArray(); + } + } + + private JSONArray entriesForDate(String date) { + JSONArray filtered = new JSONArray(); + JSONArray entries = allEntries(); + for (int i = 0; i < entries.length(); i++) { + JSONObject entry = entries.optJSONObject(i); + if (entry != null && date.equals(entry.optString("date"))) filtered.put(entry); + } + return filtered; + } + + private String readImageAsDataUri(Uri uri) throws Exception { + try (InputStream input = getContentResolver().openInputStream(uri)) { + if (input == null) throw new IllegalStateException("Could not read selected image"); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] data = new byte[8192]; + int read; + while ((read = input.read(data)) != -1) buffer.write(data, 0, read); + String type = getContentResolver().getType(uri); + if (type == null) type = "image/jpeg"; + return "data:" + type + ";base64," + Base64.encodeToString(buffer.toByteArray(), Base64.NO_WRAP); + } + } + + private String displayName(Uri uri) { + try (Cursor cursor = getContentResolver().query(uri, null, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + if (index >= 0) return cursor.getString(index); + } + } catch (Exception ignored) { + } + return uri.getLastPathSegment() == null ? "selected image" : uri.getLastPathSegment(); + } + + private String readText(InputStream input) throws Exception { + if (input == null) return ""; + StringBuilder builder = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) builder.append(line); + } + return builder.toString(); + } + + private String todayKey() { + return LocalDate.now().toString(); + } + + private void status(String message, boolean error) { + statusText.setText(message); + statusText.setTextColor(error ? Color.rgb(164, 52, 52) : Color.rgb(47, 125, 89)); + } + + private LinearLayout card() { + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(dp(16), dp(16), dp(16), dp(16)); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + params.setMargins(0, 0, 0, dp(14)); + layout.setLayoutParams(params); + + GradientDrawable background = new GradientDrawable(); + background.setColor(Color.WHITE); + background.setCornerRadius(dp(18)); + background.setStroke(dp(1), Color.rgb(229, 221, 207)); + layout.setBackground(background); + return layout; + } + + private TextView text(String value, int sp, boolean bold) { + TextView view = new TextView(this); + view.setText(value); + view.setTextSize(sp); + view.setTextColor(Color.rgb(38, 55, 45)); + view.setLineSpacing(0, 1.12f); + if (bold) view.setTypeface(Typeface.DEFAULT, Typeface.BOLD); + return view; + } + + private EditText input(String hint) { + EditText editText = new EditText(this); + editText.setHint(hint); + editText.setTextSize(14); + editText.setSingleLine(false); + editText.setPadding(dp(12), dp(10), dp(12), dp(10)); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + params.setMargins(0, dp(10), 0, 0); + editText.setLayoutParams(params); + + GradientDrawable background = new GradientDrawable(); + background.setColor(Color.rgb(252, 250, 246)); + background.setCornerRadius(dp(12)); + background.setStroke(dp(1), Color.rgb(219, 211, 197)); + editText.setBackground(background); + return editText; + } + + private Button button(String label) { + Button button = new Button(this); + button.setText(label); + button.setTextColor(Color.WHITE); + button.setAllCaps(false); + GradientDrawable background = new GradientDrawable(); + background.setColor(Color.rgb(47, 125, 89)); + background.setCornerRadius(dp(12)); + button.setBackground(background); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + params.setMargins(0, dp(10), 0, 0); + button.setLayoutParams(params); + return button; + } + + private int dp(int value) { + return Math.round(value * getResources().getDisplayMetrics().density); + } +} diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..f3bbbfa --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,10 @@ + + + + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..5b7ccba --- /dev/null +++ b/build.gradle @@ -0,0 +1,3 @@ +plugins { + id 'com.android.application' version '8.7.3' apply false +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..54bc2d6ee93b1b998e1c542c31fc2d8643ee6b41 GIT binary patch literal 32944 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 q&Ofi6zouCKY()WU|CRgSCtOOhkT2IX5D