diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index e5ac80b1c..bd58abbe7 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -253,6 +253,8 @@ "privacy": "Privacy", "terms": "Terms", "checkForUpdates": "Check for updates", + "installManually": "Install manually", + "update": "Update", "checking": "Checking...", "youAreOnTheLatestVersion": "You are on the latest version", "warning": "Warning", diff --git a/lib/store/offline_authenticator_db.dart b/lib/store/offline_authenticator_db.dart new file mode 100644 index 000000000..393e5302c --- /dev/null +++ b/lib/store/offline_authenticator_db.dart @@ -0,0 +1,170 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:ente_auth/models/authenticator/auth_entity.dart'; +import 'package:ente_auth/models/authenticator/local_auth_entity.dart'; +import 'package:flutter/foundation.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:sqflite/sqflite.dart'; + +class OfflineAuthenticatorDB { + static const _databaseName = "ente.offline_authenticator.db"; + static const _databaseVersion = 1; + + static const entityTable = 'entities'; + + OfflineAuthenticatorDB._privateConstructor(); + static final OfflineAuthenticatorDB instance = OfflineAuthenticatorDB._privateConstructor(); + + static Future? _dbFuture; + + Future get database async { + _dbFuture ??= _initDatabase(); + return _dbFuture!; + } + + Future _initDatabase() async { + final Directory documentsDirectory = + await getApplicationDocumentsDirectory(); + final String path = join(documentsDirectory.path, _databaseName); + debugPrint(path); + return await openDatabase( + path, + version: _databaseVersion, + onCreate: _onCreate, + ); + } + + Future _onCreate(Database db, int version) async { + await db.execute( + ''' + CREATE TABLE $entityTable ( + _generatedID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id TEXT, + encryptedData TEXT NOT NULL, + header TEXT NOT NULL, + createdAt INTEGER NOT NULL, + updatedAt INTEGER NOT NULL, + shouldSync INTEGER DEFAULT 0, + UNIQUE(id) + ); + ''', + ); + } + + Future insert(String encData, String header) async { + final db = await instance.database; + final int timeInMicroSeconds = DateTime.now().microsecondsSinceEpoch; + final insertedID = await db.insert( + entityTable, + { + "encryptedData": encData, + "header": header, + "shouldSync": 1, + "createdAt": timeInMicroSeconds, + "updatedAt": timeInMicroSeconds, + }, + ); + return insertedID; + } + + Future updateEntry( + int generatedID, + String encData, + String header, + ) async { + final db = await instance.database; + final int timeInMicroSeconds = DateTime.now().microsecondsSinceEpoch; + int affectedRows = await db.update( + entityTable, + { + "encryptedData": encData, + "header": header, + "shouldSync": 1, + "updatedAt": timeInMicroSeconds, + }, + where: '_generatedID = ?', + whereArgs: [generatedID], + ); + return affectedRows; + } + + Future insertOrReplace(List authEntities) async { + final db = await instance.database; + final batch = db.batch(); + for (AuthEntity authEntity in authEntities) { + final insertRow = authEntity.toMap(); + insertRow.remove('isDeleted'); + insertRow.putIfAbsent('shouldSync', () => 0); + batch.insert( + entityTable, + insertRow, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + await batch.commit(noResult: true); + } + + Future updateLocalEntity(LocalAuthEntity localAuthEntity) async { + final db = await instance.database; + await db.update( + entityTable, + localAuthEntity.toMap(), + where: '_generatedID = ?', + whereArgs: [localAuthEntity.generatedID], + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + Future getEntryByID(int genID) async { + final db = await instance.database; + final rows = await db + .query(entityTable, where: '_generatedID = ?', whereArgs: [genID]); + final listOfAuthEntities = _convertRows(rows); + if (listOfAuthEntities.isEmpty) { + return null; + } else { + return listOfAuthEntities.first; + } + } + + Future> getAll() async { + final db = await instance.database; + final rows = await db.rawQuery("SELECT * from $entityTable"); + return _convertRows(rows); + } + +// deleteByID will prefer generated id if both ids are passed during deletion + Future deleteByIDs({List? generatedIDs, List? ids}) async { + final db = await instance.database; + final batch = db.batch(); + const whereGenID = '_generatedID = ?'; + const whereID = 'id = ?'; + if (generatedIDs != null) { + for (int genId in generatedIDs) { + batch.delete(entityTable, where: whereGenID, whereArgs: [genId]); + } + } + if (ids != null) { + for (String id in ids) { + batch.delete(entityTable, where: whereID, whereArgs: [id]); + } + } + final result = await batch.commit(); + debugPrint("Done"); + } + + Future clearTable() async { + final db = await instance.database; + await db.delete(entityTable); + } + + List _convertRows(List> rows) { + final keys = []; + for (final row in rows) { + keys.add(LocalAuthEntity.fromMap(row)); + } + return keys; + } +} diff --git a/lib/ui/settings/app_update_dialog.dart b/lib/ui/settings/app_update_dialog.dart index c6286b164..39fa04bc8 100644 --- a/lib/ui/settings/app_update_dialog.dart +++ b/lib/ui/settings/app_update_dialog.dart @@ -3,10 +3,12 @@ import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/core/network.dart'; import 'package:ente_auth/ente_theme_data.dart'; +import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/services/update_service.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:open_filex/open_filex.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class AppUpdateDialog extends StatefulWidget { final LatestVersionInfo? latestVersionInfo; @@ -82,6 +84,23 @@ class _AppUpdateDialogState extends State { child: const Text( "Update", ), + + ), + ), + const Padding(padding: EdgeInsets.all(8)), + Center( + child: InkWell( + child: Text( + context.l10n.installManually, + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(decoration: TextDecoration.underline), + ), + onTap: () => launchUrlString( + widget.latestVersionInfo!.url!, + mode: LaunchMode.externalApplication, + ), ), ), ],