import 'dart:collection'; import 'package:fluent_reader_lite/models/source.dart'; import 'package:fluent_reader_lite/utils/global.dart'; import 'package:fluent_reader_lite/utils/store.dart'; import 'package:fluent_reader_lite/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:html/parser.dart'; import 'package:http/http.dart' as http; import 'package:sqflite/sqflite.dart'; import 'item.dart'; class SourcesModel with ChangeNotifier { Map _sources = Map(); Map _deleted = Map(); bool _showUnreadTip = Store.sp.getBool(StoreKeys.UNREAD_SOURCE_TIP) ?? true; bool get showUnreadTip => _showUnreadTip; set showUnreadTip(bool value) { if (_showUnreadTip != value) { _showUnreadTip = value; Store.sp.setBool(StoreKeys.UNREAD_SOURCE_TIP, value); } } bool has(String id) => _sources.containsKey(id); RSSSource getSource(String id) => _sources[id] ?? _deleted[id]; Iterable getSources() => _sources.values; Future init() async { final maps = await Global.db.query("sources"); for (var map in maps) { var source = RSSSource.fromMap(map); _sources[source.id] = source; } notifyListeners(); await updateUnreadCounts(); } Future updateUnreadCounts() async { final rows = await Global.db.rawQuery( "SELECT source, COUNT(iid) FROM items WHERE hasRead=0 GROUP BY source;" ); for (var source in _sources.values) { var cloned = source.clone(); _sources[source.id] = cloned; cloned.unreadCount = 0; } for (var row in rows) { _sources[row["source"]].unreadCount = row["COUNT(iid)"]; } notifyListeners(); } void updateUnreadCount(String sid, int diff) { _sources[sid].unreadCount += diff; notifyListeners(); } Future updateWithFetchedItems(Iterable items) async { Set changed = Set(); for (var item in items) { var source = _sources[item.source]; if (!item.hasRead) source.unreadCount += 1; if (item.date.compareTo(source.latest) > 0 || source.lastTitle.length == 0) { source.latest = item.date; source.lastTitle = item.title; changed.add(source.id); } } notifyListeners(); if (changed.length > 0) { var batch = Global.db.batch(); for (var sid in changed) { var source = _sources[sid]; batch.update( "sources", { "latest": source.latest.millisecondsSinceEpoch, "lastTitle": source.lastTitle, }, where: "sid = ?", whereArgs: [source.id], ); } await batch.commit(); } } Future put(RSSSource source, {force: false}) async { if (_deleted.containsKey(source.id) && !force) return; _sources[source.id] = source; notifyListeners(); await Global.db.insert( "sources", source.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } Future putAll(Iterable sources, {force: false}) async { Batch batch = Global.db.batch(); for (var source in sources) { if (_deleted.containsKey(source.id) && !force) continue; _sources[source.id] = source; batch.insert( "sources", source.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } notifyListeners(); await batch.commit(noResult: true); } Future updateSources() async { final tuple = await Global.service.getSources(); final sources = tuple.item1; var curr = Set.from(_sources.keys); List newSources = []; for (var source in sources) { if (curr.contains(source.id)) { curr.remove(source.id); } else { newSources.add(source); } } await putAll(newSources, force: true); await removeSources(curr); Global.groupsModel.groups = tuple.item2; fetchFavicons(); } Future removeSources(Iterable ids) async { final batch = Global.db.batch(); for (var id in ids) { if (!_sources.containsKey(id)) continue; var source = _sources[id]; batch.delete( "items", where: "source = ?", whereArgs: [id], ); batch.delete( "sources", where: "sid = ?", whereArgs: [id], ); _sources.remove(id); _deleted[id] = source; } await batch.commit(noResult: true); Global.feedsModel.initAll(); notifyListeners(); } Future fetchFavicons() async { for (var key in _sources.keys) { if (_sources[key].iconUrl == null) { _fetchFavicon(_sources[key].url).then((url) { if (!_sources.containsKey(key)) return; var source = _sources[key].clone(); source.iconUrl = url == null ? "" : url; put(source); }); } } } Future _fetchFavicon(String url) async { try { url = url.split("/").getRange(0, 3).join("/"); var result = await http.get(url); if (result.statusCode == 200) { var htmlStr = result.body; var dom = parse(htmlStr); var links = dom.getElementsByTagName("link"); for (var link in links) { var rel = link.attributes["rel"]; if ((rel == "icon" || rel == "shortcut icon") && link.attributes.containsKey("href")) { var href = link.attributes["href"]; var parsedUrl = Uri.parse(url); if (href.startsWith("//")) return parsedUrl.scheme + ":" + href; else if (href.startsWith("/")) return url + href; else return href; } } } url = url + "/favicon.ico"; if (await Utils.validateFavicon(url)) { return url; } else { return null; } } catch(exp) { return null; } } }