diff --git a/.github/workflows/drankcounter-release.yml b/.github/workflows/drankcounter-release.yml new file mode 100644 index 00000000..69744682 --- /dev/null +++ b/.github/workflows/drankcounter-release.yml @@ -0,0 +1,40 @@ +name: drankcounter-release + +on: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + +jobs: + drankcounter-release: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install bob + run: cargo install --git https://github.com/bplaat/crates.git bob + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + with: + cmdline-tools-version: 14742923 + - name: Build release + working-directory: bin/drankcounter + run: | + export JAVA_HOME=$JAVA_HOME_21_X64 + export PATH=$JAVA_HOME/bin:$PATH + echo "${{ secrets.DRANKCOUNTER_KEYSTORE }}" | base64 --decode > keystore.jks + bob build --release + - name: Create Git tag and GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: bin/drankcounter + run: | + TAG_VERSION=$(sed -nE 's/^[[:space:]]*version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' bob.toml) + gh release create "drankcounter/v${TAG_VERSION}" \ + --title "DrankCounter v${TAG_VERSION}" \ + --notes "Download the \`.apk\` file and open it to install the application" \ + target/release/drankcounter-${TAG_VERSION}.apk diff --git a/LICENSE b/LICENSE index 908dd7f2..6f129937 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018-2025 Bastiaan van der Plaat +Copyright (c) 2018-2026 Bastiaan van der Plaat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 84a1eff1..e34d0743 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,19 @@ A collection of various Android apps that I created for myself and others + + DrankCounter icon
+ DrankCounter +
+ + + + HackerNews icon
HackerNews
- - NOS icon
@@ -45,12 +51,6 @@ A collection of various Android apps that I created for myself and others
- - ReactTest icon
- ReactTest -
- - RedSquare icon
RedSquare @@ -75,6 +75,7 @@ A collection of various Android apps that I created for myself and others - [BassieTest](bin/bassietest/) A example test app for the bob build tool and sandbox for some ideas - [Bible](bin/bible/) An offline Android Bible app containing multiple bible translations - [CoinList](bin/coinlist/) A cryptocurrency information app similar to the [coinlist](https://github.com/bplaat/coinlist) website +- [DrankCounter](bin/drankcounter/) A basic alcohol tracker app with home screen widget - [HackerNews](bin/hackernews/) A simple [HackerNews](https://news.ycombinator.com/) webview app because installed PWA's suck sadly - [NOS](bin/nos/) A simple sync-once [NOS](https://nos.nl/) feed reader app - [ReactTest](bin/reacttest/) A test app for the [ReactDroid](lib/reactdroid/) library @@ -102,6 +103,6 @@ A collection of various Android apps that I created for myself and others ## License -Copyright © 2018-2025 [Bastiaan van der Plaat](https://github.com/bplaat) +Copyright © 2018-2026 [Bastiaan van der Plaat](https://github.com/bplaat) Licensed under the [MIT](LICENSE) license. diff --git a/bin/drankcounter/AndroidManifest.xml b/bin/drankcounter/AndroidManifest.xml new file mode 100644 index 00000000..38c1b99b --- /dev/null +++ b/bin/drankcounter/AndroidManifest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/drankcounter/README.md b/bin/drankcounter/README.md new file mode 100644 index 00000000..5a557a72 --- /dev/null +++ b/bin/drankcounter/README.md @@ -0,0 +1,3 @@ +# DrankCounter Android App + +A basic alcohol tracker app with home screen widget diff --git a/bin/drankcounter/bob.toml b/bin/drankcounter/bob.toml new file mode 100644 index 00000000..d44f25c9 --- /dev/null +++ b/bin/drankcounter/bob.toml @@ -0,0 +1,12 @@ +[package] +name = "drankcounter" +id = "nl.plaatsoft.drankcounter" +version = "1.0.0" + +[dependencies] +alerts = { path = "../../lib/alerts" } +compat = { path = "../../lib/compat" } +jspecify = { maven = "org.jspecify:jspecify:1.0.0" } + +[package.metadata.android] +main_activity = ".activities.MainActivity" diff --git a/bin/drankcounter/docs/images/icon-maskable.svg b/bin/drankcounter/docs/images/icon-maskable.svg new file mode 100644 index 00000000..fd7ef404 --- /dev/null +++ b/bin/drankcounter/docs/images/icon-maskable.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/bin/drankcounter/docs/images/icon.svg b/bin/drankcounter/docs/images/icon.svg new file mode 100644 index 00000000..78d09ba4 --- /dev/null +++ b/bin/drankcounter/docs/images/icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/bin/drankcounter/res/drawable-anydpi/ic_account_supervisor.xml b/bin/drankcounter/res/drawable-anydpi/ic_account_supervisor.xml new file mode 100644 index 00000000..a24a59c8 --- /dev/null +++ b/bin/drankcounter/res/drawable-anydpi/ic_account_supervisor.xml @@ -0,0 +1,4 @@ + + + + diff --git a/bin/drankcounter/res/drawable-anydpi/ic_arrow_left.xml b/bin/drankcounter/res/drawable-anydpi/ic_arrow_left.xml new file mode 100644 index 00000000..c0b69291 --- /dev/null +++ b/bin/drankcounter/res/drawable-anydpi/ic_arrow_left.xml @@ -0,0 +1,4 @@ + + + + diff --git a/bin/drankcounter/res/drawable-anydpi/ic_cup.xml b/bin/drankcounter/res/drawable-anydpi/ic_cup.xml new file mode 100644 index 00000000..1440e211 --- /dev/null +++ b/bin/drankcounter/res/drawable-anydpi/ic_cup.xml @@ -0,0 +1,4 @@ + + + + diff --git a/bin/drankcounter/res/drawable-anydpi/ic_cup_white.xml b/bin/drankcounter/res/drawable-anydpi/ic_cup_white.xml new file mode 100644 index 00000000..b55bb219 --- /dev/null +++ b/bin/drankcounter/res/drawable-anydpi/ic_cup_white.xml @@ -0,0 +1,4 @@ + + + + diff --git a/bin/drankcounter/res/drawable-anydpi/ic_dots_vertical.xml b/bin/drankcounter/res/drawable-anydpi/ic_dots_vertical.xml new file mode 100644 index 00000000..2ff93fd6 --- /dev/null +++ b/bin/drankcounter/res/drawable-anydpi/ic_dots_vertical.xml @@ -0,0 +1,4 @@ + + + + diff --git a/bin/drankcounter/res/drawable-anydpi/ic_glass_mug.xml b/bin/drankcounter/res/drawable-anydpi/ic_glass_mug.xml new file mode 100644 index 00000000..6c5c9a39 --- /dev/null +++ b/bin/drankcounter/res/drawable-anydpi/ic_glass_mug.xml @@ -0,0 +1,4 @@ + + + + diff --git a/bin/drankcounter/res/drawable-anydpi/ic_glass_mug_white.xml b/bin/drankcounter/res/drawable-anydpi/ic_glass_mug_white.xml new file mode 100644 index 00000000..fda4777b --- /dev/null +++ b/bin/drankcounter/res/drawable-anydpi/ic_glass_mug_white.xml @@ -0,0 +1,4 @@ + + + + diff --git a/bin/drankcounter/res/drawable-anydpi/ic_glass_wine.xml b/bin/drankcounter/res/drawable-anydpi/ic_glass_wine.xml new file mode 100644 index 00000000..69185592 --- /dev/null +++ b/bin/drankcounter/res/drawable-anydpi/ic_glass_wine.xml @@ -0,0 +1,4 @@ + + + + diff --git a/bin/drankcounter/res/drawable-anydpi/ic_glass_wine_white.xml b/bin/drankcounter/res/drawable-anydpi/ic_glass_wine_white.xml new file mode 100644 index 00000000..c09a7d7d --- /dev/null +++ b/bin/drankcounter/res/drawable-anydpi/ic_glass_wine_white.xml @@ -0,0 +1,4 @@ + + + + diff --git a/bin/drankcounter/res/drawable-anydpi/ic_information.xml b/bin/drankcounter/res/drawable-anydpi/ic_information.xml new file mode 100644 index 00000000..bc7e9810 --- /dev/null +++ b/bin/drankcounter/res/drawable-anydpi/ic_information.xml @@ -0,0 +1,4 @@ + + + + diff --git a/bin/drankcounter/res/drawable-anydpi/ic_invert_colors.xml b/bin/drankcounter/res/drawable-anydpi/ic_invert_colors.xml new file mode 100644 index 00000000..052dc564 --- /dev/null +++ b/bin/drankcounter/res/drawable-anydpi/ic_invert_colors.xml @@ -0,0 +1,4 @@ + + + + diff --git a/bin/drankcounter/res/drawable-anydpi/ic_launcher_foreground.xml b/bin/drankcounter/res/drawable-anydpi/ic_launcher_foreground.xml new file mode 100644 index 00000000..31ea9d88 --- /dev/null +++ b/bin/drankcounter/res/drawable-anydpi/ic_launcher_foreground.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/bin/drankcounter/res/drawable-anydpi/ic_plus.xml b/bin/drankcounter/res/drawable-anydpi/ic_plus.xml new file mode 100644 index 00000000..beb8df90 --- /dev/null +++ b/bin/drankcounter/res/drawable-anydpi/ic_plus.xml @@ -0,0 +1,4 @@ + + + + diff --git a/bin/drankcounter/res/drawable-anydpi/ic_share_variant.xml b/bin/drankcounter/res/drawable-anydpi/ic_share_variant.xml new file mode 100644 index 00000000..355c9f93 --- /dev/null +++ b/bin/drankcounter/res/drawable-anydpi/ic_share_variant.xml @@ -0,0 +1,4 @@ + + + + diff --git a/bin/drankcounter/res/drawable-anydpi/ic_star.xml b/bin/drankcounter/res/drawable-anydpi/ic_star.xml new file mode 100644 index 00000000..3422aa05 --- /dev/null +++ b/bin/drankcounter/res/drawable-anydpi/ic_star.xml @@ -0,0 +1,4 @@ + + + + diff --git a/bin/drankcounter/res/drawable-anydpi/ic_web.xml b/bin/drankcounter/res/drawable-anydpi/ic_web.xml new file mode 100644 index 00000000..39b40751 --- /dev/null +++ b/bin/drankcounter/res/drawable-anydpi/ic_web.xml @@ -0,0 +1,4 @@ + + + + diff --git a/bin/drankcounter/res/drawable/app_bar_icon_button_ripple.xml b/bin/drankcounter/res/drawable/app_bar_icon_button_ripple.xml new file mode 100644 index 00000000..81e0df40 --- /dev/null +++ b/bin/drankcounter/res/drawable/app_bar_icon_button_ripple.xml @@ -0,0 +1,4 @@ + + diff --git a/bin/drankcounter/res/drawable/widget_background.xml b/bin/drankcounter/res/drawable/widget_background.xml new file mode 100644 index 00000000..6faf0f02 --- /dev/null +++ b/bin/drankcounter/res/drawable/widget_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/bin/drankcounter/res/layout-night/widget_drankcounter.xml b/bin/drankcounter/res/layout-night/widget_drankcounter.xml new file mode 100644 index 00000000..719a9d88 --- /dev/null +++ b/bin/drankcounter/res/layout-night/widget_drankcounter.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + diff --git a/bin/drankcounter/res/layout/activity_main.xml b/bin/drankcounter/res/layout/activity_main.xml new file mode 100644 index 00000000..9c579329 --- /dev/null +++ b/bin/drankcounter/res/layout/activity_main.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + diff --git a/bin/drankcounter/res/layout/activity_settings.xml b/bin/drankcounter/res/layout/activity_settings.xml new file mode 100644 index 00000000..400d2adb --- /dev/null +++ b/bin/drankcounter/res/layout/activity_settings.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/drankcounter/res/layout/item_drink.xml b/bin/drankcounter/res/layout/item_drink.xml new file mode 100644 index 00000000..ba787a03 --- /dev/null +++ b/bin/drankcounter/res/layout/item_drink.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/bin/drankcounter/res/layout/item_drink_section_header.xml b/bin/drankcounter/res/layout/item_drink_section_header.xml new file mode 100644 index 00000000..7e690036 --- /dev/null +++ b/bin/drankcounter/res/layout/item_drink_section_header.xml @@ -0,0 +1,3 @@ + + diff --git a/bin/drankcounter/res/layout/main_drink_header.xml b/bin/drankcounter/res/layout/main_drink_header.xml new file mode 100644 index 00000000..406a2e27 --- /dev/null +++ b/bin/drankcounter/res/layout/main_drink_header.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + diff --git a/bin/drankcounter/res/layout/widget_drankcounter.xml b/bin/drankcounter/res/layout/widget_drankcounter.xml new file mode 100644 index 00000000..719a9d88 --- /dev/null +++ b/bin/drankcounter/res/layout/widget_drankcounter.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + diff --git a/bin/drankcounter/res/menu/item_drink_options.xml b/bin/drankcounter/res/menu/item_drink_options.xml new file mode 100644 index 00000000..7bbfac16 --- /dev/null +++ b/bin/drankcounter/res/menu/item_drink_options.xml @@ -0,0 +1,5 @@ + + + + diff --git a/bin/drankcounter/res/menu/options.xml b/bin/drankcounter/res/menu/options.xml new file mode 100644 index 00000000..59d23d08 --- /dev/null +++ b/bin/drankcounter/res/menu/options.xml @@ -0,0 +1,5 @@ + + + + diff --git a/bin/drankcounter/res/mipmap-anydpi-v26/ic_launcher.xml b/bin/drankcounter/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..35c2a772 --- /dev/null +++ b/bin/drankcounter/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/bin/drankcounter/res/mipmap-hdpi/ic_launcher.png b/bin/drankcounter/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..6426a355 Binary files /dev/null and b/bin/drankcounter/res/mipmap-hdpi/ic_launcher.png differ diff --git a/bin/drankcounter/res/mipmap-mdpi/ic_launcher.png b/bin/drankcounter/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..19366f0c Binary files /dev/null and b/bin/drankcounter/res/mipmap-mdpi/ic_launcher.png differ diff --git a/bin/drankcounter/res/mipmap-xhdpi/ic_launcher.png b/bin/drankcounter/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..da8c3369 Binary files /dev/null and b/bin/drankcounter/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/bin/drankcounter/res/mipmap-xxhdpi/ic_launcher.png b/bin/drankcounter/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..3c65f87d Binary files /dev/null and b/bin/drankcounter/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/bin/drankcounter/res/mipmap-xxxhdpi/ic_launcher.png b/bin/drankcounter/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..732d945a Binary files /dev/null and b/bin/drankcounter/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/bin/drankcounter/res/values-night/colors.xml b/bin/drankcounter/res/values-night/colors.xml new file mode 100644 index 00000000..7f2b6a1b --- /dev/null +++ b/bin/drankcounter/res/values-night/colors.xml @@ -0,0 +1,11 @@ + + + #222 + #111 + #fff + #888 + #fff + #333 + #1E1E1E + #2B2B2B + diff --git a/bin/drankcounter/res/values-night/themes.xml b/bin/drankcounter/res/values-night/themes.xml new file mode 100644 index 00000000..21bf2f02 --- /dev/null +++ b/bin/drankcounter/res/values-night/themes.xml @@ -0,0 +1,4 @@ + + + + diff --git a/bin/drankcounter/res/values-nl/strings.xml b/bin/drankcounter/res/values-nl/strings.xml new file mode 100644 index 00000000..d78ce97c --- /dev/null +++ b/bin/drankcounter/res/values-nl/strings.xml @@ -0,0 +1,54 @@ + + + + Instellingen + + + Bier + Wijn + Sterke drank + Nog geen drankjes geregistreerd + + + Vandaag + Gisteren + + + Bier + Wijn + Sterke drank + + + Verwijderen + + + Instellingen + + Weergave + + Taal + Gebruik systeemstandaard + + Thema + Licht + Donker + Ingesteld door batterijbesparing + Gebruik systeemstandaard + + Informatie + App versie + Who, jij bent een geweldige hacker! + Beoordeel deze app + Deel deze app + Bekijk deze coole app: %s + Over deze app + Deze applicatie is gemaakt door Bastiaan van der Plaat + + Over deze app + Deze applicatie is gemaakt door Bastiaan van der Plaat + Website + OK + + + Registreer je dagelijkse drankjes + diff --git a/bin/drankcounter/res/values/colors.xml b/bin/drankcounter/res/values/colors.xml new file mode 100644 index 00000000..4df0b0b8 --- /dev/null +++ b/bin/drankcounter/res/values/colors.xml @@ -0,0 +1,13 @@ + + + #D32F2F + #fff + #D32F2F + #fff + #111 + #777 + #333 + #ddd + #F0F0F0 + #FFFFFF + diff --git a/bin/drankcounter/res/values/strings.xml b/bin/drankcounter/res/values/strings.xml new file mode 100644 index 00000000..cdf30d21 --- /dev/null +++ b/bin/drankcounter/res/values/strings.xml @@ -0,0 +1,58 @@ + + + DrankCounter + + + Settings + + + Beer + Wine + Liqueur + No drinks recorded yet + + + Today + Yesterday + + + Beer + Wine + Liqueur + + + Delete + + + Settings + + Display + + Language + English + Nederlands + Use system default + + Theme + Light + Dark + Set by Battery Saver + Use system default + + Information + App version + Who, you are an amazing hacker! + Rate this app + Share this app + Check out this cool app: %s + About this app + This application is made by Bastiaan van der Plaat + + About this app + This application is made by Bastiaan van der Plaat + Website + OK + + + Track your daily drinks + diff --git a/bin/drankcounter/res/values/styles.xml b/bin/drankcounter/res/values/styles.xml new file mode 100644 index 00000000..108f4e68 --- /dev/null +++ b/bin/drankcounter/res/values/styles.xml @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/drankcounter/res/values/themes.xml b/bin/drankcounter/res/values/themes.xml new file mode 100644 index 00000000..e15f65a8 --- /dev/null +++ b/bin/drankcounter/res/values/themes.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/bin/drankcounter/res/xml/widget_drankcounter.xml b/bin/drankcounter/res/xml/widget_drankcounter.xml new file mode 100644 index 00000000..4d509294 --- /dev/null +++ b/bin/drankcounter/res/xml/widget_drankcounter.xml @@ -0,0 +1,11 @@ + + diff --git a/bin/drankcounter/src/nl/plaatsoft/drankcounter/DrinkDatabase.java b/bin/drankcounter/src/nl/plaatsoft/drankcounter/DrinkDatabase.java new file mode 100644 index 00000000..7db88be9 --- /dev/null +++ b/bin/drankcounter/src/nl/plaatsoft/drankcounter/DrinkDatabase.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026 Bastiaan van der Plaat + * + * SPDX-License-Identifier: MIT + */ + +package nl.plaatsoft.drankcounter; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +public class DrinkDatabase extends SQLiteOpenHelper { + private static final String DB_NAME = "drankcounter.db"; + private static final int DB_VERSION = 1; + + public static final String TABLE_DRINKS = "drinks"; + public static final String COLUMN_ID = "id"; + public static final String COLUMN_TYPE = "type"; + public static final String COLUMN_CREATED_AT = "created_at"; + + public DrinkDatabase(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + var createTableSQL = "CREATE TABLE " + TABLE_DRINKS + " (" + COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + COLUMN_TYPE + " INTEGER NOT NULL, " + COLUMN_CREATED_AT + " INTEGER NOT NULL)"; + db.execSQL(createTableSQL); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // No upgrades for now + } +} diff --git a/bin/drankcounter/src/nl/plaatsoft/drankcounter/DrinkDatabaseHelper.java b/bin/drankcounter/src/nl/plaatsoft/drankcounter/DrinkDatabaseHelper.java new file mode 100644 index 00000000..8fd5b35e --- /dev/null +++ b/bin/drankcounter/src/nl/plaatsoft/drankcounter/DrinkDatabaseHelper.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2026 Bastiaan van der Plaat + * + * SPDX-License-Identifier: MIT + */ + +package nl.plaatsoft.drankcounter; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import nl.plaatsoft.drankcounter.models.Drink; + +public class DrinkDatabaseHelper { + private final DrinkDatabase dbHelper; + + public DrinkDatabaseHelper(Context context) { + this.dbHelper = new DrinkDatabase(context); + } + + public long insertDrink(int type, long createdAt) { + var db = dbHelper.getWritableDatabase(); + var values = new ContentValues(); + values.put(DrinkDatabase.COLUMN_TYPE, type); + values.put(DrinkDatabase.COLUMN_CREATED_AT, createdAt); + return db.insert(DrinkDatabase.TABLE_DRINKS, null, values); + } + + public List getAllDrinks() { + var db = dbHelper.getReadableDatabase(); + var drinks = new ArrayList(); + var cursor = db.query( + DrinkDatabase.TABLE_DRINKS, null, null, null, null, null, DrinkDatabase.COLUMN_CREATED_AT + " DESC"); + if (cursor.moveToFirst()) { + do { + var id = cursor.getLong(cursor.getColumnIndexOrThrow(DrinkDatabase.COLUMN_ID)); + var type = cursor.getInt(cursor.getColumnIndexOrThrow(DrinkDatabase.COLUMN_TYPE)); + var createdAt = cursor.getLong(cursor.getColumnIndexOrThrow(DrinkDatabase.COLUMN_CREATED_AT)); + drinks.add(new Drink(id, type, createdAt)); + } while (cursor.moveToNext()); + } + cursor.close(); + return drinks; + } + + public List getTodaysDrinks() { + var cal = Calendar.getInstance(); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + var startOfDay = cal.getTimeInMillis() / 1000; + var db = dbHelper.getReadableDatabase(); + var drinks = new ArrayList(); + var cursor = db.query(DrinkDatabase.TABLE_DRINKS, null, DrinkDatabase.COLUMN_CREATED_AT + " >= ?", + new String[] {String.valueOf(startOfDay)}, null, null, DrinkDatabase.COLUMN_CREATED_AT + " DESC"); + if (cursor.moveToFirst()) { + do { + var id = cursor.getLong(cursor.getColumnIndexOrThrow(DrinkDatabase.COLUMN_ID)); + var type = cursor.getInt(cursor.getColumnIndexOrThrow(DrinkDatabase.COLUMN_TYPE)); + var createdAt = cursor.getLong(cursor.getColumnIndexOrThrow(DrinkDatabase.COLUMN_CREATED_AT)); + drinks.add(new Drink(id, type, createdAt)); + } while (cursor.moveToNext()); + } + cursor.close(); + return drinks; + } + + public static int countByType(List drinks, int type) { + var count = 0; + for (var drink : drinks) { + if (drink.type() == type) + count++; + } + return count; + } + + public void deleteDrink(long id) { + var db = dbHelper.getWritableDatabase(); + db.delete(DrinkDatabase.TABLE_DRINKS, DrinkDatabase.COLUMN_ID + " = ?", new String[] {String.valueOf(id)}); + } + + public void close() { + dbHelper.close(); + } +} diff --git a/bin/drankcounter/src/nl/plaatsoft/drankcounter/Settings.java b/bin/drankcounter/src/nl/plaatsoft/drankcounter/Settings.java new file mode 100644 index 00000000..8aa6ffdb --- /dev/null +++ b/bin/drankcounter/src/nl/plaatsoft/drankcounter/Settings.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026 Bastiaan van der Plaat + * + * SPDX-License-Identifier: MIT + */ + +package nl.plaatsoft.drankcounter; + +import android.content.Context; +import android.content.SharedPreferences; + +public class Settings { + private final SharedPreferences prefs; + + public Settings(Context context) { + prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE); + } + + // Language + public static final int LANGUAGE_ENGLISH = 0; + public static final int LANGUAGE_DUTCH = 1; + public static final int LANGUAGE_SYSTEM = 2; + + public int getLanguage() { + return prefs.getInt("language", LANGUAGE_SYSTEM); + } + + public void setLanguage(int language) { + prefs.edit().putInt("language", language).apply(); + } + + // Theme + public static final int THEME_LIGHT = 0; + public static final int THEME_DARK = 1; + public static final int THEME_SYSTEM = 2; + + public int getTheme() { + return prefs.getInt("theme", THEME_SYSTEM); + } + + public void setTheme(int theme) { + prefs.edit().putInt("theme", theme).apply(); + } +} diff --git a/bin/drankcounter/src/nl/plaatsoft/drankcounter/WidgetProvider.java b/bin/drankcounter/src/nl/plaatsoft/drankcounter/WidgetProvider.java new file mode 100644 index 00000000..17ee4241 --- /dev/null +++ b/bin/drankcounter/src/nl/plaatsoft/drankcounter/WidgetProvider.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2026 Bastiaan van der Plaat + * + * SPDX-License-Identifier: MIT + */ + +package nl.plaatsoft.drankcounter; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.widget.RemoteViews; + +import nl.plaatsoft.drankcounter.models.Drink; + +public class WidgetProvider extends AppWidgetProvider { + private static final String ACTION_ADD_DRINK = "nl.plaatsoft.drankcounter.ACTION_ADD_DRINK"; + private static final String EXTRA_DRINK_TYPE = "drink_type"; + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + for (var id : appWidgetIds) { + updateWidget(context, appWidgetManager, id); + } + } + + @Override + public void onReceive(Context context, Intent intent) { + super.onReceive(context, intent); + if (ACTION_ADD_DRINK.equals(intent.getAction())) { + var type = intent.getIntExtra(EXTRA_DRINK_TYPE, Drink.TYPE_BEER); + var dbHelper = new DrinkDatabaseHelper(context); + dbHelper.insertDrink(type, System.currentTimeMillis() / 1000); + dbHelper.close(); + updateAllWidgets(context); + } + } + + public static void updateAllWidgets(Context context) { + var appWidgetManager = AppWidgetManager.getInstance(context); + var ids = appWidgetManager.getAppWidgetIds(new ComponentName(context, WidgetProvider.class)); + for (var id : ids) { + updateWidget(context, appWidgetManager, id); + } + } + + private static void updateWidget(Context context, AppWidgetManager appWidgetManager, int widgetId) { + var dbHelper = new DrinkDatabaseHelper(context); + var todayDrinks = dbHelper.getTodaysDrinks(); + dbHelper.close(); + + var beerCount = DrinkDatabaseHelper.countByType(todayDrinks, Drink.TYPE_BEER); + var wineCount = DrinkDatabaseHelper.countByType(todayDrinks, Drink.TYPE_WINE); + var liqueurCount = DrinkDatabaseHelper.countByType(todayDrinks, Drink.TYPE_LIQUEUR); + + var lightViews = new RemoteViews(context.getPackageName(), R.layout.widget_drankcounter); + applyCountsAndClicks(context, lightViews, beerCount, wineCount, liqueurCount); + + RemoteViews views; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + var darkViews = new RemoteViews(context.getPackageName(), R.layout.widget_drankcounter); + applyCountsAndClicks(context, darkViews, beerCount, wineCount, liqueurCount); + views = new RemoteViews(lightViews, darkViews); + } else { + views = lightViews; + } + + appWidgetManager.updateAppWidget(widgetId, views); + } + + private static void applyCountsAndClicks( + Context context, RemoteViews views, int beerCount, int wineCount, int liqueurCount) { + views.setTextViewText(R.id.widget_beer_count, String.valueOf(beerCount)); + views.setTextViewText(R.id.widget_wine_count, String.valueOf(wineCount)); + views.setTextViewText(R.id.widget_liqueur_count, String.valueOf(liqueurCount)); + + views.setOnClickPendingIntent(R.id.widget_beer_button, buildAddDrinkIntent(context, Drink.TYPE_BEER)); + views.setOnClickPendingIntent(R.id.widget_wine_button, buildAddDrinkIntent(context, Drink.TYPE_WINE)); + views.setOnClickPendingIntent(R.id.widget_liqueur_button, buildAddDrinkIntent(context, Drink.TYPE_LIQUEUR)); + } + + private static PendingIntent buildAddDrinkIntent(Context context, int drinkType) { + var intent = new Intent(context, WidgetProvider.class); + intent.setAction(ACTION_ADD_DRINK); + intent.putExtra(EXTRA_DRINK_TYPE, drinkType); + var flags = PendingIntent.FLAG_UPDATE_CURRENT; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + flags |= PendingIntent.FLAG_IMMUTABLE; + } + return PendingIntent.getBroadcast(context, drinkType, intent, flags); + } +} diff --git a/bin/drankcounter/src/nl/plaatsoft/drankcounter/activities/BaseActivity.java b/bin/drankcounter/src/nl/plaatsoft/drankcounter/activities/BaseActivity.java new file mode 100644 index 00000000..13cfcf40 --- /dev/null +++ b/bin/drankcounter/src/nl/plaatsoft/drankcounter/activities/BaseActivity.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026 Bastiaan van der Plaat + * + * SPDX-License-Identifier: MIT + */ + +package nl.plaatsoft.drankcounter.activities; + +import java.util.Locale; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.os.Build; +import android.os.PowerManager; +import android.view.ViewGroup; +import android.window.OnBackInvokedCallback; +import android.window.OnBackInvokedDispatcher; + +import nl.plaatsoft.android.compat.CompatActivity; +import nl.plaatsoft.android.compat.WindowInsetsCompat; +import nl.plaatsoft.drankcounter.Settings; + +import org.jspecify.annotations.Nullable; + +public abstract class BaseActivity extends CompatActivity { + protected @SuppressWarnings("null") Settings settings; + + @Override + public void attachBaseContext(@SuppressWarnings("null") Context context) { + settings = new Settings(context); + var language = settings.getLanguage(); + var theme = settings.getTheme(); + + // Update configuration when different from system defaults + if (language != Settings.LANGUAGE_SYSTEM || theme != Settings.THEME_SYSTEM + || (theme == Settings.THEME_SYSTEM && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)) { + var configuration = new Configuration(context.getResources().getConfiguration()); + + if (language == Settings.LANGUAGE_ENGLISH) + configuration.setLocale(Locale.forLanguageTag("en")); + if (language == Settings.LANGUAGE_DUTCH) + configuration.setLocale(Locale.forLanguageTag("nl")); + + if (theme == Settings.THEME_LIGHT) { + configuration.uiMode |= Configuration.UI_MODE_NIGHT_NO; + configuration.uiMode &= ~Configuration.UI_MODE_NIGHT_YES; + } + if (theme == Settings.THEME_DARK) { + configuration.uiMode |= Configuration.UI_MODE_NIGHT_YES; + configuration.uiMode &= ~Configuration.UI_MODE_NIGHT_NO; + } + // Set dark mode on when in battery saver mode + if (theme == Settings.THEME_SYSTEM && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (((PowerManager)context.getSystemService(Context.POWER_SERVICE)).isPowerSaveMode()) { + configuration.uiMode |= Configuration.UI_MODE_NIGHT_YES; + configuration.uiMode &= ~Configuration.UI_MODE_NIGHT_NO; + } else { + configuration.uiMode |= Configuration.UI_MODE_NIGHT_NO; + configuration.uiMode &= ~Configuration.UI_MODE_NIGHT_YES; + } + } + + super.attachBaseContext(context.createConfigurationContext(configuration)); + return; + } + super.attachBaseContext(context); + } +} diff --git a/bin/drankcounter/src/nl/plaatsoft/drankcounter/activities/MainActivity.java b/bin/drankcounter/src/nl/plaatsoft/drankcounter/activities/MainActivity.java new file mode 100644 index 00000000..e16ec076 --- /dev/null +++ b/bin/drankcounter/src/nl/plaatsoft/drankcounter/activities/MainActivity.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2026 Bastiaan van der Plaat + * + * SPDX-License-Identifier: MIT + */ + +package nl.plaatsoft.drankcounter.activities; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.TextView; + +import nl.plaatsoft.android.alerts.RatingAlert; +import nl.plaatsoft.android.alerts.UpdateAlert; +import nl.plaatsoft.android.compat.ContextCompat; +import nl.plaatsoft.android.fetch.FetchDataTask; +import nl.plaatsoft.android.fetch.FetchImageTask; +import nl.plaatsoft.drankcounter.DrinkDatabaseHelper; +import nl.plaatsoft.drankcounter.R; +import nl.plaatsoft.drankcounter.WidgetProvider; +import nl.plaatsoft.drankcounter.components.DrinkAdapter; +import nl.plaatsoft.drankcounter.models.Drink; + +import org.jspecify.annotations.Nullable; + +public class MainActivity extends BaseActivity implements PopupMenu.OnMenuItemClickListener { + private static final int SETTINGS_REQUEST_CODE = 1; + + private Handler handler = new Handler(Looper.getMainLooper()); + private int oldLanguage = -1; + private int oldTheme = -1; + + private DrinkDatabaseHelper dbHelper; + private DrinkAdapter drinkAdapter; + private ListView drinkList; + private TextView emptyText; + private TextView beerCountView; + private TextView wineCountView; + private TextView liqueurCountView; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + // Initialize database + dbHelper = new DrinkDatabaseHelper(this); + + // Setup list with drink buttons as scrollable header + drinkList = findViewById(R.id.main_drink_list); + useWindowInsets(drinkList); + emptyText = findViewById(R.id.main_empty_list); + + var headerView = LayoutInflater.from(this).inflate(R.layout.main_drink_header, drinkList, false); + drinkList.addHeaderView(headerView, null, false); + + drinkAdapter = new DrinkAdapter(this, dbHelper); + drinkAdapter.setOnDeleteListener(this::refreshDrinkList); + drinkList.setAdapter(drinkAdapter); + + beerCountView = headerView.findViewById(R.id.main_beer_count); + wineCountView = headerView.findViewById(R.id.main_wine_count); + liqueurCountView = headerView.findViewById(R.id.main_liqueur_count); + + // Options menu button + findViewById(R.id.main_options_menu_button).setOnClickListener(view -> { + var optionsMenu = new PopupMenu(this, view, Gravity.TOP | Gravity.RIGHT); + optionsMenu.getMenuInflater().inflate(R.menu.options, optionsMenu.getMenu()); + optionsMenu.setOnMenuItemClickListener(this); + optionsMenu.show(); + }); + + // Add drink buttons (in scrollable header) + headerView.findViewById(R.id.main_add_beer_button).setOnClickListener(view -> addDrink(Drink.TYPE_BEER)); + headerView.findViewById(R.id.main_add_wine_button).setOnClickListener(view -> addDrink(Drink.TYPE_WINE)); + headerView.findViewById(R.id.main_add_liqueur_button).setOnClickListener(view -> addDrink(Drink.TYPE_LIQUEUR)); + + // Show update alert + UpdateAlert.checkAndShow(this, + "https://raw.githubusercontent.com/bplaat/android-apps/refs/heads/master/bin/drankcounter/bob.toml", + SettingsActivity.STORE_PAGE_URL); + } + + private void addDrink(int type) { + dbHelper.insertDrink(type, System.currentTimeMillis() / 1000); + refreshDrinkList(); + } + + @Override + protected void onResume() { + super.onResume(); + refreshDrinkList(); + } + + private void refreshDrinkList() { + var drinks = dbHelper.getAllDrinks(); + var todayDrinks = dbHelper.getTodaysDrinks(); + + emptyText.setVisibility(drinks.isEmpty() ? View.VISIBLE : View.GONE); + drinkAdapter.setDrinks(drinks); + + beerCountView.setText(String.valueOf(DrinkDatabaseHelper.countByType(todayDrinks, Drink.TYPE_BEER))); + wineCountView.setText(String.valueOf(DrinkDatabaseHelper.countByType(todayDrinks, Drink.TYPE_WINE))); + liqueurCountView.setText(String.valueOf(DrinkDatabaseHelper.countByType(todayDrinks, Drink.TYPE_LIQUEUR))); + + WidgetProvider.updateAllWidgets(this); + } + + @Override + public boolean onMenuItemClick(@SuppressWarnings("null") MenuItem item) { + if (item.getItemId() == R.id.menu_options_settings) { + oldLanguage = settings.getLanguage(); + oldTheme = settings.getTheme(); + startActivityForResult(new Intent(this, SettingsActivity.class), SETTINGS_REQUEST_CODE); + return true; + } + return false; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, @SuppressWarnings("null") Intent data) { + // When settings activity is closed check for restart + if (requestCode == SETTINGS_REQUEST_CODE) { + if (oldLanguage != -1 && oldTheme != -1) { + if (oldLanguage != settings.getLanguage() || oldTheme != settings.getTheme()) { + handler.post(() -> recreate()); + } + } + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (dbHelper != null) { + dbHelper.close(); + } + } +} diff --git a/bin/drankcounter/src/nl/plaatsoft/drankcounter/activities/SettingsActivity.java b/bin/drankcounter/src/nl/plaatsoft/drankcounter/activities/SettingsActivity.java new file mode 100644 index 00000000..bc6b3da3 --- /dev/null +++ b/bin/drankcounter/src/nl/plaatsoft/drankcounter/activities/SettingsActivity.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2026 Bastiaan van der Plaat + * + * SPDX-License-Identifier: MIT + */ + +package nl.plaatsoft.drankcounter.activities; + +import android.app.AlertDialog; +import android.content.Intent; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import nl.plaatsoft.drankcounter.R; + +import org.jspecify.annotations.Nullable; + +@SuppressWarnings("this-escape") +public class SettingsActivity extends BaseActivity { + public static final String STORE_PAGE_URL = "https://github.com/bplaat/android-apps/tree/master/bin/drankcounter"; + private static final String ABOUT_WEBSITE_URL = "https://bplaat.nl/"; + + private int versionButtonClickCounter = 0; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + useWindowInsets(); + + // Back button + findViewById(R.id.settings_back_button).setOnClickListener(view -> finish()); + + // Language button + var languages = new String[] {getResources().getString(R.string.settings_language_english), + getResources().getString(R.string.settings_language_dutch), + getResources().getString(R.string.settings_language_system)}; + var language = settings.getLanguage(); + ((TextView)findViewById(R.id.settings_language_label)).setText(languages[language]); + findViewById(R.id.settings_language_button).setOnClickListener(view -> { + var alertDialog = new AlertDialog.Builder(this) + .setTitle(R.string.settings_language_button) + .setSingleChoiceItems(languages, language, + (dialog, which) -> { + dialog.dismiss(); + if (language != which) { + settings.setLanguage(which); + recreate(); + } + }) + .show(); + var density = getResources().getDisplayMetrics().density; + alertDialog.getListView().setPadding(0, 0, 0, (int)(16 * density)); + }); + + // Themes button + var themes = new String[] {getResources().getString(R.string.settings_theme_light), + getResources().getString(R.string.settings_theme_dark), + Build.VERSION.SDK_INT < Build.VERSION_CODES.Q + ? getResources().getString(R.string.settings_theme_battery_saver) + : getResources().getString(R.string.settings_theme_system)}; + var theme = settings.getTheme(); + ((TextView)findViewById(R.id.settings_theme_label)).setText(themes[theme]); + findViewById(R.id.settings_theme_button).setOnClickListener(view -> { + var alertDialog = new AlertDialog.Builder(this) + .setTitle(R.string.settings_theme_button) + .setSingleChoiceItems(themes, theme, + (dialog, which) -> { + dialog.dismiss(); + if (theme != which) { + settings.setTheme(which); + recreate(); + } + }) + .show(); + var density = getResources().getDisplayMetrics().density; + alertDialog.getListView().setPadding(0, 0, 0, (int)(16 * density)); + }); + + // Version button easter egg + try { + ((TextView)findViewById(R.id.settings_version_label)) + .setText("v" + getPackageManager().getPackageInfo(getPackageName(), 0).versionName); + } catch (NameNotFoundException exception) { + Log.e(getPackageName(), "Can't get app version", exception); + } + findViewById(R.id.settings_version_button).setOnClickListener(view -> { + versionButtonClickCounter++; + if (versionButtonClickCounter == 8) { + versionButtonClickCounter = 0; + Toast.makeText(this, R.string.settings_version_message, Toast.LENGTH_SHORT).show(); + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://youtu.be/dQw4w9WgXcQ?t=43"))); + } + }); + + // Rate button + findViewById(R.id.settings_rate_button).setOnClickListener(view -> { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(STORE_PAGE_URL))); + }); + + // Share button + findViewById(R.id.settings_share_button).setOnClickListener(view -> { + var intent = new Intent(); + intent.setAction(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra( + Intent.EXTRA_TEXT, getResources().getString(R.string.settings_share_message, STORE_PAGE_URL)); + startActivity(Intent.createChooser(intent, null)); + }); + + // About button + findViewById(R.id.settings_about_button).setOnClickListener(view -> { + new AlertDialog.Builder(this) + .setTitle(R.string.settings_about_alert_title_label) + .setMessage(R.string.settings_about_alert_message_label) + .setNegativeButton(R.string.settings_about_alert_website_button, + (dialog, which) -> { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(ABOUT_WEBSITE_URL))); }) + .setPositiveButton(R.string.settings_about_alert_ok_button, null) + .show(); + }); + + // Footer button + findViewById(R.id.settings_footer_button).setOnClickListener(view -> { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(ABOUT_WEBSITE_URL))); + }); + } +} diff --git a/bin/drankcounter/src/nl/plaatsoft/drankcounter/components/DrinkAdapter.java b/bin/drankcounter/src/nl/plaatsoft/drankcounter/components/DrinkAdapter.java new file mode 100644 index 00000000..3acf0d0a --- /dev/null +++ b/bin/drankcounter/src/nl/plaatsoft/drankcounter/components/DrinkAdapter.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2026 Bastiaan van der Plaat + * + * SPDX-License-Identifier: MIT + */ + +package nl.plaatsoft.drankcounter.components; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +import android.content.Context; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.PopupMenu; +import android.widget.TextView; + +import nl.plaatsoft.drankcounter.DrinkDatabaseHelper; +import nl.plaatsoft.drankcounter.R; +import nl.plaatsoft.drankcounter.Settings; +import nl.plaatsoft.drankcounter.models.Drink; + +import org.jspecify.annotations.Nullable; + +public class DrinkAdapter extends BaseAdapter { + private static final int VIEW_TYPE_HEADER = 0; + private static final int VIEW_TYPE_DRINK = 1; + + public interface OnDeleteListener { + void onDrinkDeleted(); + } + + private static class DrinkViewHolder { + public @SuppressWarnings("null") ImageView drinkIcon; + public @SuppressWarnings("null") TextView drinkType; + public @SuppressWarnings("null") TextView drinkTime; + public @SuppressWarnings("null") ImageButton drinkMenuButton; + } + + private final Context context; + private final DrinkDatabaseHelper dbHelper; + private final SimpleDateFormat dayKeyFormat; + private final SimpleDateFormat dayDisplayFormat; + private final SimpleDateFormat timeFormat; + private final String todayLabel; + private final String yesterdayLabel; + private final List items = new ArrayList<>(); + private @Nullable OnDeleteListener onDeleteListener; + + public DrinkAdapter(Context context, DrinkDatabaseHelper dbHelper) { + this.context = context; + this.dbHelper = dbHelper; + var settings = new Settings(context); + var locale = getLocaleFromSettings(settings); + this.dayKeyFormat = new SimpleDateFormat("yyyyMMdd", Locale.getDefault()); + this.dayDisplayFormat = new SimpleDateFormat("d MMMM", locale); + this.timeFormat = new SimpleDateFormat("HH:mm", locale); + this.todayLabel = context.getString(R.string.day_header_today); + this.yesterdayLabel = context.getString(R.string.day_header_yesterday); + } + + private Locale getLocaleFromSettings(Settings settings) { + return switch (settings.getLanguage()) { + case Settings.LANGUAGE_ENGLISH -> Locale.forLanguageTag("en"); + case Settings.LANGUAGE_DUTCH -> Locale.forLanguageTag("nl"); + default -> Locale.getDefault(); + }; + } + + public void setOnDeleteListener(OnDeleteListener listener) { + this.onDeleteListener = listener; + } + + public void setDrinks(List drinks) { + items.clear(); + var today = dayKeyFormat.format(new Date()); + var cal = Calendar.getInstance(); + cal.add(Calendar.DAY_OF_YEAR, -1); + var yesterday = dayKeyFormat.format(cal.getTime()); + + String lastDayKey = null; + for (var drink : drinks) { + var date = new Date(drink.createdAt() * 1000); + var dayKey = dayKeyFormat.format(date); + if (!dayKey.equals(lastDayKey)) { + if (dayKey.equals(today)) { + items.add(todayLabel); + } else if (dayKey.equals(yesterday)) { + items.add(yesterdayLabel); + } else { + items.add(dayDisplayFormat.format(date)); + } + lastDayKey = dayKey; + } + items.add(drink); + } + notifyDataSetChanged(); + } + + @Override + public int getCount() { + return items.size(); + } + + @Override + public Object getItem(int position) { + return items.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getViewTypeCount() { + return 2; + } + + @Override + public int getItemViewType(int position) { + return items.get(position) instanceof Drink ? VIEW_TYPE_DRINK : VIEW_TYPE_HEADER; + } + + @Override + public boolean isEnabled(int position) { + return false; + } + + private int getIconResource(int type) { + return switch (type) { + case Drink.TYPE_BEER -> R.drawable.ic_glass_mug; + case Drink.TYPE_WINE -> R.drawable.ic_glass_wine; + case Drink.TYPE_LIQUEUR -> R.drawable.ic_cup; + default -> 0; + }; + } + + private String getTypeString(int type) { + return switch (type) { + case Drink.TYPE_BEER -> context.getString(R.string.drink_type_beer); + case Drink.TYPE_WINE -> context.getString(R.string.drink_type_wine); + case Drink.TYPE_LIQUEUR -> context.getString(R.string.drink_type_liqueur); + default -> "Unknown"; + }; + } + + @Override + public View getView(int position, @Nullable View convertView, @SuppressWarnings("null") ViewGroup parent) { + if (getItemViewType(position) == VIEW_TYPE_HEADER) { + if (convertView == null) { + convertView = LayoutInflater.from(context).inflate(R.layout.item_drink_section_header, parent, false); + } + ((TextView)Objects.requireNonNull(convertView)).setText((String)items.get(position)); + return convertView; + } + + DrinkViewHolder viewHolder; + if (convertView == null) { + convertView = LayoutInflater.from(context).inflate(R.layout.item_drink, parent, false); + viewHolder = new DrinkViewHolder(); + viewHolder.drinkIcon = Objects.requireNonNull(convertView).findViewById(R.id.drink_icon); + viewHolder.drinkType = convertView.findViewById(R.id.drink_type); + viewHolder.drinkTime = convertView.findViewById(R.id.drink_datetime); + viewHolder.drinkMenuButton = convertView.findViewById(R.id.drink_menu_button); + convertView.setTag(viewHolder); + } else { + viewHolder = (DrinkViewHolder)convertView.getTag(); + } + + var drink = (Drink)items.get(position); + viewHolder.drinkIcon.setImageResource(getIconResource(drink.type())); + viewHolder.drinkType.setText(getTypeString(drink.type())); + viewHolder.drinkTime.setText(timeFormat.format(new Date(drink.createdAt() * 1000))); + + viewHolder.drinkMenuButton.setOnClickListener(view -> { + var menu = new PopupMenu(context, view, Gravity.END); + menu.getMenuInflater().inflate(R.menu.item_drink_options, menu.getMenu()); + menu.setOnMenuItemClickListener(item -> { + if (item.getItemId() == R.id.menu_item_drink_delete) { + dbHelper.deleteDrink(drink.id()); + if (onDeleteListener != null) { + onDeleteListener.onDrinkDeleted(); + } + return true; + } + return false; + }); + menu.show(); + }); + + return convertView; + } +} diff --git a/bin/drankcounter/src/nl/plaatsoft/drankcounter/models/Drink.java b/bin/drankcounter/src/nl/plaatsoft/drankcounter/models/Drink.java new file mode 100644 index 00000000..405bf3c6 --- /dev/null +++ b/bin/drankcounter/src/nl/plaatsoft/drankcounter/models/Drink.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2026 Bastiaan van der Plaat + * + * SPDX-License-Identifier: MIT + */ + +package nl.plaatsoft.drankcounter.models; + +public record Drink(long id, int type, long createdAt) { + public static final int TYPE_BEER = 0; + public static final int TYPE_WINE = 1; + public static final int TYPE_LIQUEUR = 2; +}