Skip to content

Commit 59f0299

Browse files
authored
Merge pull request #83 from parsamrrelax/main
supabase in
2 parents 22cb874 + ce8ca12 commit 59f0299

10 files changed

Lines changed: 1195 additions & 80 deletions

File tree

SUPABASE_SETUP.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Supabase Setup Guide for Mirarr
2+
3+
This guide will help you set up Supabase to sync your watch history across devices.
4+
5+
## Prerequisites
6+
7+
1. A Supabase account (free tier is sufficient)
8+
2. A Supabase project
9+
10+
## Step 1: Create a Supabase Project
11+
12+
1. Go to [supabase.com](https://supabase.com) and sign up/sign in
13+
2. Click "New Project"
14+
3. Choose your organization and enter project details
15+
4. The name doesn't matter. Don't change other settings
16+
5. For region choose somewhere closest to you for better latency
17+
6. Wait for the project to be created
18+
19+
20+
## Step 2: Create the Watch History Table
21+
22+
1. In your Supabase dashboard, go to the "SQL Editor"
23+
2. Copy and paste the following SQL command to create the watch history table. Click run:
24+
25+
```sql
26+
CREATE TABLE IF NOT EXISTS watch_history (
27+
id BIGSERIAL PRIMARY KEY,
28+
tmdb_id INTEGER NOT NULL,
29+
title TEXT NOT NULL,
30+
type TEXT NOT NULL CHECK (type IN ('movie', 'tv')),
31+
poster_path TEXT,
32+
watched_at TIMESTAMPTZ NOT NULL,
33+
season_number INTEGER,
34+
episode_number INTEGER,
35+
episode_title TEXT,
36+
user_rating REAL,
37+
notes TEXT,
38+
created_at TIMESTAMPTZ DEFAULT NOW(),
39+
updated_at TIMESTAMPTZ DEFAULT NOW(),
40+
UNIQUE(tmdb_id, type, season_number, episode_number)
41+
);
42+
ALTER TABLE watch_history ENABLE ROW LEVEL SECURITY;
43+
CREATE POLICY "Allow all operations on watch_history" ON watch_history
44+
FOR ALL USING (true);
45+
```
46+
47+
## Step 3: Get Your Project Credentials
48+
49+
1. In your Supabase dashboard, go to "Settings" > "Data API"
50+
2. Copy the following values:
51+
- **Project URL** (looks like `https://your-project-id.supabase.co`)
52+
- **Anon/Public Key** (the `anon` key, not the `service_role` key)
53+
54+
## Step 4: Configure Mirarr
55+
56+
1. Open Mirarr and go to Settings
57+
2. In the "Supabase Configuration" section:
58+
- Enter your **Project URL** in the "Supabase URL" field
59+
- Enter your **Anon Key** in the "Supabase Anon Key" field
60+
3. Click "Save Configuration"
61+
4. You should see a green checkmark indicating successful configuration
62+
63+
## Step 5: Sync Your Data
64+
65+
66+
### Sync Behavior Details
67+
68+
- **Upload**: Compares your local database with Supabase and ensures they match exactly. Items deleted locally will be removed from Supabase, and new/updated items will be uploaded.
69+
- **Download**: Merges remote data with local data without removing anything locally.
70+
- **Sync All**: Combines both operations for complete synchronization.
71+
72+
## Security Notes
73+
74+
- Don't share your url and anon key. The current setup has no authentication implemented.
75+
76+
## Troubleshooting
77+
78+
### "Failed to sync" error
79+
- Check your internet connection
80+
- Verify your Supabase URL and anon key are correct
81+
- Make sure the watch_history table exists in your Supabase project
82+
83+
### Table doesn't exist error
84+
- Make sure you ran the SQL commands from Step 2
85+
- Check that the table name is exactly `watch_history`
86+
87+
### Project Paused
88+
If you don't upload or download anything from your project for 7 days on free tier, the project gets paused (Supabase will email you about it.). Just open up your Supabase dashboard and click resume if this happens.
89+
90+
91+
## Support
92+
93+
If you encounter issues, please check:
94+
1. Supabase project status
95+
2. Network connectivity. Try with a VPN.
96+
3. Correct credentials in Mirarr settings
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:shared_preferences/shared_preferences.dart';
3+
import 'package:supabase_flutter/supabase_flutter.dart';
4+
5+
class SupabaseProvider extends ChangeNotifier {
6+
String? _supabaseUrl;
7+
String? _supabaseAnonKey;
8+
bool _isConfigured = false;
9+
SharedPreferences? _prefs;
10+
11+
SupabaseProvider() {
12+
loadSupabaseConfig();
13+
}
14+
15+
String? get supabaseUrl => _supabaseUrl;
16+
String? get supabaseAnonKey => _supabaseAnonKey;
17+
bool get isConfigured => _isConfigured;
18+
19+
Future<void> setSupabaseConfig(String? url, String? anonKey) async {
20+
_supabaseUrl = url;
21+
_supabaseAnonKey = anonKey;
22+
_isConfigured = url != null && url.isNotEmpty && anonKey != null && anonKey.isNotEmpty;
23+
notifyListeners();
24+
await _saveSupabaseConfig();
25+
26+
// Initialize Supabase if both URL and key are provided
27+
if (_isConfigured) {
28+
await _initializeSupabase();
29+
}
30+
}
31+
32+
Future<void> loadSupabaseConfig() async {
33+
_prefs = await SharedPreferences.getInstance();
34+
_supabaseUrl = _prefs?.getString('supabase_url');
35+
_supabaseAnonKey = _prefs?.getString('supabase_anon_key');
36+
_isConfigured = _supabaseUrl != null &&
37+
_supabaseUrl!.isNotEmpty &&
38+
_supabaseAnonKey != null &&
39+
_supabaseAnonKey!.isNotEmpty;
40+
41+
// Initialize Supabase if configuration exists
42+
if (_isConfigured) {
43+
await _initializeSupabase();
44+
}
45+
46+
notifyListeners();
47+
}
48+
49+
Future<void> _saveSupabaseConfig() async {
50+
if (_supabaseUrl != null && _supabaseUrl!.isNotEmpty) {
51+
await _prefs?.setString('supabase_url', _supabaseUrl!);
52+
} else {
53+
await _prefs?.remove('supabase_url');
54+
}
55+
56+
if (_supabaseAnonKey != null && _supabaseAnonKey!.isNotEmpty) {
57+
await _prefs?.setString('supabase_anon_key', _supabaseAnonKey!);
58+
} else {
59+
await _prefs?.remove('supabase_anon_key');
60+
}
61+
}
62+
63+
Future<void> _initializeSupabase() async {
64+
try {
65+
if (_supabaseUrl != null && _supabaseAnonKey != null) {
66+
await Supabase.initialize(
67+
url: _supabaseUrl!,
68+
anonKey: _supabaseAnonKey!,
69+
);
70+
}
71+
} catch (e) {
72+
// Supabase might already be initialized, which is fine
73+
debugPrint('Supabase initialization: $e');
74+
}
75+
}
76+
77+
Future<void> clearSupabaseConfig() async {
78+
_supabaseUrl = null;
79+
_supabaseAnonKey = null;
80+
_isConfigured = false;
81+
notifyListeners();
82+
await _prefs?.remove('supabase_url');
83+
await _prefs?.remove('supabase_anon_key');
84+
}
85+
86+
// Helper method to get Supabase client if configured
87+
SupabaseClient? get client {
88+
if (_isConfigured) {
89+
try {
90+
return Supabase.instance.client;
91+
} catch (e) {
92+
debugPrint('Error getting Supabase client: $e');
93+
return null;
94+
}
95+
}
96+
return null;
97+
}
98+
}

lib/main.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:Mirarr/functions/themeprovider_class.dart';
22
import 'package:Mirarr/functions/regionprovider_class.dart';
3+
import 'package:Mirarr/functions/supabase_provider.dart';
34
import 'package:Mirarr/functions/url_parser.dart';
45
import 'package:Mirarr/widgets/check_updates.dart';
56
import 'package:flutter/material.dart';
@@ -32,11 +33,15 @@ void main() async {
3233
final regionProvider = RegionProvider('worldwide');
3334
await regionProvider.loadRegion();
3435

36+
final supabaseProvider = SupabaseProvider();
37+
await supabaseProvider.loadSupabaseConfig();
38+
3539
runApp(
3640
MultiProvider(
3741
providers: [
3842
ChangeNotifierProvider.value(value: themeProvider),
3943
ChangeNotifierProvider.value(value: regionProvider),
44+
ChangeNotifierProvider.value(value: supabaseProvider),
4045
],
4146
child: const MyApp(),
4247
),

lib/seriesPage/UI/seasons_details.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ void seasonsAndEpisodes(
185185
final isAirDateNull = season['air_date'] == null;
186186
final isEpisodeCountZero =
187187
season['episode_count'] == 0;
188+
188189
return Column(
189190
children: [
190191
ListTile(
@@ -250,6 +251,7 @@ void seasonsAndEpisodes(
250251
serieId,
251252
serieName,
252253
imdbId,
254+
coverUrl,
253255
onWatchStatusChanged: onWatchStatusChanged),
254256
),
255257
const CustomDivider()
@@ -301,7 +303,7 @@ Future<List<dynamic>> fetchEpisodesGuide(BuildContext context, int seasonNumber,
301303
}
302304

303305
void episodesGuide(int seasonNumber, BuildContext context, int serieId,
304-
String serieName, String imdbId, {VoidCallback? onWatchStatusChanged}) {
306+
String serieName, String imdbId, String? seasonPosterPath, {VoidCallback? onWatchStatusChanged}) {
305307
showModalBottomSheet(
306308
context: context,
307309
builder: (BuildContext context) {
@@ -431,7 +433,7 @@ void episodesGuide(int seasonNumber, BuildContext context, int serieId,
431433
seasonNumber: seasonNumber,
432434
episodeNumber: episode['episode_number'],
433435
episodeTitle: episode['name'],
434-
posterPath: null,
436+
posterPath: seasonPosterPath,
435437
onToggle: () {
436438
onWatchStatusChanged?.call();
437439
},
@@ -450,6 +452,7 @@ void episodesGuide(int seasonNumber, BuildContext context, int serieId,
450452
serieId,
451453
serieName,
452454
imdbId,
455+
coverUrl,
453456
onWatchStatusChanged: onWatchStatusChanged),
454457
),
455458
const CustomDivider()
@@ -492,7 +495,7 @@ Future<Map<String, dynamic>> fetchEpisodesDetails(BuildContext context,
492495
}
493496

494497
void episodeDetails(int seasonNumber, int episodeNumber, BuildContext context,
495-
int serieId, String serieName, String imdbId, {VoidCallback? onWatchStatusChanged}) {
498+
int serieId, String serieName, String imdbId, String? seasonPosterPath, {VoidCallback? onWatchStatusChanged}) {
496499
final region =
497500
Provider.of<RegionProvider>(context, listen: false).currentRegion;
498501
showModalBottomSheet(
@@ -558,7 +561,7 @@ void episodeDetails(int seasonNumber, int episodeNumber, BuildContext context,
558561
seasonNumber: seasonNumber,
559562
episodeNumber: episodeNumber,
560563
episodeTitle: episodeName,
561-
posterPath: null,
564+
posterPath: seasonPosterPath,
562565
onToggle: () {
563566
onWatchStatusChanged?.call();
564567
},

lib/seriesPage/serieDetailPage.dart

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ String? posterPath;
327327
Visibility(
328328
visible: Platform.isAndroid,
329329
child: Positioned(
330-
top: 140,
330+
top: 190,
331331
right: 30,
332332
child: GestureDetector(
333333
onTap: () {
@@ -415,7 +415,7 @@ String? posterPath;
415415
Visibility(
416416
visible: isUserLoggedIn == true,
417417
child: Positioned(
418-
top: 40,
418+
top: 140,
419419
right: 30,
420420
child: GestureDetector(
421421
onTap: () async {
@@ -502,10 +502,9 @@ String? posterPath;
502502
userRating != null)
503503
Positioned(
504504
top: 40,
505-
left: 8,
505+
right: 20,
506506
child: Container(
507-
margin: const EdgeInsets.all(10),
508-
padding: const EdgeInsets.all(10),
507+
padding: const EdgeInsets.all(6),
509508
decoration: const BoxDecoration(
510509
color: Colors.black38,
511510
borderRadius:
@@ -605,15 +604,15 @@ String? posterPath;
605604
isSerieRated == false &&
606605
userRating == null)
607606
Positioned(
608-
top: 90,
609-
left: 15,
607+
top: 40,
608+
right: 30,
610609
child: Container(
611610
decoration: const BoxDecoration(
612611
color: Colors.black38,
613612
borderRadius:
614613
BorderRadius.all(Radius.circular(30))),
615-
child: IconButton(
616-
onPressed: () {
614+
child: GestureDetector(
615+
onTap: () {
617616
showModalBottomSheet(
618617
context: context,
619618
builder: (BuildContext context) {
@@ -665,11 +664,14 @@ String? posterPath;
665664
},
666665
);
667666
},
668-
icon: const Icon(
667+
child: const Icon(
669668
Icons.add_reaction,
670669
color: Colors.white,
671-
))),
672-
),
670+
),
671+
),
672+
),
673+
),
674+
673675

674676
Positioned(
675677
top: 40,

0 commit comments

Comments
 (0)