diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 8852fbe..8834df8 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -27,10 +27,6 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/CurrentBookWidgetProvider.kt b/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/CurrentBookWidgetProvider.kt
index a8ceaeb..f05bd6b 100644
--- a/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/CurrentBookWidgetProvider.kt
+++ b/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/CurrentBookWidgetProvider.kt
@@ -3,13 +3,6 @@ package de.doen1el.calibreWebCompanion
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.SharedPreferences
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
-import android.graphics.BitmapShader
-import android.graphics.Canvas
-import android.graphics.Paint
-import android.graphics.RectF
-import android.graphics.Shader
import android.net.Uri
import android.view.View
import android.widget.RemoteViews
@@ -44,13 +37,16 @@ class CurrentBookWidgetProvider : HomeWidgetProvider() {
val decoded =
if (coverPath.isNotEmpty() && File(coverPath).exists()) {
- decodeScaledCover(coverPath)
+ WidgetImages.decodeScaledCover(coverPath)
} else {
null
}
if (decoded != null) {
val radius = 10f * context.resources.displayMetrics.density
- views.setImageViewBitmap(R.id.widget_cover, roundBitmap(decoded, radius))
+ views.setImageViewBitmap(
+ R.id.widget_cover,
+ WidgetImages.roundBitmap(decoded, radius)
+ )
} else {
views.setImageViewResource(R.id.widget_cover, R.drawable.widget_cover_placeholder)
}
@@ -80,40 +76,4 @@ class CurrentBookWidgetProvider : HomeWidgetProvider() {
appWidgetManager.updateAppWidget(widgetId, views)
}
}
-
- private fun decodeScaledCover(path: String, maxDimen: Int = 512): Bitmap? {
- val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
- BitmapFactory.decodeFile(path, bounds)
- if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return null
-
- var sampleSize = 1
- val longestEdge = maxOf(bounds.outWidth, bounds.outHeight)
- while (longestEdge / sampleSize > maxDimen * 2) {
- sampleSize *= 2
- }
-
- val options = BitmapFactory.Options().apply { inSampleSize = sampleSize }
- val decoded = BitmapFactory.decodeFile(path, options) ?: return null
-
- val longestDecoded = maxOf(decoded.width, decoded.height)
- if (longestDecoded <= maxDimen) return decoded
-
- val scale = maxDimen.toFloat() / longestDecoded
- val width = (decoded.width * scale).toInt().coerceAtLeast(1)
- val height = (decoded.height * scale).toInt().coerceAtLeast(1)
- val scaled = Bitmap.createScaledBitmap(decoded, width, height, true)
- if (scaled != decoded) decoded.recycle()
- return scaled
- }
-
- private fun roundBitmap(src: Bitmap, radius: Float): Bitmap {
- val output = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)
- val canvas = Canvas(output)
- val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
- shader = BitmapShader(src, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
- }
- val rect = RectF(0f, 0f, src.width.toFloat(), src.height.toFloat())
- canvas.drawRoundRect(rect, radius, radius, paint)
- return output
- }
}
diff --git a/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/MainActivity.kt b/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/MainActivity.kt
index 8b4eef1..e772b57 100644
--- a/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/MainActivity.kt
+++ b/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/MainActivity.kt
@@ -1,5 +1,32 @@
package de.doen1el.calibreWebCompanion
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import es.antonborri.home_widget.HomeWidgetLaunchIntent
import io.flutter.embedding.android.FlutterActivity
-class MainActivity : FlutterActivity()
+class MainActivity : FlutterActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ restoreWidgetUri(intent)
+ super.onCreate(savedInstanceState)
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ restoreWidgetUri(intent)
+ super.onNewIntent(intent)
+ }
+
+ private fun restoreWidgetUri(intent: Intent?) {
+ if (intent == null) return
+ if (intent.action != HomeWidgetLaunchIntent.HOME_WIDGET_LAUNCH_ACTION) return
+ if (intent.data != null) return
+
+ val uri = intent.getStringExtra(EXTRA_WIDGET_URI) ?: return
+ intent.data = Uri.parse(uri)
+ }
+
+ companion object {
+ const val EXTRA_WIDGET_URI = "widget_uri"
+ }
+}
diff --git a/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/QuickActionsWidgetProvider.kt b/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/QuickActionsWidgetProvider.kt
new file mode 100644
index 0000000..73541fd
--- /dev/null
+++ b/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/QuickActionsWidgetProvider.kt
@@ -0,0 +1,69 @@
+package de.doen1el.calibreWebCompanion
+
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.content.SharedPreferences
+import android.net.Uri
+import android.view.View
+import android.widget.RemoteViews
+import es.antonborri.home_widget.HomeWidgetLaunchIntent
+import es.antonborri.home_widget.HomeWidgetProvider
+
+class QuickActionsWidgetProvider : HomeWidgetProvider() {
+
+ private data class Action(
+ val slotId: Int,
+ val tileId: Int,
+ val iconId: Int,
+ val labelId: Int,
+ val key: String
+ )
+
+ private val actions = listOf(
+ Action(R.id.qa_slot_search, R.id.qa_tile_search, R.id.qa_icon_search, R.id.qa_label_search, "search"),
+ Action(R.id.qa_slot_scan, R.id.qa_tile_scan, R.id.qa_icon_scan, R.id.qa_label_scan, "scan"),
+ Action(R.id.qa_slot_read, R.id.qa_tile_read, R.id.qa_icon_read, R.id.qa_label_read, "read"),
+ Action(R.id.qa_slot_downloads, R.id.qa_tile_downloads, R.id.qa_icon_downloads, R.id.qa_label_downloads, "downloads")
+ )
+
+ override fun onUpdate(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetIds: IntArray,
+ widgetData: SharedPreferences
+ ) {
+ val downloadsEnabled = widgetData.getString("qa_downloads", "0") == "1"
+ val palette = WidgetTheming.palette(context, widgetData)
+
+ for (widgetId in appWidgetIds) {
+ val views = RemoteViews(context.packageName, R.layout.widget_quick_actions)
+
+ palette?.let { views.setInt(R.id.widget_bg, "setColorFilter", it.background) }
+
+ for (action in actions) {
+ if (action.key == "downloads" && !downloadsEnabled) {
+ views.setViewVisibility(action.slotId, View.GONE)
+ continue
+ }
+
+ views.setViewVisibility(action.slotId, View.VISIBLE)
+ views.setOnClickPendingIntent(
+ action.slotId,
+ HomeWidgetLaunchIntent.getActivity(
+ context,
+ MainActivity::class.java,
+ Uri.parse("calibrewebcompanion://widget/action?do=${action.key}")
+ )
+ )
+
+ palette?.let { p ->
+ views.setInt(action.tileId, "setColorFilter", p.tile)
+ views.setInt(action.iconId, "setColorFilter", p.onTile)
+ views.setTextColor(action.labelId, p.onBackground)
+ }
+ }
+
+ appWidgetManager.updateAppWidget(widgetId, views)
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/ShelfWidgetProvider.kt b/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/ShelfWidgetProvider.kt
new file mode 100644
index 0000000..e3f2214
--- /dev/null
+++ b/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/ShelfWidgetProvider.kt
@@ -0,0 +1,79 @@
+package de.doen1el.calibreWebCompanion
+
+import android.app.PendingIntent
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.net.Uri
+import android.widget.RemoteViews
+import es.antonborri.home_widget.HomeWidgetBackgroundIntent
+import es.antonborri.home_widget.HomeWidgetLaunchIntent
+import es.antonborri.home_widget.HomeWidgetProvider
+
+class ShelfWidgetProvider : HomeWidgetProvider() {
+ override fun onUpdate(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetIds: IntArray,
+ widgetData: SharedPreferences
+ ) {
+ for (widgetId in appWidgetIds) {
+ val views = RemoteViews(context.packageName, R.layout.widget_shelf)
+
+ val title = widgetData.getString("sh_title", "") ?: ""
+ views.setTextViewText(
+ R.id.shelf_title,
+ if (title.isEmpty()) context.getString(R.string.widget_shelf_recent) else title
+ )
+
+ val adapterIntent = Intent(context, ShelfWidgetService::class.java).apply {
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
+ // Makes the intent unique per widget so each keeps its own adapter.
+ data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
+ }
+ views.setRemoteAdapter(R.id.shelf_grid, adapterIntent)
+ views.setEmptyView(R.id.shelf_grid, R.id.shelf_empty)
+
+ val template = Intent(context, MainActivity::class.java).apply {
+ action = HomeWidgetLaunchIntent.HOME_WIDGET_LAUNCH_ACTION
+ }
+ views.setPendingIntentTemplate(
+ R.id.shelf_grid,
+ PendingIntent.getActivity(
+ context,
+ 0,
+ template,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
+ )
+ )
+
+ WidgetTheming.palette(context, widgetData)?.let { p ->
+ views.setInt(R.id.widget_bg, "setColorFilter", p.background)
+ views.setTextColor(R.id.shelf_title, p.onBackground)
+ views.setTextColor(R.id.shelf_empty, WidgetTheming.muted(p.onBackground))
+ views.setInt(R.id.shelf_refresh, "setColorFilter", p.accent)
+ }
+
+ // Reloads the shelf in a background isolate, without opening the app.
+ val refresh = HomeWidgetBackgroundIntent.getBroadcast(
+ context,
+ Uri.parse("calibrewebcompanion://widget/refresh")
+ )
+ views.setOnClickPendingIntent(R.id.shelf_refresh, refresh)
+ views.setOnClickPendingIntent(R.id.shelf_empty, refresh)
+
+ views.setOnClickPendingIntent(
+ R.id.shelf_header,
+ HomeWidgetLaunchIntent.getActivity(
+ context,
+ MainActivity::class.java,
+ Uri.parse("calibrewebcompanion://widget/shelf")
+ )
+ )
+
+ appWidgetManager.updateAppWidget(widgetId, views)
+ appWidgetManager.notifyAppWidgetViewDataChanged(widgetId, R.id.shelf_grid)
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/ShelfWidgetService.kt b/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/ShelfWidgetService.kt
new file mode 100644
index 0000000..a3f082a
--- /dev/null
+++ b/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/ShelfWidgetService.kt
@@ -0,0 +1,122 @@
+package de.doen1el.calibreWebCompanion
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.widget.RemoteViews
+import android.widget.RemoteViewsService
+import es.antonborri.home_widget.HomeWidgetPlugin
+import org.json.JSONArray
+import java.io.File
+import kotlin.math.sqrt
+
+private const val MIN_COVER_DIMEN = 96
+private const val MAX_COVER_DIMEN = 256
+
+class ShelfWidgetService : RemoteViewsService() {
+ override fun onGetViewFactory(intent: Intent): RemoteViewsFactory =
+ ShelfRemoteViewsFactory(applicationContext)
+}
+
+private data class ShelfItem(
+ val uuid: String,
+ val title: String,
+ val authors: String,
+ val cover: String
+)
+
+private class ShelfRemoteViewsFactory(private val context: Context) :
+ RemoteViewsService.RemoteViewsFactory {
+
+ private var items: List = emptyList()
+ private var palette: WidgetTheming.Palette? = null
+ private var coverDimen: Int = MAX_COVER_DIMEN
+
+ override fun onCreate() = Unit
+
+ override fun onDataSetChanged() {
+ val data = HomeWidgetPlugin.getData(context)
+ items = parse(data.getString("sh_json", "") ?: "")
+ palette = WidgetTheming.palette(context, data)
+ coverDimen = coverDimenFor(items.size)
+ }
+
+ private fun coverDimenFor(count: Int): Int {
+ if (count <= 0) return MAX_COVER_DIMEN
+
+ val metrics = context.resources.displayMetrics
+ val hostLimit =
+ metrics.widthPixels.toLong() * metrics.heightPixels.toLong() * 4L * 3L / 2L
+ val perItem = hostLimit * 30 / 100 / count
+ val height = sqrt(perItem / 2.7).toInt()
+
+ return height.coerceIn(MIN_COVER_DIMEN, MAX_COVER_DIMEN)
+ }
+
+ override fun onDestroy() {
+ items = emptyList()
+ }
+
+ override fun getCount(): Int = items.size
+
+ override fun getViewAt(position: Int): RemoteViews {
+ val views = RemoteViews(context.packageName, R.layout.widget_shelf_item)
+ val item = items.getOrNull(position) ?: return views
+
+ views.setTextViewText(R.id.item_title, item.title)
+ views.setTextViewText(R.id.item_authors, item.authors)
+ views.setContentDescription(R.id.item_cover, item.title)
+
+ val decoded =
+ if (item.cover.isNotEmpty() && File(item.cover).exists()) {
+ WidgetImages.decodeScaledCover(item.cover, maxDimen = coverDimen)
+ } else {
+ null
+ }
+ if (decoded != null) {
+ val radius = 8f * context.resources.displayMetrics.density
+ views.setImageViewBitmap(R.id.item_cover, WidgetImages.roundBitmap(decoded, radius))
+ } else {
+ views.setImageViewResource(R.id.item_cover, R.drawable.widget_cover_placeholder)
+ }
+
+ palette?.let { p ->
+ views.setTextColor(R.id.item_title, p.onBackground)
+ views.setTextColor(R.id.item_authors, WidgetTheming.muted(p.onBackground))
+ }
+
+ val target = "calibrewebcompanion://widget/book?uuid=${item.uuid}"
+ views.setOnClickFillInIntent(
+ R.id.item_root,
+ Intent()
+ .setData(Uri.parse(target))
+ .putExtra(MainActivity.EXTRA_WIDGET_URI, target)
+ )
+
+ return views
+ }
+
+ override fun getLoadingView(): RemoteViews? = null
+
+ override fun getViewTypeCount(): Int = 1
+
+ override fun getItemId(position: Int): Long = position.toLong()
+
+ override fun hasStableIds(): Boolean = true
+
+ private fun parse(json: String): List {
+ if (json.isEmpty()) return emptyList()
+ return runCatching {
+ val array = JSONArray(json)
+ (0 until array.length()).mapNotNull { index ->
+ val entry = array.optJSONObject(index) ?: return@mapNotNull null
+ ShelfItem(
+ uuid = entry.optString("uuid"),
+ title = entry.optString("title"),
+ authors = entry.optString("authors"),
+ cover = entry.optString("cover")
+ )
+ }
+ }.getOrDefault(emptyList())
+ }
+}
diff --git a/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/WidgetImages.kt b/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/WidgetImages.kt
new file mode 100644
index 0000000..0f9a6b4
--- /dev/null
+++ b/android/app/src/main/kotlin/de/doen1el/calibreWebCompanion/WidgetImages.kt
@@ -0,0 +1,47 @@
+package de.doen1el.calibreWebCompanion
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.BitmapShader
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.RectF
+import android.graphics.Shader
+
+object WidgetImages {
+ fun decodeScaledCover(path: String, maxDimen: Int = 512): Bitmap? {
+ val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
+ BitmapFactory.decodeFile(path, bounds)
+ if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return null
+
+ var sampleSize = 1
+ val longestEdge = maxOf(bounds.outWidth, bounds.outHeight)
+ while (longestEdge / sampleSize > maxDimen * 2) {
+ sampleSize *= 2
+ }
+
+ val options = BitmapFactory.Options().apply { inSampleSize = sampleSize }
+ val decoded = BitmapFactory.decodeFile(path, options) ?: return null
+
+ val longestDecoded = maxOf(decoded.width, decoded.height)
+ if (longestDecoded <= maxDimen) return decoded
+
+ val scale = maxDimen.toFloat() / longestDecoded
+ val width = (decoded.width * scale).toInt().coerceAtLeast(1)
+ val height = (decoded.height * scale).toInt().coerceAtLeast(1)
+ val scaled = Bitmap.createScaledBitmap(decoded, width, height, true)
+ if (scaled != decoded) decoded.recycle()
+ return scaled
+ }
+
+ fun roundBitmap(src: Bitmap, radius: Float): Bitmap {
+ val output = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(output)
+ val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ shader = BitmapShader(src, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
+ }
+ val rect = RectF(0f, 0f, src.width.toFloat(), src.height.toFloat())
+ canvas.drawRoundRect(rect, radius, radius, paint)
+ return output
+ }
+}
diff --git a/android/app/src/main/res/drawable/ic_widget_book.xml b/android/app/src/main/res/drawable/ic_widget_book.xml
new file mode 100644
index 0000000..8a2bda0
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_widget_book.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_widget_download.xml b/android/app/src/main/res/drawable/ic_widget_download.xml
new file mode 100644
index 0000000..2c0ca6a
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_widget_download.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_widget_refresh.xml b/android/app/src/main/res/drawable/ic_widget_refresh.xml
new file mode 100644
index 0000000..7e2c548
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_widget_refresh.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_widget_scan.xml b/android/app/src/main/res/drawable/ic_widget_scan.xml
new file mode 100644
index 0000000..573c38e
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_widget_scan.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_widget_search.xml b/android/app/src/main/res/drawable/ic_widget_search.xml
new file mode 100644
index 0000000..88f8efb
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_widget_search.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/android/app/src/main/res/layout/widget_quick_actions.xml b/android/app/src/main/res/layout/widget_quick_actions.xml
new file mode 100644
index 0000000..afda30b
--- /dev/null
+++ b/android/app/src/main/res/layout/widget_quick_actions.xml
@@ -0,0 +1,186 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/widget_shelf.xml b/android/app/src/main/res/layout/widget_shelf.xml
new file mode 100644
index 0000000..8892260
--- /dev/null
+++ b/android/app/src/main/res/layout/widget_shelf.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/widget_shelf_item.xml b/android/app/src/main/res/layout/widget_shelf_item.xml
new file mode 100644
index 0000000..5c735fa
--- /dev/null
+++ b/android/app/src/main/res/layout/widget_shelf_item.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 38c44a3..d2f5a0d 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -10,4 +10,17 @@
Authors
Categories
Series
+
+ Shelf
+ A grid of books from a shelf, the library or your downloads
+ Recently added
+ No books yet — tap to refresh
+ Refresh
+
+ Quick actions
+ Search, scan and keep reading
+ Search
+ Scan
+ Read
+ Downloads
diff --git a/android/app/src/main/res/xml/quick_actions_widget_info.xml b/android/app/src/main/res/xml/quick_actions_widget_info.xml
new file mode 100644
index 0000000..f9b5060
--- /dev/null
+++ b/android/app/src/main/res/xml/quick_actions_widget_info.xml
@@ -0,0 +1,12 @@
+
+
diff --git a/android/app/src/main/res/xml/shelf_widget_info.xml b/android/app/src/main/res/xml/shelf_widget_info.xml
new file mode 100644
index 0000000..5fed9fe
--- /dev/null
+++ b/android/app/src/main/res/xml/shelf_widget_info.xml
@@ -0,0 +1,12 @@
+
+
diff --git a/lib/core/services/widget_background.dart b/lib/core/services/widget_background.dart
new file mode 100644
index 0000000..f8fca76
--- /dev/null
+++ b/lib/core/services/widget_background.dart
@@ -0,0 +1,27 @@
+import 'package:flutter/widgets.dart';
+import 'package:logger/logger.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+import 'package:calibre_web_companion/core/services/api_service.dart';
+import 'package:calibre_web_companion/core/services/widget_service.dart';
+import 'package:calibre_web_companion/features/offline/data/repositories/offline_library_repository.dart';
+
+@pragma('vm:entry-point')
+Future widgetBackgroundCallback(Uri? uri) async {
+ if (uri == null || uri.scheme != 'calibrewebcompanion') return;
+ if (!uri.pathSegments.contains('refresh')) return;
+
+ WidgetsFlutterBinding.ensureInitialized();
+
+ final logger = Logger();
+ final prefs = await SharedPreferences.getInstance();
+ await ApiService().initialize();
+
+ final widgetService = WidgetService(
+ prefs: prefs,
+ logger: logger,
+ offlineRepository: OfflineLibraryRepository(prefs: prefs, logger: logger),
+ );
+
+ await widgetService.refreshShelf();
+}
diff --git a/lib/core/services/widget_service.dart b/lib/core/services/widget_service.dart
index c5b237e..6ef86c7 100644
--- a/lib/core/services/widget_service.dart
+++ b/lib/core/services/widget_service.dart
@@ -10,6 +10,8 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:calibre_web_companion/core/services/api_service.dart';
import 'package:calibre_web_companion/core/services/image_cache_manager.dart';
+import 'package:calibre_web_companion/core/services/widget_background.dart';
+import 'package:calibre_web_companion/core/services/widget_shelf_loader.dart';
import 'package:calibre_web_companion/features/offline/data/repositories/offline_library_repository.dart';
import 'package:calibre_web_companion/features/settings/data/models/predefined_colors.dart';
@@ -57,9 +59,16 @@ class WidgetService {
static const String _currentBookProvider = 'CurrentBookWidgetProvider';
static const String _statsProvider = 'LibraryStatsWidgetProvider';
+ static const String _shelfProvider = 'ShelfWidgetProvider';
+ static const String _quickActionsProvider = 'QuickActionsWidgetProvider';
static const String kTapTargetKey = 'widget_tap_target';
+ static const String kShelfSourceKey = 'widget_shelf_source';
+ static const String kShelfIdKey = 'widget_shelf_id';
+ static const String kShelfLabelKey = 'widget_shelf_label';
static const String _kCurrentBookKey = 'widget_current_book';
+ static const String _kShelfBooksKey = 'widget_shelf_books';
+ static const int shelfMaxBooks = 40;
bool get _supported => Platform.isAndroid;
@@ -70,6 +79,39 @@ class WidgetService {
await prefs.setString(kTapTargetKey, target.key);
}
+ WidgetShelfSource get shelfSource =>
+ WidgetShelfSourceX.fromKey(prefs.getString(kShelfSourceKey));
+
+ String get shelfId => prefs.getString(kShelfIdKey) ?? '';
+
+ String get shelfLabel => prefs.getString(kShelfLabelKey) ?? '';
+
+ Future setShelfConfig({
+ required WidgetShelfSource source,
+ String id = '',
+ String label = '',
+ }) async {
+ await prefs.setString(kShelfSourceKey, source.key);
+ await prefs.setString(kShelfIdKey, id);
+ await prefs.setString(kShelfLabelKey, label);
+ await refreshShelf();
+ }
+
+ List get shelfBooks {
+ final raw = prefs.getString(_kShelfBooksKey);
+ if (raw == null || raw.isEmpty) return const [];
+ try {
+ final decoded = jsonDecode(raw);
+ if (decoded is! List) return const [];
+ return decoded
+ .whereType