Skip to content
This repository was archived by the owner on Jan 1, 2026. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions lib/domain/data/feed_collection.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:collection/collection.dart';
import 'package:rss_it/domain/data/feed_entity.dart';
import 'package:rss_it/domain/data/folder_entity.dart';

final class FeedCollection {
final FolderEntity? folder;
final List<FeedEntity> feeds;

const FeedCollection({
required this.folder,
required this.feeds,
});

String get displayName =>
folder?.name ??
(feeds.isEmpty ? 'Unsorted feeds' : 'Unsorted (${feeds.length})');

bool get isFolder => folder != null;

/// Returns feeds sorted alphabetically to offer predictable ordering.
List<FeedEntity> get sortedFeeds =>
feeds.sorted((a, b) => a.title.compareTo(b.title));
}
10 changes: 9 additions & 1 deletion lib/domain/data/feed_entity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import 'package:dart_scope_functions/dart_scope_functions.dart';
import 'package:rss_it_library/protos/feed.pb.dart';

final class FeedEntity {
static FeedEntity fromRemoteFeed(Feed remoteFeed) {
static FeedEntity fromRemoteFeed(
Feed remoteFeed, {
int? folderId,
}) {
return FeedEntity(
url: remoteFeed.url,
title: remoteFeed.title,
description: remoteFeed.description,
thumbnailURL: remoteFeed.image,
addedAt: DateTime.now(),
folderId: folderId,
);
}

Expand All @@ -18,6 +22,7 @@ final class FeedEntity {
final String? description;
final String? thumbnailURL;
final DateTime addedAt;
final int? folderId;

FeedEntity({
this.id,
Expand All @@ -26,6 +31,7 @@ final class FeedEntity {
required this.description,
required this.thumbnailURL,
required this.addedAt,
this.folderId,
});

factory FeedEntity.fromJson(Map<String, Object?> json) {
Expand All @@ -36,6 +42,7 @@ final class FeedEntity {
description: json['description'] as String?,
thumbnailURL: json['thumbnail_url'] as String?,
addedAt: (json['added_at'] as String).let((it) => DateTime.parse(it)),
folderId: json['folder_id'] as int?,
);
}

Expand All @@ -47,6 +54,7 @@ final class FeedEntity {
'description': description,
'thumbnail_url': thumbnailURL,
'added_at': addedAt.toIso8601String(),
'folder_id': folderId,
};
}
}
41 changes: 41 additions & 0 deletions lib/domain/data/folder_entity.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import 'package:dart_scope_functions/dart_scope_functions.dart';

final class FolderEntity {
final int? id;
final String name;
final DateTime createdAt;

const FolderEntity({
this.id,
required this.name,
required this.createdAt,
});

factory FolderEntity.fromJson(Map<String, Object?> json) {
return FolderEntity(
id: json['id'] as int?,
name: json['name'] as String,
createdAt: (json['created_at'] as String).let(DateTime.parse),
);
}

Map<String, Object?> toJson() {
return {
if (id != null) 'id': id,
'name': name,
'created_at': createdAt.toIso8601String(),
};
}

FolderEntity copyWith({
int? id,
String? name,
DateTime? createdAt,
}) {
return FolderEntity(
id: id ?? this.id,
name: name ?? this.name,
createdAt: createdAt ?? this.createdAt,
);
}
}
15 changes: 15 additions & 0 deletions lib/domain/providers/db_provider.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:rss_it/domain/data/feed_entity.dart';
import 'package:rss_it/domain/data/feed_item_entity.dart';
import 'package:rss_it/domain/data/folder_entity.dart';

enum GetFeedsOrderBy { title, addedAt }

Expand All @@ -16,7 +17,21 @@ abstract interface class DBProvider {
required Iterable<FeedItemEntity> incomingFeedItems,
});

Future<Iterable<FolderEntity>> getFolders({
OrderByDirection orderByDirection = OrderByDirection.ascending,
});

Future<int> createFolder({required FolderEntity folder});
Future<void> renameFolder({
required int folderID,
required String newName,
});

Future<void> deleteFolder({required int folderID});
Future<void> moveFeedToFolder({required int feedID, int? folderID});

Future<Iterable<FeedEntity>> getFeeds({
int? folderID,
GetFeedsOrderBy orderBy = GetFeedsOrderBy.title,
OrderByDirection orderByDirection = OrderByDirection.ascending,
});
Expand Down
81 changes: 79 additions & 2 deletions lib/domain/providers/sqlite_db_provider.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:rss_it/domain/data/feed_entity.dart';
import 'package:rss_it/domain/data/feed_item_entity.dart';
import 'package:rss_it/domain/data/folder_entity.dart';
import 'package:rss_it/domain/providers/db_provider.dart';
import 'package:sqflite/sqflite.dart';

Expand Down Expand Up @@ -55,6 +56,7 @@ final class SQLiteDBProvider implements DBProvider {

@override
Future<Iterable<FeedEntity>> getFeeds({
int? folderID,
GetFeedsOrderBy orderBy = GetFeedsOrderBy.title,
OrderByDirection orderByDirection = OrderByDirection.ascending,
}) async {
Expand All @@ -68,9 +70,13 @@ final class SQLiteDBProvider implements DBProvider {
OrderByDirection.descending => 'desc',
};

final whereClause = folderID != null ? 'where folder_id = ?' : '';
final query =
'select * from feeds order by $orderByColumn $orderByDirectionString';
final result = await _database.rawQuery(query);
'select * from feeds $whereClause order by $orderByColumn $orderByDirectionString';
final result = await _database.rawQuery(
query,
folderID != null ? [folderID] : null,
);
return result.map((item) => FeedEntity.fromJson(item));
}

Expand All @@ -89,4 +95,75 @@ final class SQLiteDBProvider implements DBProvider {
await txn.delete('feeds', where: 'id = $feedID');
});
}

@override
Future<Iterable<FolderEntity>> getFolders({
OrderByDirection orderByDirection = OrderByDirection.ascending,
}) async {
final orderByDirectionString = switch (orderByDirection) {
OrderByDirection.ascending => 'asc',
OrderByDirection.descending => 'desc',
};

final query = 'select * from folders order by name $orderByDirectionString';
final result = await _database.rawQuery(query);
return result.map(FolderEntity.fromJson);
}

@override
Future<int> createFolder({required FolderEntity folder}) async {
return _database.insert('folders', folder.toJson());
}

@override
Future<void> renameFolder({
required int folderID,
required String newName,
}) {
return _database.update(
'folders',
{'name': newName},
where: 'id = ?',
whereArgs: [folderID],
);
}

@override
Future<void> deleteFolder({required int folderID}) async {
await _database.transaction((txn) async {
final feedRows = await txn.query(
'feeds',
columns: ['id'],
where: 'folder_id = ?',
whereArgs: [folderID],
);
final feedIDs = feedRows.map((row) => row['id'] as int).toList();

if (feedIDs.isNotEmpty) {
final placeholders = List.filled(feedIDs.length, '?').join(', ');
await txn.delete(
'feed_items',
where: 'feed_id in ($placeholders)',
whereArgs: feedIDs,
);
await txn.delete(
'feeds',
where: 'id in ($placeholders)',
whereArgs: feedIDs,
);
}

await txn.delete('folders', where: 'id = ?', whereArgs: [folderID]);
});
}

@override
Future<void> moveFeedToFolder({required int feedID, int? folderID}) async {
await _database.update(
'feeds',
{'folder_id': folderID},
where: 'id = ?',
whereArgs: [feedID],
);
}
}
45 changes: 43 additions & 2 deletions lib/domain/repositories/default_feed_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:dart_scope_functions/dart_scope_functions.dart';
import 'package:rss_it/domain/data/enums.dart';
import 'package:rss_it/domain/data/feed_entity.dart';
import 'package:rss_it/domain/data/feed_item_entity.dart';
import 'package:rss_it/domain/data/folder_entity.dart';
import 'package:rss_it/domain/providers/db_provider.dart';
import 'package:rss_it/domain/repositories/feed_repository.dart';
import 'package:rss_it_library/protos/feed.pb.dart';
Expand Down Expand Up @@ -83,6 +84,15 @@ final class DefaultFeedRepository
return result;
}

@override
Future<Iterable<FolderEntity>> getFoldersFromDB() async {
logger.info('Fetching folders from database...');
final result = await _dbProvider.getFolders();
logger.info('...folders count: ${result.length}');

return result;
}

@override
Future<Iterable<FeedItemEntity>> getFeedItemsFromDB(int feedID) async {
logger.info('Fetching feed items from database (feedID: $feedID)...');
Expand Down Expand Up @@ -125,9 +135,12 @@ final class DefaultFeedRepository
}

@override
Future<void> saveFeedToDB(Feed remoteFeed) async {
Future<void> saveFeedToDB(Feed remoteFeed, {int? folderID}) async {
logger.info('Saving feed to database...');
final feedEntity = FeedEntity.fromRemoteFeed(remoteFeed);
final feedEntity = FeedEntity.fromRemoteFeed(
remoteFeed,
folderId: folderID,
);
final feedID = await _dbProvider.createFeedAndReturnID(feed: feedEntity);
logger.info('...feed ID: $feedID');

Expand All @@ -145,4 +158,32 @@ final class DefaultFeedRepository
await _dbProvider.deleteFeed(feedID: feedID);
logger.info('...feed deleted from database.');
}

@override
Future<int> createFolder(String name) async {
logger.info('Creating folder with name $name...');
final folderID = await _dbProvider.createFolder(
folder: FolderEntity(name: name, createdAt: DateTime.now()),
);
logger.info('...folder created with id $folderID');
return folderID;
}

@override
Future<void> renameFolder(int folderID, String newName) async {
logger.info('Renaming folder $folderID to $newName');
await _dbProvider.renameFolder(folderID: folderID, newName: newName);
}

@override
Future<void> deleteFolder(int folderID) async {
logger.info('Deleting folder $folderID');
await _dbProvider.deleteFolder(folderID: folderID);
}

@override
Future<void> moveFeedToFolder({required int feedID, int? folderID}) async {
logger.info('Moving feed $feedID to folder $folderID');
await _dbProvider.moveFeedToFolder(feedID: feedID, folderID: folderID);
}
}
8 changes: 7 additions & 1 deletion lib/domain/repositories/feed_repository.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:rss_it/domain/data/enums.dart';
import 'package:rss_it/domain/data/feed_entity.dart';
import 'package:rss_it/domain/data/feed_item_entity.dart';
import 'package:rss_it/domain/data/folder_entity.dart';
import 'package:rss_it_library/protos/feed.pb.dart';

abstract interface class FeedRepository {
Expand All @@ -9,9 +10,14 @@ abstract interface class FeedRepository {
Future<Iterable<Feed>> getFeedsFromRemote(List<String> urls);
Future<Iterable<FeedEntity>> getFeedsFromDB();
Future<Iterable<FeedItemEntity>> getFeedItemsFromDB(int feedID);
Future<Iterable<FolderEntity>> getFoldersFromDB();

Future<void> updatedFeedItemsIfNecessary(Iterable<Feed> newRemoteFeeds);

Future<void> saveFeedToDB(Feed remoteFeed);
Future<void> saveFeedToDB(Feed remoteFeed, {int? folderID});
Future<int> createFolder(String name);
Future<void> renameFolder(int folderID, String newName);
Future<void> deleteFolder(int folderID);
Future<void> moveFeedToFolder({required int feedID, int? folderID});
Future<void> deleteFeedFromDB(int feedID);
}
Loading