diff --git a/.flutter-version b/.flutter-version
index bfe4261..371986f 100644
--- a/.flutter-version
+++ b/.flutter-version
@@ -1 +1 @@
-3.38.6
+3.41.0
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
index 399f698..f019305 100644
--- a/android/app/src/debug/AndroidManifest.xml
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -3,5 +3,5 @@
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
-
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 63bb0fd..b0bca77 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml
index 399f698..f019305 100644
--- a/android/app/src/profile/AndroidManifest.xml
+++ b/android/app/src/profile/AndroidManifest.xml
@@ -3,5 +3,5 @@
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
-
+
diff --git a/assets/brands.json b/assets/brands.json
index 278bb2a..2290996 100644
--- a/assets/brands.json
+++ b/assets/brands.json
@@ -1,661 +1,661 @@
[
{
"text": "Netflix",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/69/Netflix_logo.svg/500px-Netflix_logo.svg.png",
+ "icon": "netflix",
"category": "Streaming",
"country": "US"
},
{
"text": "Spotify",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/1/19/Spotify_logo_without_text.svg/500px-Spotify_logo_without_text.svg.png",
+ "icon": "spotify",
"category": "Music",
"country": "SE"
},
{
"text": "YouTube Premium",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/09/YouTube_full-color_icon_%282017%29.svg/500px-YouTube_full-color_icon_%282017%29.svg.png",
+ "icon": "youtube",
"category": "Streaming",
"country": "US"
},
{
"text": "Disney+",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3e/Disney%2B_logo.svg/500px-Disney%2B_logo.svg.png",
+ "icon": null,
"category": "Streaming",
"country": "US"
},
{
"text": "Amazon Prime",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/f/f1/Prime_Video.png",
+ "icon": "amazonprime",
"category": "Streaming",
"country": "US"
},
{
"text": "Apple Music",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e0/Apple_Music_logo.svg/1024px-Apple_Music_logo.svg.png",
+ "icon": "applemusic",
"category": "Music",
"country": "US"
},
{
"text": "Apple One",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/1/1b/Apple_One_logo.svg/1200px-Apple_One_logo.svg.png",
+ "icon": "apple",
"category": "Bundle",
"country": "US"
},
{
"text": "Apple TV+",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/28/Apple_TV_Plus_Logo.svg/500px-Apple_TV_Plus_Logo.svg.png",
+ "icon": "appletv",
"category": "Streaming",
"country": "US"
},
{
"text": "iCloud+",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/1/1c/ICloud_logo.svg/500px-ICloud_logo.svg.png",
+ "icon": "icloud",
"category": "Cloud",
"country": "US"
},
{
"text": "Google One",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/1/12/Google_One_logo_%282018%29.svg/1200px-Google_One_logo_%282018%29.svg.png",
+ "icon": "google",
"category": "Cloud",
"country": "US"
},
{
"text": "Microsoft 365",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0e/Microsoft_365_%282022%29.svg/1091px-Microsoft_365_%282022%29.svg.png",
+ "icon": null,
"category": "Productivity",
"country": "US"
},
{
"text": "Xbox Game Pass",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Xbox_Game_Pass_logo_-_colored_version.svg/1200px-Xbox_Game_Pass_logo_-_colored_version.svg.png",
+ "icon": null,
"category": "Gaming",
"country": "US"
},
{
"text": "PlayStation Plus",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f4/PlayStation_Plus_second_logo_and_wordmark.svg/1200px-PlayStation_Plus_second_logo_and_wordmark.svg.png",
+ "icon": "playstation",
"category": "Gaming",
"country": "JP"
},
{
"text": "Nintendo Switch Online",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/31/Nintendo_Switch_Online_logo.svg/1200px-Nintendo_Switch_Online_logo.svg.png",
+ "icon": null,
"category": "Gaming",
"country": "JP"
},
{
"text": "Adobe Creative Cloud",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/62/Adobe_Creative_Cloud_rainbow_icon.svg/512px-Adobe_Creative_Cloud_rainbow_icon.svg.png",
+ "icon": null,
"category": "Productivity",
"country": "US"
},
{
"text": "ChatGPT Plus",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/04/ChatGPT_logo.svg/500px-ChatGPT_logo.svg.png",
+ "icon": "openai",
"category": "AI",
"country": "US"
},
{
"text": "Claude Pro",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/78/Anthropic_logo.svg/500px-Anthropic_logo.svg.png",
+ "icon": "anthropic",
"category": "AI",
"country": "US"
},
{
"text": "GitHub Copilot",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/91/Octicons-mark-github.svg/500px-Octicons-mark-github.svg.png",
+ "icon": "github",
"category": "Developer",
"country": "US"
},
{
"text": "Midjourney",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/e/ed/Midjourney_Emblem.png",
+ "icon": null,
"category": "AI",
"country": "US"
},
{
"text": "Duolingo",
- "logo": "https://logo.clearbit.com/duolingo.com",
+ "icon": "duolingo",
"category": "Education",
"country": "US"
},
{
"text": "Hulu",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f9/Hulu_logo_%282018%29.svg/1200px-Hulu_logo_%282018%29.svg.png",
+ "icon": null,
"category": "Streaming",
"country": "US"
},
{
"text": "Max (HBO)",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/ce/Max_logo.svg/500px-Max_logo.svg.png",
+ "icon": null,
"category": "Streaming",
"country": "US"
},
{
"text": "Peacock",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/NBCUniversal_Peacock_Logo.svg/500px-NBCUniversal_Peacock_Logo.svg.png",
+ "icon": null,
"category": "Streaming",
"country": "US"
},
{
"text": "Paramount+",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a5/Paramount_Plus.svg/500px-Paramount_Plus.svg.png",
+ "icon": null,
"category": "Streaming",
"country": "US"
},
{
"text": "Audible",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d2/Audible_logo.svg/1200px-Audible_logo.svg.png",
+ "icon": "audible",
"category": "Books",
"country": "US"
},
{
"text": "Kindle Unlimited",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/20/Amazon_Kindle_logo.svg/1200px-Amazon_Kindle_logo.svg.png",
+ "icon": null,
"category": "Books",
"country": "US"
},
{
"text": "Dropbox",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/78/Dropbox_Icon.svg/500px-Dropbox_Icon.svg.png",
+ "icon": "dropbox",
"category": "Cloud",
"country": "US"
},
{
"text": "Slack",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d5/Slack_icon_2019.svg/500px-Slack_icon_2019.svg.png",
+ "icon": "slack",
"category": "Productivity",
"country": "US"
},
{
"text": "Zoom",
- "logo": "https://logo.clearbit.com/zoom.us",
+ "icon": "zoom",
"category": "Productivity",
"country": "US"
},
{
"text": "Discord Nitro",
- "logo": "https://logo.clearbit.com/discord.com",
+ "icon": "discord",
"category": "Social",
"country": "US"
},
{
"text": "Twitch",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/26/Twitch_logo.svg/500px-Twitch_logo.svg.png",
+ "icon": "twitch",
"category": "Streaming",
"country": "US"
},
{
"text": "Patreon",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/94/Patreon_logo.svg/500px-Patreon_logo.svg.png",
+ "icon": "patreon",
"category": "Social",
"country": "US"
},
{
"text": "Canva",
- "logo": "https://logo.clearbit.com/canva.com",
+ "icon": "canva",
"category": "Design",
"country": "AU"
},
{
"text": "Figma",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/Figma-logo.svg/500px-Figma-logo.svg.png",
+ "icon": "figma",
"category": "Design",
"country": "US"
},
{
"text": "Notion",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/4/45/Notion_app_logo.png",
+ "icon": "notion",
"category": "Productivity",
"country": "US"
},
{
"text": "Evernote",
- "logo": "https://logo.clearbit.com/evernote.com",
+ "icon": "evernote",
"category": "Productivity",
"country": "US"
},
{
"text": "Todoist",
- "logo": "https://logo.clearbit.com/todoist.com",
+ "icon": "todoist",
"category": "Productivity",
"country": "US"
},
{
"text": "NordVPN",
- "logo": "https://logo.clearbit.com/nordvpn.com",
+ "icon": "nordvpn",
"category": "Security",
"country": "PA"
},
{
"text": "ExpressVPN",
- "logo": "https://logo.clearbit.com/expressvpn.com",
+ "icon": "expressvpn",
"category": "Security",
"country": "VG"
},
{
"text": "Dashlane",
- "logo": "https://logo.clearbit.com/dashlane.com",
+ "icon": "dashlane",
"category": "Security",
"country": "US"
},
{
"text": "1Password",
- "logo": "https://logo.clearbit.com/1password.com",
+ "icon": "n1password",
"category": "Security",
"country": "CA"
},
{
"text": "Peloton",
- "logo": "https://logo.clearbit.com/onepeloton.com",
+ "icon": "peloton",
"category": "Fitness",
"country": "US"
},
{
"text": "Strava",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/cb/Strava_Logo.svg/500px-Strava_Logo.svg.png",
+ "icon": "strava",
"category": "Fitness",
"country": "US"
},
{
"text": "Fitbit Premium",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e0/Fitbit_logo.svg/500px-Fitbit_logo.svg.png",
+ "icon": "fitbit",
"category": "Fitness",
"country": "US"
},
{
"text": "Calm",
- "logo": "https://logo.clearbit.com/calm.com",
+ "icon": null,
"category": "Health",
"country": "US"
},
{
"text": "Headspace",
- "logo": "https://logo.clearbit.com/headspace.com",
+ "icon": "headspace",
"category": "Health",
"country": "US"
},
{
"text": "Uber One",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/c/cc/Uber_logo_2018.png",
+ "icon": "uber",
"category": "Transport",
"country": "US"
},
{
"text": "Lyft Pink",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a0/Lyft_logo.svg/500px-Lyft_logo.svg.png",
+ "icon": "lyft",
"category": "Transport",
"country": "US"
},
{
"text": "DoorDash DashPass",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/DoorDash_Logo.svg/1200px-DoorDash_Logo.svg.png",
+ "icon": "doordash",
"category": "Food",
"country": "US"
},
{
"text": "Grubhub+",
- "logo": "https://logo.clearbit.com/grubhub.com",
+ "icon": "grubhub",
"category": "Food",
"country": "US"
},
{
"text": "Instacart+",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9f/Instacart_logo_and_wordmark.svg/500px-Instacart_logo_and_wordmark.svg.png",
+ "icon": "instacart",
"category": "Food",
"country": "US"
},
{
"text": "Walmart+",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b1/Walmart_logo_%282008%29.svg/1200px-Walmart_logo_%282008%29.svg.png",
+ "icon": "walmart",
"category": "Shopping",
"country": "US"
},
{
"text": "Tinder Gold",
- "logo": "https://logo.clearbit.com/tinder.com",
+ "icon": "tinder",
"category": "Dating",
"country": "US"
},
{
"text": "Bumble Boost",
- "logo": "https://logo.clearbit.com/bumble.com",
+ "icon": null,
"category": "Dating",
"country": "US"
},
{
"text": "Hinge Preferred",
- "logo": "https://logo.clearbit.com/hinge.co",
+ "icon": null,
"category": "Dating",
"country": "US"
},
{
"text": "LinkedIn Premium",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/c/ca/LinkedIn_logo_initials.png",
+ "icon": null,
"category": "Career",
"country": "US"
},
{
"text": "X Premium",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/ce/X_logo_2023.svg/500px-X_logo_2023.svg.png",
+ "icon": "x",
"category": "Social",
"country": "US"
},
{
"text": "Snapchat+",
- "logo": "https://logo.clearbit.com/snapchat.com",
+ "icon": "snapchat",
"category": "Social",
"country": "US"
},
{
"text": "Medium",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Medium_logo_Monogram.svg/500px-Medium_logo_Monogram.svg.png",
+ "icon": "medium",
"category": "Reading",
"country": "US"
},
{
"text": "New York Times",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/7/77/The_New_York_Times_logo.png",
+ "icon": "newyorktimes",
"category": "News",
"country": "US"
},
{
"text": "Wall Street Journal",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c4/The_Wall_Street_Journal_Logo.svg/1200px-The_Wall_Street_Journal_Logo.svg.png",
+ "icon": null,
"category": "News",
"country": "US"
},
{
"text": "Washington Post",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b9/The_Washington_Post_logo.svg/1200px-The_Washington_Post_logo.svg.png",
+ "icon": null,
"category": "News",
"country": "US"
},
{
"text": "The Guardian",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/75/The_Guardian_2018.svg/500px-The_Guardian_2018.svg.png",
+ "icon": null,
"category": "News",
"country": "GB"
},
{
"text": "Financial Times",
- "logo": "https://logo.clearbit.com/ft.com",
+ "icon": null,
"category": "News",
"country": "GB"
},
{
"text": "HelloFresh",
- "logo": "https://logo.clearbit.com/hellofresh.com",
+ "icon": "hellofresh",
"category": "Food",
"country": "DE"
},
{
"text": "Blue Apron",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/70/Blue_Apron_logo.svg/1200px-Blue_Apron_logo.svg.png",
+ "icon": null,
"category": "Food",
"country": "US"
},
{
"text": "Ipsy",
- "logo": "https://logo.clearbit.com/ipsy.com",
+ "icon": null,
"category": "Beauty",
"country": "US"
},
{
"text": "Birchbox",
- "logo": "https://cdn-icons-png.flaticon.com/512/5977/5977575.png",
+ "icon": null,
"category": "Beauty",
"country": "US"
},
{
"text": "Stitch Fix",
- "logo": "https://logo.clearbit.com/stitchfix.com",
+ "icon": null,
"category": "Fashion",
"country": "US"
},
{
"text": "Rent The Runway",
- "logo": "https://1000logos.net/wp-content/uploads/2021/05/Rent-the-Runway-logo.png",
+ "icon": null,
"category": "Fashion",
"country": "US"
},
{
"text": "Fabletics",
- "logo": "https://logo.clearbit.com/fabletics.com",
+ "icon": null,
"category": "Fashion",
"country": "US"
},
{
"text": "BarkBox",
- "logo": "https://seeklogo.com/images/B/barkbox-logo-B114D62002-seeklogo.com.png",
+ "icon": null,
"category": "Pet",
"country": "US"
},
{
"text": "FabFitFun",
- "logo": "https://1000logos.net/wp-content/uploads/2023/01/FabFitFun-Logo.png",
+ "icon": null,
"category": "Lifestyle",
"country": "US"
},
{
"text": "Crunchyroll",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f6/Crunchyroll_Logo.svg/500px-Crunchyroll_Logo.svg.png",
+ "icon": "crunchyroll",
"category": "Streaming",
"country": "US"
},
{
"text": "Funimation",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Funimation_2016.svg/1200px-Funimation_2016.svg.png",
+ "icon": null,
"category": "Streaming",
"country": "US"
},
{
"text": "Shudder",
- "logo": "https://logo.clearbit.com/shudder.com",
+ "icon": null,
"category": "Streaming",
"country": "US"
},
{
"text": "Criterion Channel",
- "logo": "https://logo.clearbit.com/criterionchannel.com",
+ "icon": null,
"category": "Streaming",
"country": "US"
},
{
"text": "DAZN",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/DAZN_Logo_Master.svg/1200px-DAZN_Logo_Master.svg.png",
+ "icon": "dazn",
"category": "Sports",
"country": "GB"
},
{
"text": "ESPN+",
- "logo": "https://logo.clearbit.com/espn.com",
+ "icon": null,
"category": "Sports",
"country": "US"
},
{
"text": "NBA League Pass",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e5/NBA_script.svg/1200px-NBA_script.svg.png",
+ "icon": "nba",
"category": "Sports",
"country": "US"
},
{
"text": "MLB.TV",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a6/Major_League_Baseball_logo.svg/500px-Major_League_Baseball_logo.svg.png",
+ "icon": null,
"category": "Sports",
"country": "US"
},
{
"text": "Coursera Plus",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/Coursera-Logo_600x600.svg/500px-Coursera-Logo_600x600.svg.png",
+ "icon": "coursera",
"category": "Education",
"country": "US"
},
{
"text": "Skillshare",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c0/Skillshare_logo_2020.svg/1200px-Skillshare_logo_2020.svg.png",
+ "icon": "skillshare",
"category": "Education",
"country": "US"
},
{
"text": "MasterClass",
- "logo": "https://logo.clearbit.com/masterclass.com",
+ "icon": null,
"category": "Education",
"country": "US"
},
{
"text": "Udemy",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e3/Udemy_logo.svg/500px-Udemy_logo.svg.png",
+ "icon": "udemy",
"category": "Education",
"country": "US"
},
{
"text": "Scribd",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8d/Scribd_logo.svg/1200px-Scribd_logo.svg.png",
+ "icon": "scribd",
"category": "Books",
"country": "US"
},
{
"text": "Blinkist",
- "logo": "https://logo.clearbit.com/blinkist.com",
+ "icon": null,
"category": "Books",
"country": "DE"
},
{
"text": "Tidal",
- "logo": "https://logo.clearbit.com/tidal.com",
+ "icon": "tidal",
"category": "Music",
"country": "NO"
},
{
"text": "Deezer",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Deezer_logo_2019.svg/1200px-Deezer_logo_2019.svg.png",
+ "icon": null,
"category": "Music",
"country": "FR"
},
{
"text": "SoundCloud Go+",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/78/Soundcloud_logo.svg/1200px-Soundcloud_logo.svg.png",
+ "icon": "soundcloud",
"category": "Music",
"country": "DE"
},
{
"text": "Pandora Plus",
- "logo": "https://logo.clearbit.com/pandora.com",
+ "icon": null,
"category": "Music",
"country": "US"
},
{
"text": "Grammarly Premium",
- "logo": "https://logo.clearbit.com/grammarly.com",
+ "icon": "grammarly",
"category": "Productivity",
"country": "US"
},
{
"text": "Asana",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3b/Asana_logo.svg/500px-Asana_logo.svg.png",
+ "icon": "asana",
"category": "Productivity",
"country": "US"
},
{
"text": "Trello",
- "logo": "https://logo.clearbit.com/trello.com",
+ "icon": "trello",
"category": "Productivity",
"country": "US"
},
{
"text": "Monday.com",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c6/Monday_logo.svg/1200px-Monday_logo.svg.png",
+ "icon": null,
"category": "Productivity",
"country": "IL"
},
{
"text": "ClickUp",
- "logo": "https://logo.clearbit.com/clickup.com",
+ "icon": "clickup",
"category": "Productivity",
"country": "US"
},
{
"text": "EA Play",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/EA_Play_logo.svg/1200px-EA_Play_logo.svg.png",
+ "icon": "ea",
"category": "Gaming",
"country": "US"
},
{
"text": "Ubisoft+",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/78/Ubisoft_logo.svg/500px-Ubisoft_logo.svg.png",
+ "icon": "ubisoft",
"category": "Gaming",
"country": "FR"
},
{
"text": "GeForce Now",
- "logo": "https://logo.clearbit.com/nvidia.com",
+ "icon": "nvidia",
"category": "Gaming",
"country": "US"
},
{
"text": "Mailchimp",
- "logo": "https://logo.clearbit.com/mailchimp.com",
+ "icon": "mailchimp",
"category": "Marketing",
"country": "US"
},
{
"text": "Shopify",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0e/Shopify_logo_2018.svg/500px-Shopify_logo_2018.svg.png",
+ "icon": "shopify",
"category": "E-commerce",
"country": "CA"
},
{
"text": "Wix",
- "logo": "https://logo.clearbit.com/wix.com",
+ "icon": "wix",
"category": "Website",
"country": "IL"
},
{
"text": "Squarespace",
- "logo": "https://logo.clearbit.com/squarespace.com",
+ "icon": "squarespace",
"category": "Website",
"country": "US"
},
{
"text": "Surfshark",
- "logo": "https://logo.clearbit.com/surfshark.com",
+ "icon": "surfshark",
"category": "Security",
"country": "NL"
},
{
"text": "LastPass",
- "logo": "https://logo.clearbit.com/lastpass.com",
+ "icon": "lastpass",
"category": "Security",
"country": "US"
},
{
"text": "Bitwarden Premium",
- "logo": "https://logo.clearbit.com/bitwarden.com",
+ "icon": "bitwarden",
"category": "Security",
"country": "US"
},
{
"text": "Proton Mail",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fc/ProtonMail_logo.svg/1200px-ProtonMail_logo.svg.png",
+ "icon": "protonmail",
"category": "Cloud",
"country": "CH"
},
{
"text": "Proton VPN",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fc/ProtonMail_logo.svg/1200px-ProtonMail_logo.svg.png",
+ "icon": "protonvpn",
"category": "Security",
"country": "CH"
},
{
"text": "Mullvad VPN",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/ca/Mullvad_logo.svg/1200px-Mullvad_logo.svg.png",
+ "icon": "mullvad",
"category": "Security",
"country": "SE"
},
{
"text": "Standard Notes",
- "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Standard_Notes_Logo.png/600px-Standard_Notes_Logo.png",
+ "icon": null,
"category": "Productivity",
"country": "US"
}
diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist
index 1dc6cf7..391a902 100644
--- a/ios/Flutter/AppFrameworkInfo.plist
+++ b/ios/Flutter/AppFrameworkInfo.plist
@@ -20,7 +20,5 @@
????
CFBundleVersion
1.0
- MinimumOSVersion
- 13.0
diff --git a/lib/models/brand.dart b/lib/models/brand.dart
index d125b76..3d18836 100644
--- a/lib/models/brand.dart
+++ b/lib/models/brand.dart
@@ -8,6 +8,7 @@ abstract class Brand with _$Brand {
const factory Brand({
required String text,
String? logo,
+ String? icon,
String? category,
String? name,
String? country,
diff --git a/lib/models/brand.freezed.dart b/lib/models/brand.freezed.dart
index dc54108..a1520d4 100644
--- a/lib/models/brand.freezed.dart
+++ b/lib/models/brand.freezed.dart
@@ -15,7 +15,7 @@ T _$identity(T value) => value;
/// @nodoc
mixin _$Brand {
- String get text; String? get logo; String? get category; String? get name; String? get country; String? get desc; bool? get isNative;
+ String get text; String? get logo; String? get icon; String? get category; String? get name; String? get country; String? get desc; bool? get isNative;
/// Create a copy of Brand
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $BrandCopyWith get copyWith => _$BrandCopyWithImpl(this as Brand,
@override
bool operator ==(Object other) {
- return identical(this, other) || (other.runtimeType == runtimeType&&other is Brand&&(identical(other.text, text) || other.text == text)&&(identical(other.logo, logo) || other.logo == logo)&&(identical(other.category, category) || other.category == category)&&(identical(other.name, name) || other.name == name)&&(identical(other.country, country) || other.country == country)&&(identical(other.desc, desc) || other.desc == desc)&&(identical(other.isNative, isNative) || other.isNative == isNative));
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is Brand&&(identical(other.text, text) || other.text == text)&&(identical(other.logo, logo) || other.logo == logo)&&(identical(other.icon, icon) || other.icon == icon)&&(identical(other.category, category) || other.category == category)&&(identical(other.name, name) || other.name == name)&&(identical(other.country, country) || other.country == country)&&(identical(other.desc, desc) || other.desc == desc)&&(identical(other.isNative, isNative) || other.isNative == isNative));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
-int get hashCode => Object.hash(runtimeType,text,logo,category,name,country,desc,isNative);
+int get hashCode => Object.hash(runtimeType,text,logo,icon,category,name,country,desc,isNative);
@override
String toString() {
- return 'Brand(text: $text, logo: $logo, category: $category, name: $name, country: $country, desc: $desc, isNative: $isNative)';
+ return 'Brand(text: $text, logo: $logo, icon: $icon, category: $category, name: $name, country: $country, desc: $desc, isNative: $isNative)';
}
@@ -48,7 +48,7 @@ abstract mixin class $BrandCopyWith<$Res> {
factory $BrandCopyWith(Brand value, $Res Function(Brand) _then) = _$BrandCopyWithImpl;
@useResult
$Res call({
- String text, String? logo, String? category, String? name, String? country, String? desc, bool? isNative
+ String text, String? logo, String? icon, String? category, String? name, String? country, String? desc, bool? isNative
});
@@ -65,10 +65,11 @@ class _$BrandCopyWithImpl<$Res>
/// Create a copy of Brand
/// with the given fields replaced by the non-null parameter values.
-@pragma('vm:prefer-inline') @override $Res call({Object? text = null,Object? logo = freezed,Object? category = freezed,Object? name = freezed,Object? country = freezed,Object? desc = freezed,Object? isNative = freezed,}) {
+@pragma('vm:prefer-inline') @override $Res call({Object? text = null,Object? logo = freezed,Object? icon = freezed,Object? category = freezed,Object? name = freezed,Object? country = freezed,Object? desc = freezed,Object? isNative = freezed,}) {
return _then(_self.copyWith(
text: null == text ? _self.text : text // ignore: cast_nullable_to_non_nullable
as String,logo: freezed == logo ? _self.logo : logo // ignore: cast_nullable_to_non_nullable
+as String?,icon: freezed == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable
as String?,category: freezed == category ? _self.category : category // ignore: cast_nullable_to_non_nullable
as String?,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String?,country: freezed == country ? _self.country : country // ignore: cast_nullable_to_non_nullable
@@ -159,10 +160,10 @@ return $default(_that);case _:
/// }
/// ```
-@optionalTypeArgs TResult maybeWhen(TResult Function( String text, String? logo, String? category, String? name, String? country, String? desc, bool? isNative)? $default,{required TResult orElse(),}) {final _that = this;
+@optionalTypeArgs TResult maybeWhen(TResult Function( String text, String? logo, String? icon, String? category, String? name, String? country, String? desc, bool? isNative)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _Brand() when $default != null:
-return $default(_that.text,_that.logo,_that.category,_that.name,_that.country,_that.desc,_that.isNative);case _:
+return $default(_that.text,_that.logo,_that.icon,_that.category,_that.name,_that.country,_that.desc,_that.isNative);case _:
return orElse();
}
@@ -180,10 +181,10 @@ return $default(_that.text,_that.logo,_that.category,_that.name,_that.country,_t
/// }
/// ```
-@optionalTypeArgs TResult when(TResult Function( String text, String? logo, String? category, String? name, String? country, String? desc, bool? isNative) $default,) {final _that = this;
+@optionalTypeArgs TResult when(TResult Function( String text, String? logo, String? icon, String? category, String? name, String? country, String? desc, bool? isNative) $default,) {final _that = this;
switch (_that) {
case _Brand():
-return $default(_that.text,_that.logo,_that.category,_that.name,_that.country,_that.desc,_that.isNative);case _:
+return $default(_that.text,_that.logo,_that.icon,_that.category,_that.name,_that.country,_that.desc,_that.isNative);case _:
throw StateError('Unexpected subclass');
}
@@ -200,10 +201,10 @@ return $default(_that.text,_that.logo,_that.category,_that.name,_that.country,_t
/// }
/// ```
-@optionalTypeArgs TResult? whenOrNull(TResult? Function( String text, String? logo, String? category, String? name, String? country, String? desc, bool? isNative)? $default,) {final _that = this;
+@optionalTypeArgs TResult? whenOrNull(TResult? Function( String text, String? logo, String? icon, String? category, String? name, String? country, String? desc, bool? isNative)? $default,) {final _that = this;
switch (_that) {
case _Brand() when $default != null:
-return $default(_that.text,_that.logo,_that.category,_that.name,_that.country,_that.desc,_that.isNative);case _:
+return $default(_that.text,_that.logo,_that.icon,_that.category,_that.name,_that.country,_that.desc,_that.isNative);case _:
return null;
}
@@ -215,11 +216,12 @@ return $default(_that.text,_that.logo,_that.category,_that.name,_that.country,_t
@JsonSerializable()
class _Brand implements Brand {
- const _Brand({required this.text, this.logo, this.category, this.name, this.country, this.desc, this.isNative});
+ const _Brand({required this.text, this.logo, this.icon, this.category, this.name, this.country, this.desc, this.isNative});
factory _Brand.fromJson(Map json) => _$BrandFromJson(json);
@override final String text;
@override final String? logo;
+@override final String? icon;
@override final String? category;
@override final String? name;
@override final String? country;
@@ -239,16 +241,16 @@ Map toJson() {
@override
bool operator ==(Object other) {
- return identical(this, other) || (other.runtimeType == runtimeType&&other is _Brand&&(identical(other.text, text) || other.text == text)&&(identical(other.logo, logo) || other.logo == logo)&&(identical(other.category, category) || other.category == category)&&(identical(other.name, name) || other.name == name)&&(identical(other.country, country) || other.country == country)&&(identical(other.desc, desc) || other.desc == desc)&&(identical(other.isNative, isNative) || other.isNative == isNative));
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is _Brand&&(identical(other.text, text) || other.text == text)&&(identical(other.logo, logo) || other.logo == logo)&&(identical(other.icon, icon) || other.icon == icon)&&(identical(other.category, category) || other.category == category)&&(identical(other.name, name) || other.name == name)&&(identical(other.country, country) || other.country == country)&&(identical(other.desc, desc) || other.desc == desc)&&(identical(other.isNative, isNative) || other.isNative == isNative));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
-int get hashCode => Object.hash(runtimeType,text,logo,category,name,country,desc,isNative);
+int get hashCode => Object.hash(runtimeType,text,logo,icon,category,name,country,desc,isNative);
@override
String toString() {
- return 'Brand(text: $text, logo: $logo, category: $category, name: $name, country: $country, desc: $desc, isNative: $isNative)';
+ return 'Brand(text: $text, logo: $logo, icon: $icon, category: $category, name: $name, country: $country, desc: $desc, isNative: $isNative)';
}
@@ -259,7 +261,7 @@ abstract mixin class _$BrandCopyWith<$Res> implements $BrandCopyWith<$Res> {
factory _$BrandCopyWith(_Brand value, $Res Function(_Brand) _then) = __$BrandCopyWithImpl;
@override @useResult
$Res call({
- String text, String? logo, String? category, String? name, String? country, String? desc, bool? isNative
+ String text, String? logo, String? icon, String? category, String? name, String? country, String? desc, bool? isNative
});
@@ -276,10 +278,11 @@ class __$BrandCopyWithImpl<$Res>
/// Create a copy of Brand
/// with the given fields replaced by the non-null parameter values.
-@override @pragma('vm:prefer-inline') $Res call({Object? text = null,Object? logo = freezed,Object? category = freezed,Object? name = freezed,Object? country = freezed,Object? desc = freezed,Object? isNative = freezed,}) {
+@override @pragma('vm:prefer-inline') $Res call({Object? text = null,Object? logo = freezed,Object? icon = freezed,Object? category = freezed,Object? name = freezed,Object? country = freezed,Object? desc = freezed,Object? isNative = freezed,}) {
return _then(_Brand(
text: null == text ? _self.text : text // ignore: cast_nullable_to_non_nullable
as String,logo: freezed == logo ? _self.logo : logo // ignore: cast_nullable_to_non_nullable
+as String?,icon: freezed == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable
as String?,category: freezed == category ? _self.category : category // ignore: cast_nullable_to_non_nullable
as String?,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String?,country: freezed == country ? _self.country : country // ignore: cast_nullable_to_non_nullable
diff --git a/lib/models/brand.g.dart b/lib/models/brand.g.dart
index b388105..4ea959f 100644
--- a/lib/models/brand.g.dart
+++ b/lib/models/brand.g.dart
@@ -9,6 +9,7 @@ part of 'brand.dart';
_Brand _$BrandFromJson(Map json) => _Brand(
text: json['text'] as String,
logo: json['logo'] as String?,
+ icon: json['icon'] as String?,
category: json['category'] as String?,
name: json['name'] as String?,
country: json['country'] as String?,
@@ -19,6 +20,7 @@ _Brand _$BrandFromJson(Map json) => _Brand(
Map _$BrandToJson(_Brand instance) => {
'text': instance.text,
'logo': instance.logo,
+ 'icon': instance.icon,
'category': instance.category,
'name': instance.name,
'country': instance.country,
diff --git a/lib/providers/brands_provider.dart b/lib/providers/brands_provider.dart
index 15dfe44..ac12d0e 100644
--- a/lib/providers/brands_provider.dart
+++ b/lib/providers/brands_provider.dart
@@ -31,7 +31,7 @@ class Brands extends _$Brands {
ref.watch(brandsStorageProvider.future),
options: StorageOptions(
cacheTime: StorageCacheTime.unsafe_forever,
- destroyKey: "v1",
+ destroyKey: "v3",
),
).future;
if (state.value != null) {
diff --git a/lib/providers/brands_provider.g.dart b/lib/providers/brands_provider.g.dart
index 9120122..783a892 100644
--- a/lib/providers/brands_provider.g.dart
+++ b/lib/providers/brands_provider.g.dart
@@ -75,7 +75,7 @@ final class BrandsProvider extends $AsyncNotifierProvider> {
Brands create() => Brands();
}
-String _$brandsHash() => r'f605da45e18ac1b72d24644538cac182cbd9953c';
+String _$brandsHash() => r'97f03c4666ac0e5b65ce616e7e19160591ef1157';
@JsonPersist()
abstract class _$BrandsBase extends $AsyncNotifier> {
diff --git a/lib/providers/subs_controller.dart b/lib/providers/subs_controller.dart
index 0f3492e..9635854 100644
--- a/lib/providers/subs_controller.dart
+++ b/lib/providers/subs_controller.dart
@@ -40,7 +40,7 @@ class SubsController extends _$SubsController {
ref.watch(subsStorageProvider.future),
options: StorageOptions(
cacheTime: StorageCacheTime.unsafe_forever,
- destroyKey: "v1",
+ destroyKey: "v2",
),
).future;
scheduleNotification();
diff --git a/lib/screens/calendar_screen.dart b/lib/screens/calendar_screen.dart
index 125378b..2930429 100644
--- a/lib/screens/calendar_screen.dart
+++ b/lib/screens/calendar_screen.dart
@@ -1,10 +1,10 @@
-import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:subs_tracker/models/sub_slice.dart';
import 'package:subs_tracker/providers/subs_controller.dart';
+import 'package:subs_tracker/widgets/brand_logo.dart';
import 'package:table_calendar/table_calendar.dart';
class CalendarScreen extends HookConsumerWidget {
@@ -92,17 +92,10 @@ class CalendarScreen extends HookConsumerWidget {
itemBuilder: (context, index) {
final sub = selectedEvents[index];
return ListTile(
- leading: sub.brand?.logo != null
- ? CachedNetworkImage(
- imageUrl: sub.brand!.logo!,
- width: 32,
- height: 32,
- fit: BoxFit.contain,
- errorWidget: (_, _, _) => CircleAvatar(
- radius: 16,
- backgroundColor: Color(sub.color),
- child: Text(sub.name[0].toUpperCase()),
- ),
+ leading: sub.brand != null
+ ? BrandLogo(
+ brand: sub.brand,
+ size: 32,
)
: CircleAvatar(
radius: 16,
diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart
index 7904e14..4e3bfd0 100644
--- a/lib/screens/home_screen.dart
+++ b/lib/screens/home_screen.dart
@@ -1,4 +1,3 @@
-import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -7,6 +6,7 @@ import 'package:subs_tracker/models/sub_slice.dart';
import 'package:subs_tracker/providers/settings_controller.dart';
import 'package:subs_tracker/providers/subs_controller.dart';
import 'package:subs_tracker/widgets/add_subs_dialog.dart';
+import 'package:subs_tracker/widgets/brand_logo.dart';
import 'package:subs_tracker/widgets/edit_subs_dialog.dart';
class HomeScreen extends HookConsumerWidget {
@@ -327,27 +327,17 @@ class _CompactSliceLeading extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return Container(
- width: 40,
- height: 40,
- decoration: BoxDecoration(
- color: slice.brand?.logo != null
- ? Colors.transparent
- : Color(slice.color),
- borderRadius: BorderRadius.circular(6),
- ),
- child: slice.brand?.logo != null
- ? ClipRRect(
+ return slice.brand != null
+ ? BrandLogo(brand: slice.brand, size: 40)
+ : Container(
+ width: 40,
+ height: 40,
+ decoration: BoxDecoration(
+ color: Color(slice.color),
borderRadius: BorderRadius.circular(6),
- child: CachedNetworkImage(
- imageUrl: slice.brand!.logo!,
- fit: BoxFit.contain,
- placeholder: (_, _) => const _SliceLogoPlaceholder(),
- errorWidget: (_, _, _) => SubAvatar(s: slice),
- ),
- )
- : SubAvatar(s: slice),
- );
+ ),
+ child: SubAvatar(s: slice),
+ );
}
}
@@ -368,17 +358,4 @@ class SubAvatar extends StatelessWidget {
}
}
-class _SliceLogoPlaceholder extends StatelessWidget {
- const _SliceLogoPlaceholder();
- @override
- Widget build(BuildContext context) {
- return const Center(
- child: SizedBox(
- width: 20,
- height: 20,
- child: CircularProgressIndicator(strokeWidth: 2),
- ),
- );
- }
-}
diff --git a/lib/utils/brand_utils.dart b/lib/utils/brand_utils.dart
new file mode 100644
index 0000000..834aece
--- /dev/null
+++ b/lib/utils/brand_utils.dart
@@ -0,0 +1,17 @@
+import 'package:flutter/material.dart';
+import 'package:simple_icons/simple_icons.dart';
+import 'package:subs_tracker/models/brand.dart';
+
+/// Extension on [Brand] to derive icon data from Simple Icons.
+///
+/// Uses the built-in [SimpleIcons.values] and [SimpleIconColors.values]
+/// lookup maps, so no manual mapping is needed.
+extension BrandIconData on Brand {
+ /// Returns the [IconData] for this brand from Simple Icons, or null.
+ IconData? get iconData =>
+ icon != null ? SimpleIcons.values[icon!] : null;
+
+ /// Returns the brand color from Simple Icons, or null.
+ Color? get iconColor =>
+ icon != null ? SimpleIconColors.values[icon!] : null;
+}
diff --git a/lib/widgets/add_subs_dialog.dart b/lib/widgets/add_subs_dialog.dart
index b335e56..efb0b67 100644
--- a/lib/widgets/add_subs_dialog.dart
+++ b/lib/widgets/add_subs_dialog.dart
@@ -1,4 +1,3 @@
-import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
@@ -9,6 +8,7 @@ import 'package:subs_tracker/models/sub_slice.dart';
import 'package:subs_tracker/providers/brands_provider.dart';
import 'package:subs_tracker/providers/subs_controller.dart';
import 'package:subs_tracker/utils/color_palette.dart';
+import 'package:subs_tracker/widgets/brand_logo.dart';
class AddSubsDialog extends HookConsumerWidget {
const AddSubsDialog({super.key});
@@ -132,35 +132,9 @@ class AddSubsDialog extends HookConsumerWidget {
prefixIcon: draftSlice.value.brand != null
? Padding(
padding: const EdgeInsets.all(8),
- child: ClipRRect(
- borderRadius: BorderRadius.circular(8),
- child: SizedBox.square(
- dimension: 32,
- child:
- draftSlice.value.brand!.logo !=
- null
- ? CachedNetworkImage(
- imageUrl: draftSlice
- .value
- .brand!
- .logo!,
- fit: BoxFit.contain,
- placeholder: (_, _) =>
- const _LogoPlaceholder(),
- errorWidget: (_, _, _) =>
- const Icon(
- Icons.business,
- ),
- )
- : Container(
- color: Theme.of(context)
- .colorScheme
- .surfaceContainerHighest,
- child: const Icon(
- Icons.business,
- ),
- ),
- ),
+ child: BrandLogo(
+ brand: draftSlice.value.brand,
+ size: 32,
),
)
: const Icon(Icons.search),
@@ -211,27 +185,10 @@ class AddSubsDialog extends HookConsumerWidget {
itemBuilder: (context, index) {
final Brand option = options.elementAt(index);
return ListTile(
- leading: option.logo != null
- ? ClipRRect(
- borderRadius: BorderRadius.circular(
- 8,
- ),
- child: CachedNetworkImage(
- imageUrl: option.logo!,
- width: 40,
- height: 40,
- fit: BoxFit.cover,
- placeholder: (_, _) =>
- const _LogoPlaceholder(),
- errorWidget: (_, _, _) =>
- const Icon(
- Icons.image_not_supported,
- ),
- ),
- )
- : const CircleAvatar(
- child: Icon(Icons.business),
- ),
+ leading: BrandLogo(
+ brand: option,
+ size: 40,
+ ),
title: Text(option.text),
subtitle:
option.category != null ||
@@ -499,17 +456,4 @@ class AddSubsDialog extends HookConsumerWidget {
}
}
-class _LogoPlaceholder extends StatelessWidget {
- const _LogoPlaceholder();
- @override
- Widget build(BuildContext context) {
- return const Center(
- child: SizedBox(
- width: 16,
- height: 16,
- child: CircularProgressIndicator(strokeWidth: 2),
- ),
- );
- }
-}
diff --git a/lib/widgets/brand_logo.dart b/lib/widgets/brand_logo.dart
new file mode 100644
index 0000000..ee6a4af
--- /dev/null
+++ b/lib/widgets/brand_logo.dart
@@ -0,0 +1,59 @@
+import 'package:flutter/material.dart';
+import 'package:subs_tracker/models/brand.dart';
+import 'package:subs_tracker/utils/brand_utils.dart';
+
+/// A reusable widget that displays a brand icon from Simple Icons,
+/// or a fallback Material icon if no Simple Icons match is available.
+class BrandLogo extends StatelessWidget {
+ const BrandLogo({
+ super.key,
+ required this.brand,
+ this.size = 32,
+ });
+
+ final Brand? brand;
+ final double size;
+
+ @override
+ Widget build(BuildContext context) {
+ final iconData = brand?.iconData;
+
+ if (iconData != null) {
+ final color = brand!.iconColor ??
+ Theme.of(context).colorScheme.onSurface;
+ // Use the brand-colored icon on a subtle background
+ return Container(
+ width: size,
+ height: size,
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(size * 0.2),
+ ),
+ child: Center(
+ child: Icon(
+ iconData,
+ size: size * 0.65,
+ color: color,
+ ),
+ ),
+ );
+ }
+
+ // Fallback for brands without a Simple Icons match
+ return Container(
+ width: size,
+ height: size,
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(size * 0.2),
+ ),
+ child: Center(
+ child: Icon(
+ Icons.subscriptions_outlined,
+ size: size * 0.55,
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/edit_subs_dialog.dart b/lib/widgets/edit_subs_dialog.dart
index 94ad2e9..64772d2 100644
--- a/lib/widgets/edit_subs_dialog.dart
+++ b/lib/widgets/edit_subs_dialog.dart
@@ -1,4 +1,3 @@
-import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
@@ -9,6 +8,7 @@ import 'package:subs_tracker/models/sub_slice.dart';
import 'package:subs_tracker/providers/brands_provider.dart';
import 'package:subs_tracker/providers/subs_controller.dart';
import 'package:subs_tracker/utils/color_palette.dart';
+import 'package:subs_tracker/widgets/brand_logo.dart';
class EditSubsDialog extends HookConsumerWidget {
const EditSubsDialog({
@@ -131,35 +131,9 @@ class EditSubsDialog extends HookConsumerWidget {
prefixIcon: draftSlice.value.brand != null
? Padding(
padding: const EdgeInsets.all(8),
- child: ClipRRect(
- borderRadius: BorderRadius.circular(8),
- child: SizedBox.square(
- dimension: 32,
- child:
- draftSlice.value.brand!.logo !=
- null
- ? CachedNetworkImage(
- imageUrl: draftSlice
- .value
- .brand!
- .logo!,
- fit: BoxFit.contain,
- placeholder: (_, _) =>
- const _LogoPlaceholder(),
- errorWidget: (_, _, _) =>
- const Icon(
- Icons.business,
- ),
- )
- : Container(
- color: Theme.of(context)
- .colorScheme
- .surfaceContainerHighest,
- child: const Icon(
- Icons.business,
- ),
- ),
- ),
+ child: BrandLogo(
+ brand: draftSlice.value.brand,
+ size: 32,
),
)
: const Icon(Icons.search),
@@ -210,27 +184,10 @@ class EditSubsDialog extends HookConsumerWidget {
itemBuilder: (context, index) {
final Brand option = options.elementAt(index);
return ListTile(
- leading: option.logo != null
- ? ClipRRect(
- borderRadius: BorderRadius.circular(
- 8,
- ),
- child: CachedNetworkImage(
- imageUrl: option.logo!,
- width: 40,
- height: 40,
- fit: BoxFit.cover,
- placeholder: (_, _) =>
- const _LogoPlaceholder(),
- errorWidget: (_, _, _) =>
- const Icon(
- Icons.image_not_supported,
- ),
- ),
- )
- : const CircleAvatar(
- child: Icon(Icons.business),
- ),
+ leading: BrandLogo(
+ brand: option,
+ size: 40,
+ ),
title: Text(option.text),
subtitle:
option.category != null ||
@@ -499,17 +456,4 @@ class EditSubsDialog extends HookConsumerWidget {
}
}
-class _LogoPlaceholder extends StatelessWidget {
- const _LogoPlaceholder();
- @override
- Widget build(BuildContext context) {
- return const Center(
- child: SizedBox(
- width: 16,
- height: 16,
- child: CircularProgressIndicator(strokeWidth: 2),
- ),
- );
- }
-}
diff --git a/lib/widgets/pie_chart.dart b/lib/widgets/pie_chart.dart
index db437b2..f0b6377 100644
--- a/lib/widgets/pie_chart.dart
+++ b/lib/widgets/pie_chart.dart
@@ -6,6 +6,7 @@ import 'package:subs_tracker/models/brand.dart';
import 'package:subs_tracker/models/sub_slice.dart';
import 'package:subs_tracker/providers/subs_controller.dart';
import 'package:subs_tracker/utils/color_utils.dart';
+import 'package:subs_tracker/widgets/brand_logo.dart';
class SubsPie extends ConsumerStatefulWidget {
const SubsPie({super.key});
@@ -100,13 +101,9 @@ class _Badge extends StatelessWidget {
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
- child: brand?.logo != null ? Image.network(
- brand!.logo!,
- width: 40,
- height: 20,
- fit: BoxFit.contain,
- errorBuilder: (_, _, _) =>
- const Icon(Icons.business, size: 16),
+ child: brand != null ? BrandLogo(
+ brand: brand,
+ size: 28,
) :
Flexible(
child: Text(
diff --git a/pubspec.lock b/pubspec.lock
index 53794a5..70d1592 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -113,38 +113,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.12.1"
- cached_network_image:
- dependency: "direct main"
- description:
- name: cached_network_image
- sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
- url: "https://pub.dev"
- source: hosted
- version: "3.4.1"
- cached_network_image_platform_interface:
- dependency: transitive
- description:
- name: cached_network_image_platform_interface
- sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
- url: "https://pub.dev"
- source: hosted
- version: "4.1.1"
- cached_network_image_web:
- dependency: transitive
- description:
- name: cached_network_image_web
- sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
- url: "https://pub.dev"
- source: hosted
- version: "1.3.1"
characters:
dependency: transitive
description:
name: characters
- sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
+ sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
- version: "1.4.0"
+ version: "1.4.1"
checked_yaml:
dependency: transitive
description:
@@ -414,14 +390,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
- flutter_cache_manager:
- dependency: transitive
- description:
- name: flutter_cache_manager
- sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
- url: "https://pub.dev"
- source: hosted
- version: "3.4.1"
flutter_colorpicker:
dependency: "direct main"
description:
@@ -685,14 +653,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
- js:
- dependency: transitive
- description:
- name: js
- sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
- url: "https://pub.dev"
- source: hosted
- version: "0.7.2"
json_annotation:
dependency: "direct main"
description:
@@ -761,18 +721,18 @@ packages:
dependency: transitive
description:
name: matcher
- sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
+ sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev"
source: hosted
- version: "0.12.17"
+ version: "0.12.18"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
- sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+ sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
- version: "0.11.1"
+ version: "0.13.0"
meta:
dependency: transitive
description:
@@ -805,14 +765,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.2"
- octo_image:
- dependency: transitive
- description:
- name: octo_image
- sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
- url: "https://pub.dev"
- source: hosted
- version: "2.1.0"
package_config:
dependency: transitive
description:
@@ -1141,6 +1093,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.4+2"
+ simple_icons:
+ dependency: "direct main"
+ description:
+ name: simple_icons
+ sha256: "2ca3cd79c9f12e97a8588cae0f342609f19fd2e82315356cb09b5c4987ad0808"
+ url: "https://pub.dev"
+ source: hosted
+ version: "14.6.1"
sky_engine:
dependency: transitive
description: flutter
@@ -1294,26 +1254,26 @@ packages:
dependency: transitive
description:
name: test
- sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
+ sha256: "77cc98ea27006c84e71a7356cf3daf9ddbde2d91d84f77dbfe64cf0e4d9611ae"
url: "https://pub.dev"
source: hosted
- version: "1.26.3"
+ version: "1.28.0"
test_api:
dependency: transitive
description:
name: test_api
- sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
+ sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8"
url: "https://pub.dev"
source: hosted
- version: "0.7.7"
+ version: "0.7.8"
test_core:
dependency: transitive
description:
name: test_core
- sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
+ sha256: f1072617a6657e5fc09662e721307f7fb009b4ed89b19f47175d11d5254a62d4
url: "https://pub.dev"
source: hosted
- version: "0.6.12"
+ version: "0.6.14"
timezone:
dependency: "direct main"
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 9f98706..fa8b8a9 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -33,7 +33,6 @@ dependencies:
sqflite: ^2.4.2
path: ^1.9.1
flutter_colorpicker: ^1.0.3
- cached_network_image: ^3.4.1
flutter_local_notifications: ^19.5.0
timezone: ^0.10.1
go_router: ^17.0.1
@@ -47,6 +46,7 @@ dependencies:
crypto: ^3.0.7
shared_preferences: ^2.5.3
easy_localization: ^3.0.8
+ simple_icons: ^14.6.1
dev_dependencies:
flutter_test:
@@ -85,6 +85,7 @@ flutter:
assets:
- assets/
- assets/translations/
+
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg