Skip to content

Commit

Permalink
Added Offline threads functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
TheGuyDangerous committed Oct 10, 2024
1 parent bd3a5c6 commit 548c765
Show file tree
Hide file tree
Showing 6 changed files with 463 additions and 221 deletions.
198 changes: 135 additions & 63 deletions lib/screens/library/library_screen_state.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import 'dart:io';
import '../../widgets/library/history_list.dart';
import '../../widgets/library/empty_state.dart';
import '../../widgets/library/incognito_message.dart';
Expand All @@ -9,6 +10,8 @@ import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:provider/provider.dart';
import '../../theme_provider.dart';
import 'library_screen.dart';
import '../thread/thread_screen.dart';
import '../thread/thread_loading_screen.dart';

const int maxHistoryItems = 50;

Expand All @@ -29,11 +32,52 @@ class LibraryScreenState extends State<LibraryScreen> {
final prefs = await SharedPreferences.getInstance();
_isIncognitoMode = prefs.getBool('incognitoMode') ?? false;
if (!_isIncognitoMode) {
final history = prefs.getStringList('search_history') ?? [];
final savedThreads = prefs.getStringList('saved_threads') ?? [];
final searchHistory = prefs.getStringList('search_history') ?? [];
debugPrint('Loaded saved_threads from SharedPreferences: $savedThreads');
debugPrint(
'Loaded search_history from SharedPreferences: $searchHistory');

setState(() {
_searchHistory = history
.map((item) => json.decode(item) as Map<String, dynamic>)
.toList();
_searchHistory = [
...savedThreads.map((item) {
try {
final data = json.decode(item) as Map<String, dynamic>;
data['isSaved'] = true;
debugPrint('Loaded saved thread: $data');
return data;
} catch (e) {
debugPrint('Error decoding saved thread: $e');
return null;
}
}).whereType<Map<String, dynamic>>(),
...searchHistory.map((item) {
try {
final data = json.decode(item) as Map<String, dynamic>;
data['isSaved'] = false;
debugPrint('Loaded search history item: $data');
return data;
} catch (e) {
debugPrint('Error decoding search history item: $e');
return null;
}
}).whereType<Map<String, dynamic>>(),
];

// Remove duplicates based on query and timestamp
_searchHistory =
_searchHistory.fold<List<Map<String, dynamic>>>([], (list, item) {
if (!list.any((element) =>
element['query'] == item['query'] &&
element['timestamp'] == item['timestamp'])) {
list.add(item);
}
return list;
});

// Sort the combined list by timestamp
_searchHistory.sort((a, b) => DateTime.parse(b['timestamp'])
.compareTo(DateTime.parse(a['timestamp'])));
});
} else {
setState(() {
Expand All @@ -42,85 +86,113 @@ class LibraryScreenState extends State<LibraryScreen> {
}
}

@override
Widget build(BuildContext context) {
final themeProvider = Provider.of<ThemeProvider>(context);

return Scaffold(
appBar: AppBar(
title: Text('Library',
style:
TextStyle(fontFamily: 'Raleway', fontWeight: FontWeight.bold)),
backgroundColor: themeProvider.isDarkMode ? Colors.black : Colors.white,
foregroundColor: themeProvider.isDarkMode ? Colors.white : Colors.black,
elevation: 0,
),
body: SmartRefresher(
controller: _refreshController,
onRefresh: _onRefresh,
child: _isIncognitoMode
? IncognitoMessage()
: _searchHistory.isEmpty
? EmptyState()
: HistoryList(
searchHistory: _searchHistory,
onDeleteItem: _deleteHistoryItem,
onClearAll: _clearAllHistory,
onItemTap: (query) =>
_searchService.performSearch(context, query),
),
),
);
}

void _deleteHistoryItem(int index) async {
Future<void> _onDeleteItem(int index) async {
setState(() {
_searchHistory.removeAt(index);
});
await _saveSearchHistory();
}

void _clearAllHistory() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('Clear All History'),
content: Text('Are you sure you want to clear all search history?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text('Clear'),
),
],
),
);

if (confirmed == true) {
setState(() {
_searchHistory.clear();
});
await _saveSearchHistory();
}
Future<void> _onClearAll() async {
setState(() {
_searchHistory.clear();
});
await _saveSearchHistory();
}

Future<void> _saveSearchHistory() async {
final prefs = await SharedPreferences.getInstance();
if (_searchHistory.length > maxHistoryItems) {
_searchHistory = _searchHistory.sublist(0, maxHistoryItems);
}
final history = _searchHistory.map((item) => json.encode(item)).toList();
await prefs.setStringList('search_history', history);
final savedThreads =
_searchHistory.where((item) => item['isSaved'] == true).toList();
final searchHistory =
_searchHistory.where((item) => item['isSaved'] != true).toList();

await prefs.setStringList('saved_threads',
savedThreads.map((item) => json.encode(item)).toList());
await prefs.setStringList('search_history',
searchHistory.map((item) => json.encode(item)).toList());
}

void _onRefresh() async {
await _loadSearchHistory();
_refreshController.refreshCompleted();
}

@override
Widget build(BuildContext context) {
return Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return Scaffold(
appBar: AppBar(
title: Text('Library'),
),
body: SmartRefresher(
controller: _refreshController,
onRefresh: _onRefresh,
child: _isIncognitoMode
? IncognitoMessage()
: _searchHistory.isEmpty
? EmptyState()
: HistoryList(
searchHistory: _searchHistory,
onDeleteItem: _onDeleteItem,
onClearAll: _onClearAll,
onItemTap: _handleItemTap,
),
),
);
},
);
}

void _handleItemTap(Map<String, dynamic> item) async {
final savedThreadPath = item['path'] as String?;
debugPrint('Tapped item with savedThreadPath: $savedThreadPath');

if (savedThreadPath != null) {
final file = File(savedThreadPath);
if (await file.exists()) {
try {
final jsonString = await file.readAsString();
final threadData = json.decode(jsonString);
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ThreadScreen(
query: threadData['query'],
searchResults: List<Map<String, dynamic>>.from(
threadData['searchResults']),
summary: threadData['summary'],
savedSections: threadData['sections'] != null
? List<Map<String, dynamic>>.from(threadData['sections'])
: null,
),
),
);
} catch (e) {
debugPrint('Error loading saved thread: $e');
_performNewSearch(item['query']);
}
} else {
debugPrint('Saved thread file does not exist: $savedThreadPath');
_performNewSearch(item['query']);
}
} else {
debugPrint('No saved thread path found');
_performNewSearch(item['query']);
}
}

void _performNewSearch(String query) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ThreadLoadingScreen(query: query),
),
);
}

@override
void dispose() {
_refreshController.dispose();
Expand Down
78 changes: 60 additions & 18 deletions lib/screens/thread/thread_loading_screen.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:io';
import '../../widgets/thread/loading_shimmer.dart';
import '../../services/search_service.dart';
import 'thread_screen.dart';
import '../../theme_provider.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';

class ThreadLoadingScreen extends StatefulWidget {
final String query;
final String? savedThreadPath;
final Map<String, dynamic>? savedThreadData;

const ThreadLoadingScreen({super.key, required this.query});
const ThreadLoadingScreen({
Key? key,
required this.query,
this.savedThreadPath,
this.savedThreadData,
}) : super(key: key);

@override
State<ThreadLoadingScreen> createState() => _ThreadLoadingScreenState();
Expand All @@ -21,43 +31,75 @@ class _ThreadLoadingScreenState extends State<ThreadLoadingScreen> {
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_performSearch();
_loadThreadOrSearch();
});
}

Future<void> _loadThreadOrSearch() async {
if (widget.savedThreadData != null) {
_navigateToThreadScreen(widget.savedThreadData!);
} else if (widget.savedThreadPath != null) {
await _loadSavedThread(widget.savedThreadPath!);
} else {
await _performSearch();
}
}

Future<void> _loadSavedThread(String path) async {
try {
final file = File(path);
if (await file.exists()) {
final jsonString = await file.readAsString();
final threadData = json.decode(jsonString);
_navigateToThreadScreen(threadData);
} else {
await _performSearch();
}
} catch (e) {
debugPrint('Error loading saved thread: $e');
await _performSearch();
}
}

Future<void> _performSearch() async {
final results = await _searchService.performSearch(context, widget.query);
if (mounted) {
if (results != null) {
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
ThreadScreen(
query: results['query'],
searchResults: results['searchResults'],
summary: results['summary'],
),
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: Duration(milliseconds: 300),
),
);
_navigateToThreadScreen(results);
} else {
Navigator.of(context).pop();
}
}
}

void _navigateToThreadScreen(Map<String, dynamic> data) {
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => ThreadScreen(
query: data['query'],
searchResults: List<Map<String, dynamic>>.from(data['searchResults']),
summary: data['summary'],
savedSections: data['sections'] as List<dynamic>?,
),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: Duration(milliseconds: 300),
),
);
}

@override
Widget build(BuildContext context) {
final themeProvider = Provider.of<ThemeProvider>(context);

return Scaffold(
backgroundColor: themeProvider.isDarkMode ? Colors.black : Colors.white,
appBar: AppBar(
title: Text('Searching...'),
title: Text(
widget.savedThreadPath != null || widget.savedThreadData != null
? 'Loading Saved Thread'
: 'Searching...'),
backgroundColor: themeProvider.isDarkMode ? Colors.black : Colors.white,
foregroundColor: themeProvider.isDarkMode ? Colors.white : Colors.black,
),
Expand Down
6 changes: 4 additions & 2 deletions lib/screens/thread/thread_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ class ThreadScreen extends StatefulWidget {
final String query;
final List<Map<String, dynamic>> searchResults;
final String summary;
final List<dynamic>? savedSections;

const ThreadScreen({
super.key,
Key? key,
required this.query,
required this.searchResults,
required this.summary,
});
this.savedSections,
}) : super(key: key);

@override
ThreadScreenState createState() => ThreadScreenState();
Expand Down
Loading

0 comments on commit 548c765

Please sign in to comment.