diff --git a/.fleet/goals/example.md b/.fleet/goals/example.md new file mode 100644 index 0000000..a473a00 --- /dev/null +++ b/.fleet/goals/example.md @@ -0,0 +1,24 @@ +--- +milestone: "1" +--- + +# Example Fleet Goal + +Analyze the codebase for potential improvements and create +issues for the engineering team. + +## Tools +- Test Coverage: `npx vitest --coverage --json` + +## Assessment Hints +- Focus on missing error handling in API routes +- Look for hardcoded configuration values + +## Insight Hints +- Report on overall test coverage metrics +- Note any unusually complex functions (cyclomatic complexity) + +## Constraints +- Do NOT propose changes already covered by open issues +- Do NOT propose changes rejected in recently closed issues +- Keep tasks small and isolated — one logical change per issue diff --git a/.github/workflows/fleet-analyze.yml b/.github/workflows/fleet-analyze.yml new file mode 100644 index 0000000..73efc52 --- /dev/null +++ b/.github/workflows/fleet-analyze.yml @@ -0,0 +1,42 @@ +# Generated by @google/jules-fleet init +# https://github.com/google-labs-code/jules-sdk + +name: Fleet Analyze + +on: + schedule: + - cron: '0 */6 * * *' + workflow_dispatch: + inputs: + goal: + description: 'Path to goal file (or blank for all)' + type: string + default: '' + milestone: + description: 'Milestone ID override' + type: string + default: '' + +concurrency: + group: fleet-analyze + cancel-in-progress: false + +jobs: + analyze: + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + pull-requests: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - run: npx -y --package=@google/jules-fleet jules-fleet analyze --goal "${{ inputs.goal }}" --milestone "${{ inputs.milestone }}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JULES_API_KEY: ${{ secrets.JULES_API_KEY }} + FLEET_APP_ID: ${{ secrets.FLEET_APP_ID }} + FLEET_APP_PRIVATE_KEY_BASE64: ${{ secrets.FLEET_APP_PRIVATE_KEY_BASE64 }} + FLEET_APP_INSTALLATION_ID: ${{ secrets.FLEET_APP_INSTALLATION_ID }} diff --git a/.github/workflows/fleet-dispatch.yml b/.github/workflows/fleet-dispatch.yml new file mode 100644 index 0000000..f56a49a --- /dev/null +++ b/.github/workflows/fleet-dispatch.yml @@ -0,0 +1,57 @@ +# Generated by @google/jules-fleet init +# https://github.com/google-labs-code/jules-sdk + +name: Fleet Dispatch + +on: + workflow_dispatch: + inputs: + milestone: + description: 'Milestone ID to dispatch (leave empty to dispatch all)' + type: string + required: false + +concurrency: + group: fleet-dispatch + cancel-in-progress: false + +jobs: + discover: + runs-on: ubuntu-latest + outputs: + milestones: ${{ steps.list.outputs.milestones }} + steps: + - name: Resolve milestones + id: list + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INPUT_MILESTONE: ${{ inputs.milestone }} + run: | + if [ -n "$INPUT_MILESTONE" ]; then + echo "milestones=[\"$INPUT_MILESTONE\"]" >> "$GITHUB_OUTPUT" + else + milestones=$(gh api repos/${{ github.repository }}/milestones --jq '[.[].number | tostring]') + echo "milestones=$milestones" >> "$GITHUB_OUTPUT" + fi + + dispatch: + needs: discover + runs-on: ubuntu-latest + strategy: + matrix: + milestone: ${{ fromJSON(needs.discover.outputs.milestones) }} + permissions: + contents: read + issues: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - run: npx -y --package=@google/jules-fleet jules-fleet dispatch --milestone ${{ matrix.milestone }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JULES_API_KEY: ${{ secrets.JULES_API_KEY }} + FLEET_APP_ID: ${{ secrets.FLEET_APP_ID }} + FLEET_APP_PRIVATE_KEY_BASE64: ${{ secrets.FLEET_APP_PRIVATE_KEY_BASE64 }} + FLEET_APP_INSTALLATION_ID: ${{ secrets.FLEET_APP_INSTALLATION_ID }} diff --git a/.github/workflows/fleet-label.yml b/.github/workflows/fleet-label.yml new file mode 100644 index 0000000..6796972 --- /dev/null +++ b/.github/workflows/fleet-label.yml @@ -0,0 +1,45 @@ +name: Fleet Label PR +on: + pull_request: + types: [opened, edited, synchronize] + +permissions: + pull-requests: write + issues: read + +jobs: + label_pr: + runs-on: ubuntu-latest + steps: + - name: Check linked issue and apply label/milestone + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_URL: ${{ github.event.pull_request.html_url }} + REPO: ${{ github.repository }} + run: | + # Use GitHub's own closing keyword resolution to find linked issues + ISSUE_NUMBER=$(gh pr view "$PR_URL" --json closingIssuesReferences --jq '.closingIssuesReferences[0].number // empty') + + if [ -z "$ISSUE_NUMBER" ]; then + echo "No closing issue reference found on this PR. Exiting." + exit 0 + fi + + echo "Found linked issue: #$ISSUE_NUMBER" + + # Check if the linked issue has the 'fleet' label + HAS_FLEET=$(gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json labels --jq '[.labels[].name] | any(. == "fleet")') + + if [ "$HAS_FLEET" = "true" ]; then + echo "Linked issue has 'fleet' label. Applying 'fleet-merge-ready' to PR." + gh pr edit "$PR_URL" --add-label "fleet-merge-ready" + + # Check if linked issue has a milestone and copy it + MILESTONE_TITLE=$(gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json milestone --jq '.milestone.title // empty') + if [ -n "$MILESTONE_TITLE" ]; then + echo "Applying milestone '$MILESTONE_TITLE' to PR." + gh pr edit "$PR_URL" --milestone "$MILESTONE_TITLE" + fi + else + echo "Linked issue does not have 'fleet' label. Ignoring." + fi diff --git a/.github/workflows/fleet-merge.yml b/.github/workflows/fleet-merge.yml new file mode 100644 index 0000000..87ac367 --- /dev/null +++ b/.github/workflows/fleet-merge.yml @@ -0,0 +1,54 @@ +# Generated by @google/jules-fleet init +# https://github.com/google-labs-code/jules-sdk + +name: Fleet Merge + +on: + schedule: + - cron: '0 */3 * * *' + workflow_dispatch: + inputs: + mode: + description: 'PR selection mode' + type: choice + options: + - label + - fleet-run + default: 'label' + fleet_run_id: + description: 'Fleet run ID (required for fleet-run mode)' + type: string + default: '' + redispatch: + description: 'Enable smart conflict resolution' + type: boolean + default: true + +concurrency: + group: fleet-merge + cancel-in-progress: true + +jobs: + merge: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - run: | + REDISPATCH_FLAG="--redispatch" + if [ "${{ inputs.redispatch }}" = "false" ]; then + REDISPATCH_FLAG="" + fi + npx -y --package=@google/jules-fleet jules-fleet merge --mode ${{ inputs.mode || 'label' }} --run-id "${{ inputs.fleet_run_id }}" $REDISPATCH_FLAG + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JULES_API_KEY: ${{ secrets.JULES_API_KEY }} + FLEET_APP_ID: ${{ secrets.FLEET_APP_ID }} + FLEET_APP_PRIVATE_KEY_BASE64: ${{ secrets.FLEET_APP_PRIVATE_KEY_BASE64 }} + FLEET_APP_INSTALLATION_ID: ${{ secrets.FLEET_APP_INSTALLATION_ID }} diff --git a/.github/workflows/jules-merge-conflicts.yml b/.github/workflows/jules-merge-conflicts.yml new file mode 100644 index 0000000..7b31d84 --- /dev/null +++ b/.github/workflows/jules-merge-conflicts.yml @@ -0,0 +1,25 @@ +# Generated by @google/jules-fleet init +# This workflow scans PRs for overlapping file changes. +name: Conflict Detection + +on: + pull_request: + branches: [main] + +permissions: + contents: read + pull-requests: read + +jobs: + scan-overlaps: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Scan for overlapping changes + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + npx -y --package=@google/jules-sdk --package=@modelcontextprotocol/sdk --package=@google/jules-merge jules-merge scan --json '{"prs":[${{ github.event.pull_request.number }}],"repo":"${{ github.repository }}","base":"${{ github.event.pull_request.base.ref }}"}' diff --git a/app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt b/app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt index 642acd1..9bc7daa 100644 --- a/app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt +++ b/app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt @@ -197,16 +197,16 @@ class AwidgetProvider : AppWidgetProvider() { // --- Load Preferences --- val showTime = prefs.getBoolean("show_time", true) - val sizeTime = prefs.getFloat("size_time", 64f) + val sizeTime = prefs.getFloat("size_time", 56f) val showDate = prefs.getBoolean("show_date", true) - val sizeDate = prefs.getFloat("size_date", 14f) + val sizeDate = prefs.getFloat("size_date", 16f) val showBattery = prefs.getBoolean("show_battery", true) - val sizeBattery = prefs.getFloat("size_battery", 24f) - val boldBattery = prefs.getBoolean("bold_battery", false) + val sizeBattery = prefs.getFloat("size_battery", 32f) + val boldBattery = prefs.getBoolean("bold_battery", true) - val showTemp = prefs.getBoolean("show_temp", true) + val showTemp = prefs.getBoolean("show_temp", false) val sizeTemp = prefs.getFloat("size_temp", 18f) val boldTemp = prefs.getBoolean("bold_temp", false) @@ -214,7 +214,7 @@ class AwidgetProvider : AppWidgetProvider() { val sizeWeather = prefs.getFloat("size_weather", 18f) val boldWeather = prefs.getBoolean("bold_weather", false) - var showEvents = prefs.getBoolean("show_events", true) + var showEvents = prefs.getBoolean("show_events", false) if (showEvents && androidx.core.content.ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR) != android.content.pm.PackageManager.PERMISSION_GRANTED) { showEvents = false } @@ -250,7 +250,7 @@ class AwidgetProvider : AppWidgetProvider() { val sizeWorldClock = prefs.getFloat("size_world_clock", 18f) val worldClockZoneStr = prefs.getString("world_clock_zone_str", "UTC") ?: "UTC" - val showStorage = prefs.getBoolean("show_storage", true) + val showStorage = prefs.getBoolean("show_storage", false) val sizeStorage = prefs.getFloat("size_storage", 14f) var showTasks = prefs.getBoolean("show_tasks", false) @@ -286,7 +286,7 @@ class AwidgetProvider : AppWidgetProvider() { val fontStyle = prefs.getInt("font_style", 0) - val bgOpacity = prefs.getFloat("bg_opacity", 100f) + val bgOpacity = prefs.getFloat("bg_opacity", 85f) val textColorPrimaryIdx = prefs.getInt("text_color_primary_idx", 0) val textColorSecondaryIdx = prefs.getInt("text_color_secondary_idx", 0) val bgColorIdx = prefs.getInt("bg_color_idx", 0) @@ -366,7 +366,7 @@ class AwidgetProvider : AppWidgetProvider() { } } - val showOutline = prefs.getBoolean("show_outline", true) + val showOutline = prefs.getBoolean("show_outline", false) val outlineColor = resolveOutlineColor(outlineColorIdx) views.setImageViewResource(R.id.widget_outline, R.drawable.widget_bg_outline) views.setViewVisibility(R.id.widget_outline, if (showOutline) android.view.View.VISIBLE else android.view.View.GONE) @@ -753,12 +753,19 @@ class AwidgetProvider : AppWidgetProvider() { // Calculate cumulative Y positions for each visible item val rightDp = context.resources.displayMetrics.density var cumulativeTopDp = 24f // Starting top margin from top of widget + var isFirstVisible = true for (entry in rightStack) { if (entry.isVisible) { + if (isFirstVisible) { + // Compensate for font intrinsic top padding (matches left side logic) + val intrinsicGap = entry.size * 0.18f + cumulativeTopDp = maxOf(0f, 24f - intrinsicGap) + isFirstVisible = false + } val topPaddingPx = (cumulativeTopDp * rightDp).toInt() views.setViewPadding(entry.viewId, 0, topPaddingPx, 0, 0) // Advance by this item's height + small gap - val itemHeightDp = entry.size * 1.2f // approximate line height + val itemHeightDp = entry.size * 1.15f // approximate line height cumulativeTopDp += itemHeightDp + 2f } } diff --git a/app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt b/app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt index 1c8e68f..56ba133 100644 --- a/app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt +++ b/app/src/main/java/com/leanbitlab/lwidget/MainActivity.kt @@ -341,6 +341,7 @@ class MainActivity : AppCompatActivity() { private fun resetChevron(header: View) { val chevron = header.findViewById(R.id.header_chevron) + ?: header.findViewById(R.id.header_chevron_appearance_presets) ?: header.findViewById(R.id.header_chevron_appearance_outline) ?: header.findViewById(R.id.header_chevron_appearance_colors) ?: header.findViewById(R.id.header_chevron_appearance_theme) @@ -621,17 +622,20 @@ class MainActivity : AppCompatActivity() { } // Appearance subsections if (sectionKey == "appearance") { + val presetsContent = findViewById(R.id.content_appearance_presets) val outlineContent = findViewById(R.id.content_appearance_outline) val colorsContent = findViewById(R.id.content_appearance_colors) val themeContent = findViewById(R.id.content_appearance_theme) val fontContent = findViewById(R.id.content_appearance_font) val transparencyContent = findViewById(R.id.content_appearance_transparency) + presetsContent?.visibility = View.GONE outlineContent?.visibility = View.GONE colorsContent?.visibility = View.GONE themeContent?.visibility = View.GONE fontContent?.visibility = View.GONE transparencyContent?.visibility = View.GONE prefs.edit() + .putBoolean("section_appearance_presets_expanded", false) .putBoolean("section_appearance_outline_expanded", false) .putBoolean("section_appearance_colors_expanded", false) .putBoolean("section_appearance_theme_expanded", false) @@ -640,6 +644,7 @@ class MainActivity : AppCompatActivity() { .apply() // Reset nested chevrons listOf( + R.id.header_chevron_appearance_presets, R.id.header_chevron_appearance_outline, R.id.header_chevron_appearance_colors, R.id.header_chevron_appearance_theme, @@ -660,10 +665,10 @@ class MainActivity : AppCompatActivity() { private fun bindReorderSection() { val defaultOrder = listOf( ReorderItem("show_battery", getString(R.string.section_battery), prefs.getBoolean("show_battery", true)), - ReorderItem("show_temp", getString(R.string.section_temp), prefs.getBoolean("show_temp", true)), + ReorderItem("show_temp", getString(R.string.section_temp), prefs.getBoolean("show_temp", false)), ReorderItem("show_weather_condition", getString(R.string.section_weather_condition), prefs.getBoolean("show_weather_condition", false)), ReorderItem("show_data_usage", getString(R.string.section_data_usage), prefs.getBoolean("show_data_usage", false)), - ReorderItem("show_storage", getString(R.string.section_storage), prefs.getBoolean("show_storage", true)), + ReorderItem("show_storage", getString(R.string.section_storage), prefs.getBoolean("show_storage", false)), ReorderItem("show_steps", getString(R.string.section_steps), prefs.getBoolean("show_steps", false)), ReorderItem("show_screen_time", getString(R.string.section_screen_time), prefs.getBoolean("show_screen_time", false)) ) @@ -781,7 +786,7 @@ class MainActivity : AppCompatActivity() { R.id.header_time, R.drawable.ic_time, getString(R.string.section_time), R.id.content_time, R.id.row_time_toggle, "show_time", true, - sizeRowId = R.id.row_time_size, prefSizeKey = "size_time", defSize = 64f, minSize = 12f, maxSize = 120f, + sizeRowId = R.id.row_time_size, prefSizeKey = "size_time", defSize = 56f, minSize = 12f, maxSize = 120f, selectorRowId = R.id.row_time_format, selectorOptions = timeFormatOptions, prefSelectorKey = "time_format_idx", defSelectorIdx = 0, isContent = true ) @@ -813,7 +818,7 @@ class MainActivity : AppCompatActivity() { R.id.header_date, R.drawable.ic_date, getString(R.string.section_date), R.id.content_date, R.id.row_date_toggle, "show_date", true, - sizeRowId = R.id.row_date_size, prefSizeKey = "size_date", defSize = 14f, minSize = 10f, maxSize = 24f, + sizeRowId = R.id.row_date_size, prefSizeKey = "size_date", defSize = 16f, minSize = 10f, maxSize = 24f, selectorRowId = R.id.row_date_format, selectorOptions = dateFormatOptions, prefSelectorKey = "date_format_idx", defSelectorIdx = 0, isContent = true ) @@ -824,17 +829,17 @@ class MainActivity : AppCompatActivity() { R.id.header_battery, R.drawable.ic_battery, getString(R.string.section_battery), R.id.content_battery, R.id.row_battery_toggle, "show_battery", true, - sizeRowId = R.id.row_battery_size, prefSizeKey = "size_battery", defSize = 24f, minSize = 10f, maxSize = 74f, + sizeRowId = R.id.row_battery_size, prefSizeKey = "size_battery", defSize = 32f, minSize = 10f, maxSize = 74f, isContent = true ).also { it.tag = "battery" } - bindToggle(R.id.row_battery_bold, "Bold Text", "bold_battery", false) + bindToggle(R.id.row_battery_bold, "Bold Text", "bold_battery", true) } private fun setupTempSection() { // Temp bindFoldedSection( R.id.header_temp, R.drawable.ic_temp, getString(R.string.section_temp), R.id.content_temp, R.id.row_temp_toggle, - "show_temp", true, + "show_temp", false, sizeRowId = R.id.row_temp_size, prefSizeKey = "size_temp", defSize = 18f, minSize = 10f, maxSize = 74f, isContent = true ).also { it.tag = "temp" } @@ -939,7 +944,7 @@ class MainActivity : AppCompatActivity() { bindFoldedSection( R.id.header_storage, R.drawable.ic_storage, getString(R.string.section_storage), R.id.content_storage, R.id.row_storage_toggle, - "show_storage", true, + "show_storage", false, sizeRowId = R.id.row_storage_size, prefSizeKey = "size_storage", defSize = 14f, minSize = 10f, maxSize = 74f, isContent = true ).also { it.tag = "storage" } @@ -1047,17 +1052,9 @@ class MainActivity : AppCompatActivity() { keepAliveSwitch.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { - val neededPermissions = mutableListOf() - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q && - ContextCompat.checkSelfPermission(this, Manifest.permission.ACTIVITY_RECOGNITION) != PackageManager.PERMISSION_GRANTED) { - neededPermissions.add(Manifest.permission.ACTIVITY_RECOGNITION) - } if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { - neededPermissions.add(Manifest.permission.POST_NOTIFICATIONS) - } - if (neededPermissions.isNotEmpty()) { - ActivityCompat.requestPermissions(this, neededPermissions.toTypedArray(), 104) + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 104) keepAliveSwitch.isChecked = false return@setOnCheckedChangeListener } @@ -1186,6 +1183,8 @@ class MainActivity : AppCompatActivity() { } // Appearance Subsections (nested cards) + bindNestedCard(R.id.header_appearance_presets, "PRESETS", R.id.content_appearance_presets, "section_appearance_presets_expanded", R.id.header_chevron_appearance_presets) + setupPresetsSection() bindNestedCard(R.id.header_appearance_outline, "OUTLINE", R.id.content_appearance_outline, "section_appearance_outline_expanded", R.id.header_chevron_appearance_outline) bindNestedCard(R.id.header_appearance_colors, "COLORS", R.id.content_appearance_colors, "section_appearance_colors_expanded", R.id.header_chevron_appearance_colors) bindNestedCard(R.id.header_appearance_theme, "THEME", R.id.content_appearance_theme, "section_appearance_theme_expanded", R.id.header_chevron_appearance_theme) @@ -1197,7 +1196,7 @@ class MainActivity : AppCompatActivity() { bindReorderSection() // Outline toggle - bindToggle(R.id.row_outline_toggle, "Show Outline", "show_outline", true) { isChecked -> + bindToggle(R.id.row_outline_toggle, "Show Outline", "show_outline", false) { isChecked -> updateWidget() } @@ -1227,7 +1226,7 @@ class MainActivity : AppCompatActivity() { } // BG Transparency - bindSlider(R.id.row_bg_transparency, "Background Opacity", "bg_opacity", 100f, 0f, 100f) + bindSlider(R.id.row_bg_transparency, "Background Opacity", "bg_opacity", 85f, 0f, 100f) // Background Color val bgSliderRow = findViewById(R.id.row_bg_color_custom) @@ -1287,6 +1286,171 @@ class MainActivity : AppCompatActivity() { updateToggleAvailability() } + private fun setupPresetsSection() { + data class Preset( + val key: String, + val label: String, + val prefs: Map + ) + + val presets = listOf( + // Minimal: just time+date, white text, fully transparent, thin font + Preset("minimal", "Minimal", mapOf( + "show_time" to true, "size_time" to 58f, + "show_date" to true, "size_date" to 16f, + "show_battery" to false, "show_temp" to false, + "show_storage" to false, "show_data_usage" to false, + "show_steps" to false, "show_screen_time" to false, + "show_next_alarm" to false, "show_world_clock" to false, + "show_events" to false, "show_tasks" to false, + "show_outline" to false, "bg_opacity" to 0f, + "font_style" to 9, // Thin + "use_dynamic_colors" to false, + "text_color_primary_idx" to 2, "text_color_primary_r" to 255, "text_color_primary_g" to 255, "text_color_primary_b" to 255, + "text_color_secondary_idx" to 2, "text_color_secondary_r" to 200, "text_color_secondary_g" to 200, "text_color_secondary_b" to 200, + "date_color_idx" to 2, "date_color_r" to 180, "date_color_g" to 180, "date_color_b" to 190, + "bold_battery" to false, "bold_temp" to false + )), + // Neon: cyan time, magenta date, dark bg, bold condensed font + Preset("neon", "Neon", mapOf( + "show_time" to true, "size_time" to 64f, + "show_date" to true, "size_date" to 14f, + "show_battery" to true, "size_battery" to 28f, "bold_battery" to true, + "show_temp" to true, "size_temp" to 18f, "bold_temp" to true, + "show_storage" to false, "show_data_usage" to false, + "show_steps" to false, "show_screen_time" to false, + "show_next_alarm" to true, "size_next_alarm" to 12f, + "show_world_clock" to false, + "show_events" to false, "show_tasks" to false, + "show_outline" to true, "bg_opacity" to 95f, + "font_style" to 4, // Condensed + "use_dynamic_colors" to false, + "text_color_primary_idx" to 2, "text_color_primary_r" to 0, "text_color_primary_g" to 255, "text_color_primary_b" to 255, + "text_color_secondary_idx" to 2, "text_color_secondary_r" to 0, "text_color_secondary_g" to 200, "text_color_secondary_b" to 200, + "date_color_idx" to 2, "date_color_r" to 255, "date_color_g" to 0, "date_color_b" to 180, + "outline_color_idx" to 2, "outline_color_r" to 0, "outline_color_g" to 200, "outline_color_b" to 255, + "bg_color_idx" to 2, "bg_color_r" to 10, "bg_color_g" to 10, "bg_color_b" to 20, + "widget_right_column_order" to "show_battery,show_temp,show_weather_condition,show_data_usage,show_storage,show_steps,show_screen_time" + )), + // Cockpit: green on dark, monospace, info-heavy, terminal look + Preset("cockpit", "Cockpit", mapOf( + "show_time" to true, "size_time" to 42f, + "show_date" to true, "size_date" to 14f, + "show_battery" to true, "size_battery" to 18f, "bold_battery" to false, + "show_temp" to true, "size_temp" to 16f, "bold_temp" to false, + "show_storage" to true, "size_storage" to 14f, "bold_storage" to false, + "show_data_usage" to true, "size_data" to 14f, "bold_data_usage" to false, + "show_steps" to false, "show_screen_time" to false, + "show_next_alarm" to true, "size_next_alarm" to 14f, + "show_world_clock" to false, + "show_events" to false, "show_tasks" to false, + "show_outline" to true, "bg_opacity" to 90f, + "font_style" to 2, // Monospace + "use_dynamic_colors" to false, + "text_color_primary_idx" to 2, "text_color_primary_r" to 0, "text_color_primary_g" to 255, "text_color_primary_b" to 65, + "text_color_secondary_idx" to 2, "text_color_secondary_r" to 0, "text_color_secondary_g" to 180, "text_color_secondary_b" to 50, + "date_color_idx" to 2, "date_color_r" to 0, "date_color_g" to 200, "date_color_b" to 80, + "outline_color_idx" to 2, "outline_color_r" to 0, "outline_color_g" to 120, "outline_color_b" to 40, + "bg_color_idx" to 2, "bg_color_r" to 5, "bg_color_g" to 15, "bg_color_b" to 5, + "widget_right_column_order" to "show_battery,show_storage,show_data_usage,show_temp,show_weather_condition,show_steps,show_screen_time" + )), + // Sunset: warm oranges/gold, serif font, elegant minimal + Preset("sunset", "Sunset", mapOf( + "show_time" to true, "size_time" to 54f, + "show_date" to true, "size_date" to 18f, + "show_battery" to true, "size_battery" to 24f, "bold_battery" to true, + "show_temp" to false, "show_storage" to false, + "show_data_usage" to false, "show_steps" to false, + "show_screen_time" to false, + "show_next_alarm" to true, "size_next_alarm" to 14f, + "show_world_clock" to false, + "show_events" to false, "show_tasks" to false, + "show_outline" to false, "bg_opacity" to 70f, + "font_style" to 1, // Serif + "use_dynamic_colors" to false, + "text_color_primary_idx" to 2, "text_color_primary_r" to 255, "text_color_primary_g" to 180, "text_color_primary_b" to 50, + "text_color_secondary_idx" to 2, "text_color_secondary_r" to 230, "text_color_secondary_g" to 140, "text_color_secondary_b" to 60, + "date_color_idx" to 2, "date_color_r" to 255, "date_color_g" to 120, "date_color_b" to 50, + "bg_color_idx" to 2, "bg_color_r" to 30, "bg_color_g" to 15, "bg_color_b" to 8, + "widget_right_column_order" to "show_battery,show_temp,show_weather_condition,show_data_usage,show_storage,show_steps,show_screen_time" + )), + // Monochrome: white outline, all white text, medium font, classic layout + Preset("monochrome", "Monochrome", mapOf( + "show_time" to true, "size_time" to 48f, + "show_date" to true, "size_date" to 14f, + "show_battery" to true, "size_battery" to 22f, "bold_battery" to false, + "show_temp" to false, "show_storage" to true, "size_storage" to 14f, + "show_data_usage" to false, "show_steps" to false, + "show_screen_time" to false, + "show_next_alarm" to true, "size_next_alarm" to 14f, + "show_world_clock" to false, + "show_events" to false, "show_tasks" to false, + "show_outline" to true, "bg_opacity" to 50f, + "font_style" to 7, // Medium + "use_dynamic_colors" to false, + "text_color_primary_idx" to 2, "text_color_primary_r" to 240, "text_color_primary_g" to 240, "text_color_primary_b" to 240, + "text_color_secondary_idx" to 2, "text_color_secondary_r" to 170, "text_color_secondary_g" to 170, "text_color_secondary_b" to 170, + "date_color_idx" to 2, "date_color_r" to 200, "date_color_g" to 200, "date_color_b" to 200, + "outline_color_idx" to 2, "outline_color_r" to 100, "outline_color_g" to 100, "outline_color_b" to 100, + "bg_color_idx" to 2, "bg_color_r" to 25, "bg_color_g" to 25, "bg_color_b" to 25, + "widget_right_column_order" to "show_battery,show_storage,show_temp,show_weather_condition,show_data_usage,show_steps,show_screen_time" + )), + // Snowfall: icy blues, light font, airy feel + Preset("snowfall", "Snowfall", mapOf( + "show_time" to true, "size_time" to 60f, + "show_date" to true, "size_date" to 16f, + "show_battery" to false, "show_temp" to true, "size_temp" to 20f, "bold_temp" to false, + "show_storage" to false, "show_data_usage" to false, + "show_steps" to false, "show_screen_time" to false, + "show_next_alarm" to false, + "show_world_clock" to false, + "show_events" to false, "show_tasks" to false, + "show_outline" to false, "bg_opacity" to 60f, + "font_style" to 6, // Light + "use_dynamic_colors" to false, + "text_color_primary_idx" to 2, "text_color_primary_r" to 180, "text_color_primary_g" to 220, "text_color_primary_b" to 255, + "text_color_secondary_idx" to 2, "text_color_secondary_r" to 130, "text_color_secondary_g" to 180, "text_color_secondary_b" to 230, + "date_color_idx" to 2, "date_color_r" to 100, "date_color_g" to 170, "date_color_b" to 255, + "bg_color_idx" to 2, "bg_color_r" to 10, "bg_color_g" to 20, "bg_color_b" to 40, + "widget_right_column_order" to "show_temp,show_battery,show_weather_condition,show_data_usage,show_storage,show_steps,show_screen_time" + )) + ) + + val chipGroup = findViewById(R.id.preset_chip_group) + chipGroup.removeAllViews() + val activePreset = prefs.getString("active_preset", null) + + for (preset in presets) { + val chip = com.google.android.material.chip.Chip(this).apply { + text = preset.label + isCheckable = true + isChecked = (preset.key == activePreset) + chipBackgroundColor = android.content.res.ColorStateList.valueOf( + com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurfaceContainerHighest) + ) + setTextColor(com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurface)) + checkedIcon = androidx.core.content.ContextCompat.getDrawable(context, android.R.drawable.checkbox_on_background) + isCheckedIconVisible = true + setOnClickListener { + val editor = prefs.edit() + for ((k, v) in preset.prefs) { + when (v) { + is Boolean -> editor.putBoolean(k, v) + is Float -> editor.putFloat(k, v) + is Int -> editor.putInt(k, v) + is String -> editor.putString(k, v) + } + } + editor.putString("active_preset", preset.key) + editor.apply() + updateWidget() + recreate() + } + } + chipGroup.addView(chip) + } + } + private fun bindToggle( viewId: Int, title: String, prefShowKey: String, defShow: Boolean, isContent: Boolean = false, diff --git a/app/src/main/java/com/leanbitlab/lwidget/SetupActivity.kt b/app/src/main/java/com/leanbitlab/lwidget/SetupActivity.kt index 605388e..b00824d 100644 --- a/app/src/main/java/com/leanbitlab/lwidget/SetupActivity.kt +++ b/app/src/main/java/com/leanbitlab/lwidget/SetupActivity.kt @@ -59,17 +59,9 @@ class SetupActivity : AppCompatActivity() { switchKeepAlive.isChecked = prefs.getBoolean("keep_alive", false) switchKeepAlive.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { - val neededPermissions = mutableListOf() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && - ContextCompat.checkSelfPermission(this, Manifest.permission.ACTIVITY_RECOGNITION) != PackageManager.PERMISSION_GRANTED) { - neededPermissions.add(Manifest.permission.ACTIVITY_RECOGNITION) - } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { - neededPermissions.add(Manifest.permission.POST_NOTIFICATIONS) - } - if (neededPermissions.isNotEmpty()) { - requestPermissionLauncher.launch(neededPermissions.toTypedArray()) + requestPermissionLauncher.launch(arrayOf(Manifest.permission.POST_NOTIFICATIONS)) } } prefs.edit().putBoolean("keep_alive", isChecked).apply() @@ -88,8 +80,43 @@ class SetupActivity : AppCompatActivity() { } findViewById(R.id.btn_grant_steps).setOnClickListener { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - requestPermissionLauncher.launch(arrayOf(Manifest.permission.ACTIVITY_RECOGNITION)) + val neededPermissions = mutableListOf() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && + ContextCompat.checkSelfPermission(this, Manifest.permission.ACTIVITY_RECOGNITION) != PackageManager.PERMISSION_GRANTED) { + neededPermissions.add(Manifest.permission.ACTIVITY_RECOGNITION) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + neededPermissions.add(Manifest.permission.POST_NOTIFICATIONS) + } + if (neededPermissions.isNotEmpty()) { + requestPermissionLauncher.launch(neededPermissions.toTypedArray()) + } + } + + // Data Usage & Screen Time (both need Usage Stats) + findViewById(R.id.btn_grant_data_usage).setOnClickListener { + try { + startActivity(Intent(android.provider.Settings.ACTION_USAGE_ACCESS_SETTINGS)) + } catch (e: Exception) {} + } + + findViewById(R.id.btn_grant_screen_time).setOnClickListener { + try { + startActivity(Intent(android.provider.Settings.ACTION_USAGE_ACCESS_SETTINGS)) + } catch (e: Exception) {} + } + + // Weather (Breezy Weather provider) + findViewById(R.id.btn_grant_weather).setOnClickListener { + if (packageManager.getLaunchIntentForPackage("org.breezyweather") != null) { + requestPermissionLauncher.launch(arrayOf("org.breezyweather.READ_PROVIDER")) + } else { + com.google.android.material.snackbar.Snackbar.make( + findViewById(R.id.setup_view_flipper), + "Breezy Weather app is required.", + com.google.android.material.snackbar.Snackbar.LENGTH_LONG + ).show() } } @@ -114,6 +141,9 @@ class SetupActivity : AppCompatActivity() { val btnCalendar = findViewById(R.id.btn_grant_calendar) val btnTasks = findViewById(R.id.btn_grant_tasks) val btnSteps = findViewById(R.id.btn_grant_steps) + val btnDataUsage = findViewById(R.id.btn_grant_data_usage) + val btnScreenTime = findViewById(R.id.btn_grant_screen_time) + val btnWeather = findViewById(R.id.btn_grant_weather) if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED) { btnCalendar.text = "Granted" @@ -140,6 +170,35 @@ class SetupActivity : AppCompatActivity() { btnSteps.isEnabled = false prefs.edit().putBoolean("show_steps", true).apply() } + + if (hasUsageStatsPermission()) { + btnDataUsage.text = "Granted" + btnDataUsage.isEnabled = false + prefs.edit().putBoolean("show_data_usage", true).apply() + + btnScreenTime.text = "Granted" + btnScreenTime.isEnabled = false + prefs.edit().putBoolean("show_screen_time", true).apply() + } + + if (ContextCompat.checkSelfPermission(this, "org.breezyweather.READ_PROVIDER") == PackageManager.PERMISSION_GRANTED) { + btnWeather.text = "Granted" + btnWeather.isEnabled = false + prefs.edit().putBoolean("show_weather_condition", true).apply() + } + } + + private fun hasUsageStatsPermission(): Boolean { + val appOps = getSystemService(Context.APP_OPS_SERVICE) as android.app.AppOpsManager + val opMode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + appOps.unsafeCheckOpNoThrow(android.app.AppOpsManager.OPSTR_GET_USAGE_STATS, + android.os.Process.myUid(), packageName) + } else { + @Suppress("DEPRECATION") + appOps.checkOpNoThrow(android.app.AppOpsManager.OPSTR_GET_USAGE_STATS, + android.os.Process.myUid(), packageName) + } + return opMode == android.app.AppOpsManager.MODE_ALLOWED } private fun finishSetup() { diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml new file mode 100644 index 0000000..f573f1a --- /dev/null +++ b/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/anim/slide_out_left.xml b/app/src/main/res/anim/slide_out_left.xml new file mode 100644 index 0000000..52354d5 --- /dev/null +++ b/app/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index e50b815..dc98ffe 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1092,6 +1092,83 @@ android:paddingEnd="8dp" android:paddingBottom="8dp"> + + + + + + + + + + + + + + + + + + + + + + @@ -30,8 +29,8 @@ android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" - android:inAnimation="@android:anim/slide_in_left" - android:outAnimation="@android:anim/slide_out_right"> + android:inAnimation="@anim/slide_in_right" + android:outAnimation="@anim/slide_out_left"> + + + + + + + + + + + + + + + + + + + + + + + + + + +