2023-08-02 22:25:54 +08:00
import ' dart:async ' ;
2022-07-25 16:26:51 +08:00
import ' dart:convert ' ;
2023-07-28 17:53:02 +08:00
import ' dart:io ' ;
2022-07-25 16:26:51 +08:00
import ' package:flutter/material.dart ' ;
2024-03-20 15:05:54 +08:00
import ' package:flutter_hbb/common/hbbs/hbbs.dart ' ;
2023-09-14 10:17:03 +08:00
import ' package:flutter_hbb/common/widgets/peers_view.dart ' ;
2022-07-25 16:26:51 +08:00
import ' package:flutter_hbb/models/model.dart ' ;
2022-10-08 15:53:03 +08:00
import ' package:flutter_hbb/models/peer_model.dart ' ;
2022-08-03 22:03:31 +08:00
import ' package:flutter_hbb/models/platform_model.dart ' ;
2022-07-26 17:03:19 +08:00
import ' package:get/get.dart ' ;
2023-06-23 15:10:10 +08:00
import ' package:bot_toast/bot_toast.dart ' ;
2022-07-25 16:26:51 +08:00
import ' package:http/http.dart ' as http ;
2022-09-27 17:52:36 +08:00
import ' ../common.dart ' ;
2023-07-26 19:53:57 +08:00
final syncAbOption = ' sync-ab-with-recent-sessions ' ;
bool shouldSyncAb ( ) {
return bind . mainGetLocalOption ( key: syncAbOption ) . isNotEmpty ;
}
final sortAbTagsOption = ' sync-ab-tags ' ;
bool shouldSortTags ( ) {
return bind . mainGetLocalOption ( key: sortAbTagsOption ) . isNotEmpty ;
}
2023-10-11 16:50:48 +08:00
final filterAbTagOption = ' filter-ab-by-intersection ' ;
bool filterAbTagByIntersection ( ) {
return bind . mainGetLocalOption ( key: filterAbTagOption ) . isNotEmpty ;
}
2024-03-20 15:05:54 +08:00
const _personalAddressBookName = " My address book " ;
const _legacyAddressBookName = " Legacy address book " ;
2024-03-25 19:59:21 +08:00
enum ForcePullAb {
listAndCurrent ,
current ,
}
2022-10-08 16:13:24 +08:00
class AbModel {
2024-03-20 15:05:54 +08:00
final addressbooks = Map < String , BaseAb > . fromEntries ( [ ] ) . obs ;
final RxString _currentName = ' ' . obs ;
RxString get currentName = > _currentName ;
final _dummyAb = DummyAb ( ) ;
BaseAb get current = > addressbooks [ _currentName . value ] ? ? _dummyAb ;
RxList < Peer > get currentAbPeers = > current . peers ;
RxList < String > get currentAbTags = > current . tags ;
RxList < String > get selectedTags = > current . selectedTags ;
RxBool get currentAbLoading = > current . abLoading ;
2024-03-27 21:28:21 +08:00
bool get currentAbEmpty = > current . peers . isEmpty & & current . tags . isEmpty ;
2024-03-20 15:05:54 +08:00
RxString get currentAbPullError = > current . pullError ;
RxString get currentAbPushError = > current . pushError ;
String ? _personalAbGuid ;
2024-03-28 20:50:53 +08:00
RxBool legacyMode = false . obs ;
2024-03-20 15:05:54 +08:00
2023-07-26 19:53:57 +08:00
final sortTags = shouldSortTags ( ) . obs ;
2023-10-11 16:50:48 +08:00
final filterByIntersection = filterAbTagByIntersection ( ) . obs ;
2022-07-26 17:03:19 +08:00
2023-08-13 14:46:04 +08:00
var _syncAllFromRecent = true ;
var _syncFromRecentLock = false ;
2023-08-02 22:25:54 +08:00
var _timerCounter = 0 ;
2023-08-13 14:46:04 +08:00
var _cacheLoadOnceFlag = false ;
2024-03-25 19:59:21 +08:00
var listInitialized = false ;
2024-03-20 15:05:54 +08:00
var _maxPeerOneAb = 0 ;
2022-07-25 16:26:51 +08:00
WeakReference < FFI > parent ;
2023-08-02 22:25:54 +08:00
AbModel ( this . parent ) {
2024-03-20 15:05:54 +08:00
addressbooks . clear ( ) ;
2023-08-02 22:25:54 +08:00
if ( desktopType = = DesktopType . main ) {
Timer . periodic ( Duration ( milliseconds: 500 ) , ( timer ) async {
2023-08-16 14:35:29 +08:00
if ( _timerCounter + + % 6 = = 0 ) {
if ( ! gFFI . userModel . isLogin ) return ;
2024-03-25 19:59:21 +08:00
if ( ! listInitialized ) return ;
if ( ! current . initialized | | ! current . canWrite ( ) ) return ;
2024-03-20 15:05:54 +08:00
_syncFromRecent ( ) ;
2023-08-16 14:35:29 +08:00
}
2023-08-02 22:25:54 +08:00
} ) ;
}
}
2022-07-25 16:26:51 +08:00
2024-03-20 15:05:54 +08:00
reset ( ) async {
print ( " reset ab model " ) ;
addressbooks . clear ( ) ;
setCurrentName ( ' ' ) ;
await bind . mainClearAb ( ) ;
2024-03-25 19:59:21 +08:00
listInitialized = false ;
2024-03-20 15:05:54 +08:00
}
// #region ab
2024-03-25 19:59:21 +08:00
/// Pulls the address book data from the server.
///
/// If `force` is `ForcePullAb.listAndCurrent`, the function will pull the list of address books, current address book, and try initialize personal address book.
/// If `force` is `ForcePullAb.current`, the function will only pull the current address book.
/// If `quiet` is true, the function will not display any notifications or errors.
var _pulling = false ;
Future < void > pullAb (
{ required ForcePullAb ? force , required bool quiet } ) async {
if ( _pulling ) return ;
_pulling = true ;
try {
await _pullAb ( force: force , quiet: quiet ) ;
_refreshTab ( ) ;
} catch ( _ ) { }
_pulling = false ;
2024-03-20 15:05:54 +08:00
}
2024-03-25 19:59:21 +08:00
Future < void > _pullAb (
{ required ForcePullAb ? force , required bool quiet } ) async {
2024-03-25 20:14:34 +08:00
if ( bind . isDisableAb ( ) ) return ;
2024-03-25 19:59:21 +08:00
debugPrint ( " pullAb, force: $ force , quiet: $ quiet " ) ;
2023-08-16 14:35:29 +08:00
if ( ! gFFI . userModel . isLogin ) return ;
2024-03-25 19:59:21 +08:00
if ( force = = null & & listInitialized & & current . initialized ) return ;
2024-03-27 21:28:21 +08:00
if ( ! listInitialized | | force = = ForcePullAb . listAndCurrent ) {
try {
// Read personal guid every time to avoid upgrading the server without closing the main window
2024-03-25 20:38:26 +08:00
_personalAbGuid = null ;
await _getPersonalAbGuid ( ) ;
// Determine legacy mode based on whether _personalAbGuid is null
legacyMode . value = _personalAbGuid = = null ;
2024-03-27 21:28:21 +08:00
if ( ! legacyMode . value & & _maxPeerOneAb = = 0 ) {
2024-03-25 19:59:21 +08:00
await _getAbSettings ( ) ;
}
if ( _personalAbGuid ! = null ) {
debugPrint ( " pull ab list " ) ;
List < AbProfile > abProfiles = List . empty ( growable: true ) ;
abProfiles . add ( AbProfile ( _personalAbGuid ! , _personalAddressBookName ,
gFFI . userModel . userName . value , null , ShareRule . read . value ) ) ;
// get all address book name
await _getSharedAbProfiles ( abProfiles ) ;
addressbooks . removeWhere ( ( key , value ) = >
abProfiles . firstWhereOrNull ( ( e ) = > e . name = = key ) = = null ) ;
for ( int i = 0 ; i < abProfiles . length ; i + + ) {
AbProfile p = abProfiles [ i ] ;
if ( addressbooks . containsKey ( p . name ) ) {
addressbooks [ p . name ] ? . setSharedProfile ( p ) ;
} else {
addressbooks [ p . name ] = Ab ( p , p . guid = = _personalAbGuid ) ;
}
}
} else {
// only legacy address book
addressbooks
. removeWhere ( ( key , value ) = > key ! = _legacyAddressBookName ) ;
if ( ! addressbooks . containsKey ( _legacyAddressBookName ) ) {
addressbooks [ _legacyAddressBookName ] = LegacyAb ( ) ;
2024-03-22 23:32:59 +08:00
}
2022-09-15 11:06:44 +08:00
}
2024-03-25 19:59:21 +08:00
// set current address book name
if ( ! listInitialized ) {
listInitialized = true ;
2024-03-28 20:50:53 +08:00
trySetCurrentToLast ( ) ;
2024-03-22 11:02:22 +08:00
}
2024-03-25 19:59:21 +08:00
if ( ! addressbooks . containsKey ( _currentName . value ) ) {
setCurrentName ( legacyMode . value
? _legacyAddressBookName
: _personalAddressBookName ) ;
2023-08-10 17:16:53 +08:00
}
2024-03-25 19:59:21 +08:00
// pull current address book
await current . pullAb ( quiet: quiet ) ;
// try initialize personal address book
if ( ! current . isPersonal ( ) ) {
final personalAb = addressbooks [ _personalAddressBookName ] ;
if ( personalAb ! = null & & ! personalAb . initialized ) {
await personalAb . pullAb ( quiet: quiet ) ;
}
}
} catch ( e ) {
debugPrint ( " pull ab list error: $ e " ) ;
2023-08-10 17:16:53 +08:00
}
2024-03-25 20:38:26 +08:00
} else if ( listInitialized & &
( ! current . initialized | | force = = ForcePullAb . current ) ) {
2024-03-25 19:59:21 +08:00
try {
await current . pullAb ( quiet: quiet ) ;
} catch ( e ) {
debugPrint ( " pull current Ab error: $ e " ) ;
2024-03-20 15:05:54 +08:00
}
}
2024-03-25 19:59:21 +08:00
if ( listInitialized & & current . initialized ) {
_saveCache ( ) ;
2022-07-25 16:26:51 +08:00
}
}
2024-03-20 15:05:54 +08:00
Future < bool > _getAbSettings ( ) async {
try {
final api = " ${ await bind . mainGetApiServer ( ) } /api/ab/settings " ;
var headers = getHttpHeaders ( ) ;
headers [ ' Content-Type ' ] = " application/json " ;
final resp = await http . post ( Uri . parse ( api ) , headers: headers ) ;
if ( resp . statusCode = = 404 ) {
debugPrint ( " HTTP 404, api server doesn't support shared address book " ) ;
return false ;
}
Map < String , dynamic > json =
_jsonDecodeRespMap ( utf8 . decode ( resp . bodyBytes ) , resp . statusCode ) ;
if ( json . containsKey ( ' error ' ) ) {
throw json [ ' error ' ] ;
}
if ( resp . statusCode ! = 200 ) {
throw ' HTTP ${ resp . statusCode } ' ;
}
_maxPeerOneAb = json [ ' max_peer_one_ab ' ] ? ? 0 ;
return true ;
} catch ( err ) {
debugPrint ( ' get ab settings err: ${ err . toString ( ) } ' ) ;
2022-07-26 17:03:19 +08:00
}
2024-03-20 15:05:54 +08:00
return false ;
2022-11-27 12:16:45 +08:00
}
2024-03-20 15:05:54 +08:00
Future < bool > _getPersonalAbGuid ( ) async {
try {
final api = " ${ await bind . mainGetApiServer ( ) } /api/ab/personal " ;
var headers = getHttpHeaders ( ) ;
headers [ ' Content-Type ' ] = " application/json " ;
final resp = await http . post ( Uri . parse ( api ) , headers: headers ) ;
if ( resp . statusCode = = 404 ) {
2024-03-25 19:59:21 +08:00
debugPrint ( " HTTP 404, current api server is legacy mode " ) ;
2024-03-20 15:05:54 +08:00
return false ;
}
Map < String , dynamic > json =
_jsonDecodeRespMap ( utf8 . decode ( resp . bodyBytes ) , resp . statusCode ) ;
if ( json . containsKey ( ' error ' ) ) {
throw json [ ' error ' ] ;
}
if ( resp . statusCode ! = 200 ) {
throw ' HTTP ${ resp . statusCode } ' ;
}
_personalAbGuid = json [ ' guid ' ] ;
return true ;
} catch ( err ) {
debugPrint ( ' get personal ab err: ${ err . toString ( ) } ' ) ;
2023-07-26 20:43:18 +08:00
}
2024-03-20 15:05:54 +08:00
return false ;
2023-07-26 20:43:18 +08:00
}
2024-03-25 19:59:21 +08:00
Future < bool > _getSharedAbProfiles ( List < AbProfile > profiles ) async {
2024-03-20 15:05:54 +08:00
final api = " ${ await bind . mainGetApiServer ( ) } /api/ab/shared/profiles " ;
try {
var uri0 = Uri . parse ( api ) ;
final pageSize = 100 ;
var total = 0 ;
int current = 0 ;
do {
current + = 1 ;
var uri = Uri (
scheme: uri0 . scheme ,
host: uri0 . host ,
path: uri0 . path ,
port: uri0 . port ,
queryParameters: {
' current ' : current . toString ( ) ,
' pageSize ' : pageSize . toString ( ) ,
} ) ;
var headers = getHttpHeaders ( ) ;
headers [ ' Content-Type ' ] = " application/json " ;
final resp = await http . post ( uri , headers: headers ) ;
Map < String , dynamic > json =
_jsonDecodeRespMap ( utf8 . decode ( resp . bodyBytes ) , resp . statusCode ) ;
if ( json . containsKey ( ' error ' ) ) {
throw json [ ' error ' ] ;
}
if ( resp . statusCode ! = 200 ) {
throw ' HTTP ${ resp . statusCode } ' ;
}
if ( json . containsKey ( ' total ' ) ) {
if ( total = = 0 ) total = json [ ' total ' ] ;
if ( json . containsKey ( ' data ' ) ) {
final data = json [ ' data ' ] ;
if ( data is List ) {
for ( final profile in data ) {
final u = AbProfile . fromJson ( profile ) ;
2024-03-25 19:59:21 +08:00
int index = profiles . indexWhere ( ( e ) = > e . name = = u . name ) ;
2024-03-20 15:05:54 +08:00
if ( index < 0 ) {
2024-03-25 19:59:21 +08:00
profiles . add ( u ) ;
2024-03-20 15:05:54 +08:00
} else {
2024-03-25 19:59:21 +08:00
profiles [ index ] = u ;
2024-03-20 15:05:54 +08:00
}
}
}
}
}
} while ( current * pageSize < total ) ;
return true ;
} catch ( err ) {
debugPrint ( ' _getSharedAbProfiles err: ${ err . toString ( ) } ' ) ;
2023-08-13 14:46:04 +08:00
}
2024-03-20 15:05:54 +08:00
return false ;
2022-07-26 17:03:19 +08:00
}
2024-03-20 15:05:54 +08:00
// #endregion
// #region rule
List < String > addressBooksCanWrite ( ) {
List < String > list = [ ] ;
addressbooks . forEach ( ( key , value ) async {
if ( value . canWrite ( ) ) {
list . add ( key ) ;
2023-08-03 16:48:14 +08:00
}
2024-03-20 15:05:54 +08:00
} ) ;
return list ;
2023-08-03 16:48:14 +08:00
}
2024-03-20 15:05:54 +08:00
// #endregion
// #region peer
Future < String ? > addIdToCurrent (
String id , String alias , String password , List < dynamic > tags ) async {
if ( currentAbPeers . where ( ( element ) = > element . id = = id ) . isNotEmpty ) {
return " $ id already exists in address book $ _currentName " ;
2023-08-16 10:18:29 +08:00
}
2024-03-20 15:05:54 +08:00
Map < String , dynamic > peer = {
' id ' : id ,
' alias ' : alias ,
' tags ' : tags ,
} ;
// avoid set existing password to empty
if ( password . isNotEmpty ) {
peer [ ' password ' ] = password ;
2023-08-16 10:18:29 +08:00
}
2024-03-20 15:05:54 +08:00
final ret = await addPeersTo ( [ peer ] , _currentName . value ) ;
2024-04-02 22:08:47 +08:00
_syncAllFromRecent = true ;
2023-08-16 10:18:29 +08:00
return ret ;
2022-07-26 17:03:19 +08:00
}
2024-03-20 15:05:54 +08:00
// Use Map<String, dynamic> rather than Peer to distinguish between empty and null
Future < String ? > addPeersTo (
List < Map < String , dynamic > > ps ,
String name ,
) async {
final ab = addressbooks [ name ] ;
if ( ab = = null ) {
return ' no such addressbook: $ name ' ;
}
String ? errMsg = await ab . addPeers ( ps ) ;
await pullNonLegacyAfterChange ( name: name ) ;
if ( name = = _currentName . value ) {
_refreshTab ( ) ;
}
_syncAllFromRecent = true ;
_saveCache ( ) ;
return errMsg ;
2022-11-28 18:16:29 +08:00
}
2024-03-20 15:05:54 +08:00
Future < bool > changeTagForPeers ( List < String > ids , List < dynamic > tags ) async {
bool ret = await current . changeTagForPeers ( ids , tags ) ;
await pullNonLegacyAfterChange ( ) ;
currentAbPeers . refresh ( ) ;
_saveCache ( ) ;
return ret ;
2022-07-26 17:03:19 +08:00
}
2024-03-20 15:05:54 +08:00
Future < bool > changeAlias ( { required String id , required String alias } ) async {
bool res = await current . changeAlias ( id: id , alias: alias ) ;
await pullNonLegacyAfterChange ( ) ;
currentAbPeers . refresh ( ) ;
_saveCache ( ) ;
return res ;
2022-07-26 17:03:19 +08:00
}
2024-03-20 15:05:54 +08:00
Future < bool > changePersonalHashPassword ( String id , String hash ) async {
var ret = false ;
final personalAb = addressbooks [ _personalAddressBookName ] ;
if ( personalAb ! = null ) {
ret = await personalAb . changePersonalHashPassword ( id , hash ) ;
2024-04-02 22:08:47 +08:00
await personalAb . pullAb ( quiet: true ) ;
2024-03-20 15:05:54 +08:00
} else {
final legacyAb = addressbooks [ _legacyAddressBookName ] ;
if ( legacyAb ! = null ) {
ret = await legacyAb . changePersonalHashPassword ( id , hash ) ;
}
}
_saveCache ( ) ;
return ret ;
2022-07-26 17:03:19 +08:00
}
2024-03-20 15:05:54 +08:00
Future < bool > changeSharedPassword (
String abName , String id , String password ) async {
2024-04-02 22:08:47 +08:00
final ab = addressbooks [ abName ] ;
if ( ab = = null ) return false ;
final ret = await ab . changeSharedPassword ( id , password ) ;
await ab . pullAb ( quiet: true ) ;
2024-03-20 15:05:54 +08:00
return ret ;
2023-08-03 16:48:14 +08:00
}
2024-03-20 15:05:54 +08:00
Future < bool > deletePeers ( List < String > ids ) async {
final ret = await current . deletePeers ( ids ) ;
await pullNonLegacyAfterChange ( ) ;
currentAbPeers . refresh ( ) ;
_refreshTab ( ) ;
_saveCache ( ) ;
if ( legacyMode . value & & current . isPersonal ( ) ) {
// non-legacy mode not add peers automatically
Future . delayed ( Duration ( seconds: 2 ) , ( ) async {
if ( ! shouldSyncAb ( ) ) return ;
var hasSynced = false ;
for ( var id in ids ) {
if ( await bind . mainPeerExists ( id: id ) ) {
hasSynced = true ;
break ;
}
}
if ( hasSynced ) {
BotToast . showText (
contentColor: Colors . lightBlue ,
text: translate ( ' synced_peer_readded_tip ' ) ) ;
_syncAllFromRecent = true ;
}
} ) ;
2022-07-26 17:03:19 +08:00
}
2024-03-20 15:05:54 +08:00
return ret ;
2022-07-26 17:03:19 +08:00
}
2024-03-20 15:05:54 +08:00
// #endregion
// #region tags
Future < bool > addTags ( List < String > tagList ) async {
final ret = await current . addTags ( tagList , { } ) ;
await pullNonLegacyAfterChange ( ) ;
_saveCache ( ) ;
return ret ;
}
Future < bool > renameTag ( String oldTag , String newTag ) async {
final ret = await current . renameTag ( oldTag , newTag ) ;
await pullNonLegacyAfterChange ( ) ;
2023-08-10 10:08:33 +08:00
selectedTags . value = selectedTags . map ( ( e ) {
if ( e = = oldTag ) {
return newTag ;
} else {
2023-08-10 11:12:58 +08:00
return e ;
2023-08-10 10:08:33 +08:00
}
} ) . toList ( ) ;
2024-03-20 15:05:54 +08:00
_saveCache ( ) ;
return ret ;
2022-07-26 17:03:19 +08:00
}
2022-07-27 14:29:47 +08:00
2024-03-20 15:05:54 +08:00
Future < bool > setTagColor ( String tag , Color color ) async {
final ret = await current . setTagColor ( tag , color ) ;
await pullNonLegacyAfterChange ( ) ;
_saveCache ( ) ;
return ret ;
2023-08-22 19:07:01 +08:00
}
2024-03-20 15:05:54 +08:00
Future < bool > deleteTag ( String tag ) async {
final ret = await current . deleteTag ( tag ) ;
await pullNonLegacyAfterChange ( ) ;
_saveCache ( ) ;
return ret ;
2023-08-22 19:07:01 +08:00
}
2024-03-20 15:05:54 +08:00
// #endregion
2023-08-13 14:46:04 +08:00
2024-03-20 15:05:54 +08:00
// #region sync from recent
Future < void > _syncFromRecent ( { bool push = true } ) async {
2023-08-13 14:46:04 +08:00
if ( ! _syncFromRecentLock ) {
_syncFromRecentLock = true ;
2023-08-16 08:09:03 +08:00
await _syncFromRecentWithoutLock ( push: push ) ;
2023-08-13 14:46:04 +08:00
_syncFromRecentLock = false ;
2022-11-28 18:16:29 +08:00
}
2023-08-13 14:46:04 +08:00
}
2022-11-28 18:16:29 +08:00
2023-08-16 08:09:03 +08:00
Future < void > _syncFromRecentWithoutLock ( { bool push = true } ) async {
2023-08-02 22:25:54 +08:00
Future < List < Peer > > getRecentPeers ( ) async {
try {
List < String > filteredPeerIDs ;
2023-08-13 14:46:04 +08:00
if ( _syncAllFromRecent ) {
_syncAllFromRecent = false ;
2023-08-16 08:59:50 +08:00
filteredPeerIDs = [ ] ;
2023-08-02 22:25:54 +08:00
} else {
final new_stored_str = await bind . mainGetNewStoredPeers ( ) ;
if ( new_stored_str . isEmpty ) return [ ] ;
2023-08-16 08:59:50 +08:00
filteredPeerIDs = ( jsonDecode ( new_stored_str ) as List < dynamic > )
. map ( ( e ) = > e . toString ( ) )
. toList ( ) ;
if ( filteredPeerIDs . isEmpty ) return [ ] ;
2023-08-02 22:25:54 +08:00
}
final loadStr = await bind . mainLoadRecentPeersForAb (
filter: jsonEncode ( filteredPeerIDs ) ) ;
if ( loadStr . isEmpty ) {
return [ ] ;
}
List < dynamic > mapPeers = jsonDecode ( loadStr ) ;
List < Peer > recents = List . empty ( growable: true ) ;
for ( var m in mapPeers ) {
if ( m is Map < String , dynamic > ) {
recents . add ( Peer . fromJson ( m ) ) ;
}
}
return recents ;
} catch ( e ) {
2024-03-20 15:05:54 +08:00
debugPrint ( ' getRecentPeers: $ e ' ) ;
2023-08-02 22:25:54 +08:00
}
return [ ] ;
}
try {
if ( ! shouldSyncAb ( ) ) return ;
final recents = await getRecentPeers ( ) ;
if ( recents . isEmpty ) return ;
2024-03-20 15:05:54 +08:00
debugPrint ( " sync from recent, len: ${ recents . length } " ) ;
2024-03-25 19:59:21 +08:00
if ( current . canWrite ( ) & & current . initialized ) {
await current . syncFromRecent ( recents ) ;
}
2023-08-02 22:25:54 +08:00
} catch ( e ) {
2024-03-20 15:05:54 +08:00
debugPrint ( ' _syncFromRecentWithoutLock: $ e ' ) ;
2022-11-28 18:16:29 +08:00
}
}
2024-03-20 15:05:54 +08:00
void setShouldAsync ( bool v ) async {
await bind . mainSetLocalOption ( key: syncAbOption , value: v ? ' Y ' : ' ' ) ;
_syncAllFromRecent = true ;
_timerCounter = 0 ;
}
// #endregion
// #region cache
2023-08-11 15:27:50 +08:00
_saveCache ( ) {
2023-08-02 22:25:54 +08:00
try {
2024-03-20 15:05:54 +08:00
var ab_entries = _serializeCache ( ) ;
Map < String , dynamic > m = < String , dynamic > {
2023-08-02 22:25:54 +08:00
" access_token " : bind . mainGetLocalOption ( key: ' access_token ' ) ,
2024-03-20 15:05:54 +08:00
" ab_entries " : ab_entries ,
} ;
2023-08-02 22:25:54 +08:00
bind . mainSaveAb ( json: jsonEncode ( m ) ) ;
} catch ( e ) {
debugPrint ( ' ab save: $ e ' ) ;
2022-07-29 12:03:24 +08:00
}
}
2023-08-11 15:27:50 +08:00
2024-03-20 15:05:54 +08:00
List < dynamic > _serializeCache ( ) {
var res = [ ] ;
addressbooks . forEach ( ( key , value ) {
2024-03-25 19:59:21 +08:00
if ( ! value . isPersonal ( ) & & key ! = current . name ( ) ) return ;
2024-03-20 15:05:54 +08:00
res . add ( {
" guid " : value . sharedProfile ( ) ? . guid ? ? ' ' ,
" name " : key ,
" tags " : value . tags ,
" peers " : value . peers
2024-04-02 22:08:47 +08:00
. map ( ( e ) = > e . toCustomJson ( includingHash: value . isPersonal ( ) ) )
2024-03-20 15:05:54 +08:00
. toList ( ) ,
" tag_colors " : jsonEncode ( value . tagColors )
} ) ;
} ) ;
return res ;
}
2024-03-28 20:50:53 +08:00
trySetCurrentToLast ( ) {
final name = bind . getLocalFlutterOption ( k: ' current-ab-name ' ) ;
if ( addressbooks . containsKey ( name ) ) {
_currentName . value = name ;
}
}
2023-09-24 17:56:35 +08:00
Future < void > loadCache ( ) async {
2023-08-11 15:27:50 +08:00
try {
2024-03-20 15:05:54 +08:00
if ( _cacheLoadOnceFlag | | currentAbLoading . value ) return ;
2023-08-13 14:46:04 +08:00
_cacheLoadOnceFlag = true ;
2023-08-11 15:27:50 +08:00
final access_token = bind . mainGetLocalOption ( key: ' access_token ' ) ;
if ( access_token . isEmpty ) return ;
final cache = await bind . mainLoadAb ( ) ;
2024-03-20 15:05:54 +08:00
if ( currentAbLoading . value ) return ;
2023-08-11 15:27:50 +08:00
final data = jsonDecode ( cache ) ;
if ( data = = null | | data [ ' access_token ' ] ! = access_token ) return ;
2024-03-20 15:05:54 +08:00
_deserializeCache ( data ) ;
2024-03-28 20:50:53 +08:00
legacyMode . value = addressbooks . containsKey ( _legacyAddressBookName ) ;
trySetCurrentToLast ( ) ;
2023-08-11 15:27:50 +08:00
} catch ( e ) {
debugPrint ( " load ab cache: $ e " ) ;
}
}
2023-08-15 12:09:33 +08:00
2024-03-20 15:05:54 +08:00
_deserializeCache ( dynamic data ) {
if ( data = = null ) return ;
reset ( ) ;
final abEntries = data [ ' ab_entries ' ] ;
if ( abEntries is List ) {
for ( var i = 0 ; i < abEntries . length ; i + + ) {
var abEntry = abEntries [ i ] ;
if ( abEntry is Map < String , dynamic > ) {
var guid = abEntry [ ' guid ' ] ;
var name = abEntry [ ' name ' ] ;
final BaseAb ab ;
if ( name = = _legacyAddressBookName ) {
ab = LegacyAb ( ) ;
} else {
if ( name = = null | | guid = = null ) {
continue ;
}
ab = Ab ( AbProfile ( guid , name , ' ' , ' ' , ShareRule . read . value ) ,
name = = _personalAddressBookName ) ;
}
addressbooks [ name ] = ab ;
if ( abEntry [ ' tags ' ] is List ) {
ab . tags . value =
( abEntry [ ' tags ' ] as List ) . map ( ( e ) = > e . toString ( ) ) . toList ( ) ;
}
if ( abEntry [ ' peers ' ] is List ) {
for ( var peer in abEntry [ ' peers ' ] ) {
ab . peers . add ( Peer . fromJson ( peer ) ) ;
}
}
if ( abEntry [ ' tag_colors ' ] is String ) {
Map < String , dynamic > map = jsonDecode ( abEntry [ ' tag_colors ' ] ) ;
ab . tagColors . value = Map < String , int > . from ( map ) ;
}
}
2023-08-15 12:09:33 +08:00
}
}
}
2023-08-17 18:21:37 +08:00
2024-03-20 15:05:54 +08:00
// #endregion
// #region tools
Peer ? find ( String id ) {
return currentAbPeers . firstWhereOrNull ( ( e ) = > e . id = = id ) ;
2023-08-22 19:07:01 +08:00
}
2024-03-20 15:05:54 +08:00
bool idContainByCurrent ( String id ) {
return currentAbPeers . where ( ( element ) = > element . id = = id ) . isNotEmpty ;
}
void unsetSelectedTags ( ) {
selectedTags . clear ( ) ;
}
List < dynamic > getPeerTags ( String id ) {
final it = currentAbPeers . where ( ( p0 ) = > p0 . id = = id ) ;
if ( it . isEmpty ) {
return [ ] ;
} else {
return it . first . tags ;
2023-08-22 19:07:01 +08:00
}
2024-03-20 15:05:54 +08:00
}
Color getCurrentAbTagColor ( String tag ) {
int ? colorValue = current . tagColors [ tag ] ;
if ( colorValue ! = null ) {
return Color ( colorValue ) ;
2023-08-22 19:07:01 +08:00
}
2024-03-20 15:05:54 +08:00
return str2color2 ( tag , existing: current . tagColors . values . toList ( ) ) ;
}
List < String > addressBookNames ( ) {
return addressbooks . keys . toList ( ) ;
}
2024-03-25 19:59:21 +08:00
Future < void > setCurrentName ( String name ) async {
final oldName = _currentName . value ;
2024-03-20 15:05:54 +08:00
if ( addressbooks . containsKey ( name ) ) {
_currentName . value = name ;
} else {
if ( addressbooks . containsKey ( _personalAddressBookName ) ) {
_currentName . value = _personalAddressBookName ;
} else if ( addressbooks . containsKey ( _legacyAddressBookName ) ) {
_currentName . value = _legacyAddressBookName ;
} else {
_currentName . value = ' ' ;
}
2023-08-22 19:07:01 +08:00
}
2024-03-25 19:59:21 +08:00
if ( ! current . initialized ) {
2024-03-27 21:28:21 +08:00
await current . pullAb ( quiet: false ) ;
2024-03-25 19:59:21 +08:00
}
2024-03-20 15:05:54 +08:00
_refreshTab ( ) ;
2024-03-25 19:59:21 +08:00
if ( oldName ! = _currentName . value ) {
_syncAllFromRecent = true ;
2024-03-28 20:50:53 +08:00
_saveCache ( ) ;
2024-03-25 19:59:21 +08:00
}
2024-03-20 15:05:54 +08:00
}
bool isCurrentAbFull ( bool warn ) {
2024-03-22 11:02:22 +08:00
final res = current . isFull ( ) ;
if ( res & & warn ) {
BotToast . showText (
contentColor: Colors . red , text: translate ( ' exceed_max_devices ' ) ) ;
}
return res ;
2024-03-20 15:05:54 +08:00
}
void _refreshTab ( ) {
platformFFI . tryHandle ( { ' name ' : LoadEvent . addressBook } ) ;
}
// should not call this function in a loop call stack
Future < void > pullNonLegacyAfterChange ( { String ? name } ) async {
if ( name = = null ) {
if ( current . name ( ) ! = _legacyAddressBookName ) {
2024-03-25 19:59:21 +08:00
return await current . pullAb ( quiet: true ) ;
2024-03-20 15:05:54 +08:00
}
} else if ( name ! = _legacyAddressBookName ) {
final ab = addressbooks [ name ] ;
if ( ab ! = null ) {
2024-03-25 19:59:21 +08:00
return await ab . pullAb ( quiet: true ) ;
2024-03-20 15:05:54 +08:00
}
2023-08-22 19:07:01 +08:00
}
}
2024-03-20 15:05:54 +08:00
List < String > idExistIn ( String id ) {
List < String > v = [ ] ;
addressbooks . forEach ( ( key , value ) {
if ( value . peers . any ( ( e ) = > e . id = = id ) ) {
v . add ( key ) ;
2023-08-17 18:21:37 +08:00
}
} ) ;
2024-03-20 15:05:54 +08:00
return v ;
2023-08-17 18:21:37 +08:00
}
2023-09-14 10:17:03 +08:00
2024-03-20 15:05:54 +08:00
List < Peer > allPeers ( ) {
List < Peer > v = [ ] ;
addressbooks . forEach ( ( key , value ) {
v . addAll ( value . peers . map ( ( e ) = > Peer . copy ( e ) ) . toList ( ) ) ;
} ) ;
return v ;
}
String translatedName ( String name ) {
if ( name = = _personalAddressBookName | | name = = _legacyAddressBookName ) {
return translate ( name ) ;
} else {
return name ;
}
}
// #endregion
}
abstract class BaseAb {
final peers = List < Peer > . empty ( growable: true ) . obs ;
final RxList < String > tags = < String > [ ] . obs ;
final RxMap < String , int > tagColors = Map < String , int > . fromEntries ( [ ] ) . obs ;
final selectedTags = List < String > . empty ( growable: true ) . obs ;
final pullError = " " . obs ;
final pushError = " " . obs ;
final abLoading = false . obs ;
2024-03-25 19:59:21 +08:00
bool initialized = false ;
2023-09-21 16:34:04 +08:00
2024-03-20 15:05:54 +08:00
String name ( ) ;
bool isPersonal ( ) {
return name ( ) = = _personalAddressBookName | |
name ( ) = = _legacyAddressBookName ;
}
2024-04-02 22:08:47 +08:00
bool isLegacy ( ) {
return name ( ) = = _legacyAddressBookName ;
}
2024-03-25 19:59:21 +08:00
Future < void > pullAb ( { quiet = false } ) async {
debugPrint ( " pull ab \" ${ name ( ) } \" " ) ;
2024-03-20 15:05:54 +08:00
if ( abLoading . value ) return ;
if ( ! quiet ) {
abLoading . value = true ;
pullError . value = " " ;
2023-09-21 16:34:04 +08:00
}
2024-03-25 19:59:21 +08:00
initialized = false ;
2024-03-27 21:28:21 +08:00
try {
initialized = await pullAbImpl ( quiet: quiet ) ;
} catch ( _ ) { }
2024-03-20 15:05:54 +08:00
abLoading . value = false ;
}
2024-03-25 19:59:21 +08:00
Future < bool > pullAbImpl ( { quiet = false } ) ;
2024-03-20 15:05:54 +08:00
Future < String ? > addPeers ( List < Map < String , dynamic > > ps ) ;
removeHash ( Map < String , dynamic > p ) {
p . remove ( ' hash ' ) ;
}
removePassword ( Map < String , dynamic > p ) {
p . remove ( ' password ' ) ;
}
Future < bool > changeTagForPeers ( List < String > ids , List < dynamic > tags ) ;
Future < bool > changeAlias ( { required String id , required String alias } ) ;
Future < bool > changePersonalHashPassword ( String id , String hash ) ;
Future < bool > changeSharedPassword ( String id , String password ) ;
Future < bool > deletePeers ( List < String > ids ) ;
Future < bool > addTags ( List < String > tagList , Map < String , int > tagColorMap ) ;
bool tagContainBy ( String tag ) {
return tags . where ( ( element ) = > element = = tag ) . isNotEmpty ;
}
Future < bool > renameTag ( String oldTag , String newTag ) ;
Future < bool > setTagColor ( String tag , Color color ) ;
Future < bool > deleteTag ( String tag ) ;
2024-03-22 11:02:22 +08:00
bool isFull ( ) ;
2024-03-20 15:05:54 +08:00
2024-03-22 23:32:59 +08:00
void setSharedProfile ( AbProfile profile ) ;
2024-03-20 15:05:54 +08:00
AbProfile ? sharedProfile ( ) ;
bool canWrite ( ) ;
bool fullControl ( ) ;
Future < void > syncFromRecent ( List < Peer > recents ) ;
}
class LegacyAb extends BaseAb {
final sortTags = shouldSortTags ( ) . obs ;
final filterByIntersection = filterAbTagByIntersection ( ) . obs ;
bool get emtpy = > peers . isEmpty & & tags . isEmpty ;
2024-03-22 11:02:22 +08:00
// licensedDevices is obtained from personal ab, shared ab restrict it in server
var licensedDevices = 0 ;
2024-03-20 15:05:54 +08:00
LegacyAb ( ) ;
@ override
AbProfile ? sharedProfile ( ) {
return null ;
}
2024-03-22 23:32:59 +08:00
@ override
void setSharedProfile ( AbProfile ? profile ) { }
2024-03-20 15:05:54 +08:00
@ override
bool canWrite ( ) {
return true ;
}
@ override
bool fullControl ( ) {
return true ;
}
@ override
2024-03-22 11:02:22 +08:00
bool isFull ( ) {
return licensedDevices > 0 & & peers . length > = licensedDevices ;
2024-03-20 15:05:54 +08:00
}
@ override
String name ( ) {
return _legacyAddressBookName ;
}
@ override
2024-03-25 19:59:21 +08:00
Future < bool > pullAbImpl ( { quiet = false } ) async {
bool ret = false ;
2024-03-20 15:05:54 +08:00
final api = " ${ await bind . mainGetApiServer ( ) } /api/ab " ;
int ? statusCode ;
try {
var authHeaders = getHttpHeaders ( ) ;
authHeaders [ ' Content-Type ' ] = " application/json " ;
authHeaders [ ' Accept-Encoding ' ] = " gzip " ;
final resp = await http . get ( Uri . parse ( api ) , headers: authHeaders ) ;
statusCode = resp . statusCode ;
if ( resp . body . toLowerCase ( ) = = " null " ) {
2024-04-02 21:41:54 +08:00
// normal reply, empty ab return null
2024-03-20 15:05:54 +08:00
tags . clear ( ) ;
tagColors . clear ( ) ;
peers . clear ( ) ;
} else if ( resp . body . isNotEmpty ) {
Map < String , dynamic > json =
_jsonDecodeRespMap ( utf8 . decode ( resp . bodyBytes ) , resp . statusCode ) ;
if ( json . containsKey ( ' error ' ) ) {
throw json [ ' error ' ] ;
} else if ( json . containsKey ( ' data ' ) ) {
try {
2024-03-22 11:02:22 +08:00
licensedDevices = json [ ' licensed_devices ' ] ;
2024-03-20 15:05:54 +08:00
// ignore: empty_catches
} catch ( e ) { }
final data = jsonDecode ( json [ ' data ' ] ) ;
if ( data ! = null ) {
_deserialize ( data ) ;
}
2024-03-25 19:59:21 +08:00
ret = true ;
2024-03-20 15:05:54 +08:00
}
}
} catch ( err ) {
if ( ! quiet ) {
pullError . value =
' ${ translate ( ' pull_ab_failed_tip ' ) } : ${ translate ( err . toString ( ) ) } ' ;
}
} finally {
if ( pullError . isNotEmpty ) {
if ( statusCode = = 401 ) {
gFFI . userModel . reset ( resetOther: true ) ;
}
}
}
2024-03-25 19:59:21 +08:00
return ret ;
2024-03-20 15:05:54 +08:00
}
Future < bool > pushAb (
{ bool toastIfFail = true , bool toastIfSucc = true } ) async {
debugPrint ( " pushAb: toastIfFail: $ toastIfFail , toastIfSucc: $ toastIfSucc " ) ;
if ( ! gFFI . userModel . isLogin ) return false ;
pushError . value = ' ' ;
bool ret = false ;
try {
//https: //stackoverflow.com/questions/68249333/flutter-getx-updating-item-in-children-list-is-not-reactive
peers . refresh ( ) ;
final api = " ${ await bind . mainGetApiServer ( ) } /api/ab " ;
var authHeaders = getHttpHeaders ( ) ;
authHeaders [ ' Content-Type ' ] = " application/json " ;
final body = jsonEncode ( { " data " : jsonEncode ( _serialize ( ) ) } ) ;
http . Response resp ;
// support compression
2024-03-22 11:02:22 +08:00
if ( licensedDevices > 0 & & body . length > 1024 ) {
2024-03-20 15:05:54 +08:00
authHeaders [ ' Content-Encoding ' ] = " gzip " ;
resp = await http . post ( Uri . parse ( api ) ,
headers: authHeaders , body: GZipCodec ( ) . encode ( utf8 . encode ( body ) ) ) ;
} else {
resp =
await http . post ( Uri . parse ( api ) , headers: authHeaders , body: body ) ;
}
if ( resp . statusCode = = 200 & &
( resp . body . isEmpty | | resp . body . toLowerCase ( ) = = ' null ' ) ) {
ret = true ;
} else {
Map < String , dynamic > json =
_jsonDecodeRespMap ( utf8 . decode ( resp . bodyBytes ) , resp . statusCode ) ;
if ( json . containsKey ( ' error ' ) ) {
throw json [ ' error ' ] ;
} else if ( resp . statusCode = = 200 ) {
ret = true ;
} else {
throw ' HTTP ${ resp . statusCode } ' ;
}
}
} catch ( e ) {
pushError . value =
' ${ translate ( ' push_ab_failed_tip ' ) } : ${ translate ( e . toString ( ) ) } ' ;
}
if ( ! ret & & toastIfFail ) {
BotToast . showText ( contentColor: Colors . red , text: pushError . value ) ;
}
if ( ret & & toastIfSucc ) {
showToast ( translate ( ' Successful ' ) ) ;
}
return ret ;
}
// #region Peer
@ override
Future < String ? > addPeers ( List < Map < String , dynamic > > ps ) async {
bool full = false ;
for ( var p in ps ) {
2024-03-22 11:02:22 +08:00
if ( ! isFull ( ) ) {
2024-03-20 15:05:54 +08:00
p . remove ( ' password ' ) ; // legacy ab ignore password
final index = peers . indexWhere ( ( e ) = > e . id = = p [ ' id ' ] ) ;
if ( index > = 0 ) {
_merge ( Peer . fromJson ( p ) , peers [ index ] ) ;
_mergePeerFromGroup ( peers [ index ] ) ;
} else {
peers . add ( Peer . fromJson ( p ) ) ;
}
} else {
full = true ;
break ;
}
}
if ( ! await pushAb ( ) ) {
return " Failed to push to server " ;
} else if ( full ) {
return translate ( " exceed_max_devices " ) ;
} else {
return null ;
}
}
_mergePeerFromGroup ( Peer p ) {
final g = gFFI . groupModel . peers . firstWhereOrNull ( ( e ) = > p . id = = e . id ) ;
if ( g = = null ) return ;
if ( p . username . isEmpty ) {
p . username = g . username ;
}
if ( p . hostname . isEmpty ) {
p . hostname = g . hostname ;
}
if ( p . platform . isEmpty ) {
p . platform = g . platform ;
}
}
@ override
Future < bool > changeTagForPeers ( List < String > ids , List < dynamic > tags ) async {
peers . map ( ( e ) {
if ( ids . contains ( e . id ) ) {
e . tags = tags ;
}
} ) . toList ( ) ;
return await pushAb ( ) ;
}
@ override
Future < bool > changeAlias ( { required String id , required String alias } ) async {
final it = peers . where ( ( element ) = > element . id = = id ) ;
if ( it . isEmpty ) {
return false ;
}
it . first . alias = alias ;
return await pushAb ( ) ;
}
@ override
Future < bool > changeSharedPassword ( String id , String password ) async {
// no need to implement
return false ;
}
@ override
Future < void > syncFromRecent ( List < Peer > recents ) async {
bool peerSyncEqual ( Peer a , Peer b ) {
return a . hash = = b . hash & &
a . username = = b . username & &
a . platform = = b . platform & &
a . hostname = = b . hostname & &
a . alias = = b . alias ;
}
bool needSync = false ;
for ( var i = 0 ; i < recents . length ; i + + ) {
var r = recents [ i ] ;
var index = peers . indexWhere ( ( e ) = > e . id = = r . id ) ;
if ( index < 0 ) {
2024-03-22 11:02:22 +08:00
if ( ! isFull ( ) ) {
2024-03-20 15:05:54 +08:00
peers . add ( r ) ;
needSync = true ;
}
} else {
Peer old = Peer . copy ( peers [ index ] ) ;
_merge ( r , peers [ index ] ) ;
if ( ! peerSyncEqual ( peers [ index ] , old ) ) {
needSync = true ;
}
}
}
if ( needSync ) {
await pushAb ( toastIfSucc: false , toastIfFail: false ) ;
gFFI . abModel . _refreshTab ( ) ;
}
// Pull cannot be used for sync to avoid cyclic sync.
}
void _merge ( Peer r , Peer p ) {
p . hash = r . hash . isEmpty ? p . hash : r . hash ;
p . username = r . username . isEmpty ? p . username : r . username ;
p . hostname = r . hostname . isEmpty ? p . hostname : r . hostname ;
p . platform = r . platform . isEmpty ? p . platform : r . platform ;
p . alias = p . alias . isEmpty ? r . alias : p . alias ;
}
@ override
Future < bool > changePersonalHashPassword ( String id , String hash ) async {
bool changed = false ;
final it = peers . where ( ( element ) = > element . id = = id ) ;
if ( it . isNotEmpty ) {
if ( it . first . hash ! = hash ) {
it . first . hash = hash ;
changed = true ;
}
}
if ( changed ) {
return await pushAb ( toastIfSucc: false , toastIfFail: false ) ;
}
return true ;
}
@ override
Future < bool > deletePeers ( List < String > ids ) async {
peers . removeWhere ( ( e ) = > ids . contains ( e . id ) ) ;
return await pushAb ( ) ;
}
// #endregion
// #region Tag
@ override
Future < bool > addTags (
List < String > tagList , Map < String , int > tagColorMap ) async {
for ( var e in tagList ) {
if ( ! tagContainBy ( e ) ) {
tags . add ( e ) ;
}
2024-03-22 23:32:59 +08:00
if ( tagColors [ e ] = = null ) {
tagColors [ e ] = str2color2 ( e , existing: tagColors . values . toList ( ) ) . value ;
}
2024-03-20 15:05:54 +08:00
}
return await pushAb ( ) ;
}
@ override
Future < bool > renameTag ( String oldTag , String newTag ) async {
if ( tags . contains ( newTag ) ) {
BotToast . showText (
contentColor: Colors . red , text: ' Tag $ newTag already exists ' ) ;
return false ;
}
tags . value = tags . map ( ( e ) {
if ( e = = oldTag ) {
return newTag ;
} else {
return e ;
}
} ) . toList ( ) ;
for ( var peer in peers ) {
peer . tags = peer . tags . map ( ( e ) {
if ( e = = oldTag ) {
return newTag ;
} else {
return e ;
}
} ) . toList ( ) ;
}
int ? oldColor = tagColors [ oldTag ] ;
if ( oldColor ! = null ) {
tagColors . remove ( oldTag ) ;
tagColors . addAll ( { newTag: oldColor } ) ;
}
return await pushAb ( ) ;
}
@ override
Future < bool > setTagColor ( String tag , Color color ) async {
if ( tags . contains ( tag ) ) {
tagColors [ tag ] = color . value ;
}
return await pushAb ( ) ;
}
@ override
Future < bool > deleteTag ( String tag ) async {
gFFI . abModel . selectedTags . remove ( tag ) ;
tags . removeWhere ( ( element ) = > element = = tag ) ;
tagColors . remove ( tag ) ;
for ( var peer in peers ) {
if ( peer . tags . isEmpty ) {
continue ;
}
if ( peer . tags . contains ( tag ) ) {
peer . tags . remove ( tag ) ;
}
}
return await pushAb ( ) ;
}
// #endregion
Map < String , dynamic > _serialize ( ) {
final peersJsonData =
2024-04-02 22:08:47 +08:00
peers . map ( ( e ) = > e . toCustomJson ( includingHash: true ) ) . toList ( ) ;
2024-03-22 23:32:59 +08:00
for ( var e in tags ) {
if ( tagColors [ e ] = = null ) {
tagColors [ e ] = str2color2 ( e , existing: tagColors . values . toList ( ) ) . value ;
}
}
2024-03-20 15:05:54 +08:00
final tagColorJsonData = jsonEncode ( tagColors ) ;
return {
" tags " : tags ,
" peers " : peersJsonData ,
" tag_colors " : tagColorJsonData
} ;
}
_deserialize ( dynamic data ) {
if ( data = = null ) return ;
final oldOnlineIDs = peers . where ( ( e ) = > e . online ) . map ( ( e ) = > e . id ) . toList ( ) ;
tags . clear ( ) ;
tagColors . clear ( ) ;
peers . clear ( ) ;
if ( data [ ' tags ' ] is List ) {
tags . value = ( data [ ' tags ' ] as List ) . map ( ( e ) = > e . toString ( ) ) . toList ( ) ;
}
if ( data [ ' peers ' ] is List ) {
for ( final peer in data [ ' peers ' ] ) {
peers . add ( Peer . fromJson ( peer ) ) ;
}
}
2024-03-22 11:02:22 +08:00
if ( isFull ( ) ) {
peers . removeRange ( licensedDevices , peers . length ) ;
2024-03-20 15:05:54 +08:00
}
// restore online
peers
. where ( ( e ) = > oldOnlineIDs . contains ( e . id ) )
. map ( ( e ) = > e . online = true )
. toList ( ) ;
if ( data [ ' tag_colors ' ] is String ) {
Map < String , dynamic > map = jsonDecode ( data [ ' tag_colors ' ] ) ;
tagColors . value = Map < String , int > . from ( map ) ;
}
// add color to tag
final tagsWithoutColor =
tags . toList ( ) . where ( ( e ) = > ! tagColors . containsKey ( e ) ) . toList ( ) ;
for ( var t in tagsWithoutColor ) {
tagColors [ t ] = str2color2 ( t , existing: tagColors . values . toList ( ) ) . value ;
}
}
}
class Ab extends BaseAb {
2024-03-22 23:32:59 +08:00
AbProfile profile ;
2024-03-20 15:05:54 +08:00
late final bool personal ;
final sortTags = shouldSortTags ( ) . obs ;
final filterByIntersection = filterAbTagByIntersection ( ) . obs ;
bool get emtpy = > peers . isEmpty & & tags . isEmpty ;
Ab ( this . profile , this . personal ) ;
@ override
String name ( ) {
if ( personal ) {
return _personalAddressBookName ;
} else {
return profile . name ;
}
}
@ override
AbProfile ? sharedProfile ( ) {
return profile ;
}
2024-03-22 23:32:59 +08:00
@ override
void setSharedProfile ( AbProfile profile ) {
this . profile = profile ;
}
2024-03-22 11:02:22 +08:00
@ override
bool isFull ( ) {
return gFFI . abModel . _maxPeerOneAb > 0 & &
peers . length > = gFFI . abModel . _maxPeerOneAb ;
2024-03-20 15:05:54 +08:00
}
@ override
bool canWrite ( ) {
if ( personal ) {
return true ;
} else {
return profile . rule = = ShareRule . readWrite . value | |
profile . rule = = ShareRule . fullControl . value ;
}
}
@ override
bool fullControl ( ) {
if ( personal ) {
return true ;
} else {
return profile . rule = = ShareRule . fullControl . value ;
}
}
@ override
2024-03-25 19:59:21 +08:00
Future < bool > pullAbImpl ( { quiet = false } ) async {
bool ret = true ;
2024-03-20 15:05:54 +08:00
List < Peer > tmpPeers = [ ] ;
2024-03-27 21:28:21 +08:00
if ( ! await _fetchPeers ( tmpPeers , quiet: quiet ) ) {
2024-03-25 19:59:21 +08:00
ret = false ;
}
2024-03-20 15:05:54 +08:00
peers . value = tmpPeers ;
List < AbTag > tmpTags = [ ] ;
2024-03-27 21:28:21 +08:00
if ( ! await _fetchTags ( tmpTags , quiet: quiet ) ) {
2024-03-25 19:59:21 +08:00
ret = false ;
}
2024-03-20 15:05:54 +08:00
tags . value = tmpTags . map ( ( e ) = > e . name ) . toList ( ) ;
Map < String , int > tmpTagColors = { } ;
for ( var t in tmpTags ) {
tmpTagColors [ t . name ] = t . color ;
}
tagColors . value = tmpTagColors ;
2024-03-25 19:59:21 +08:00
return ret ;
2024-03-20 15:05:54 +08:00
}
2024-03-27 21:28:21 +08:00
Future < bool > _fetchPeers ( List < Peer > tmpPeers , { quiet = false } ) async {
2024-03-20 15:05:54 +08:00
final api = " ${ await bind . mainGetApiServer ( ) } /api/ab/peers " ;
2024-03-27 21:28:21 +08:00
int ? statusCode ;
2024-03-20 15:05:54 +08:00
try {
var uri0 = Uri . parse ( api ) ;
final pageSize = 100 ;
var total = 0 ;
int current = 0 ;
do {
current + = 1 ;
var uri = Uri (
scheme: uri0 . scheme ,
host: uri0 . host ,
path: uri0 . path ,
port: uri0 . port ,
queryParameters: {
' current ' : current . toString ( ) ,
' pageSize ' : pageSize . toString ( ) ,
' ab ' : profile . guid ,
} ) ;
var headers = getHttpHeaders ( ) ;
headers [ ' Content-Type ' ] = " application/json " ;
final resp = await http . post ( uri , headers: headers ) ;
2024-03-27 21:28:21 +08:00
statusCode = resp . statusCode ;
2024-03-20 15:05:54 +08:00
Map < String , dynamic > json =
_jsonDecodeRespMap ( utf8 . decode ( resp . bodyBytes ) , resp . statusCode ) ;
if ( json . containsKey ( ' error ' ) ) {
throw json [ ' error ' ] ;
}
if ( resp . statusCode ! = 200 ) {
throw ' HTTP ${ resp . statusCode } ' ;
}
if ( json . containsKey ( ' total ' ) ) {
if ( total = = 0 ) total = json [ ' total ' ] ;
if ( json . containsKey ( ' data ' ) ) {
final data = json [ ' data ' ] ;
if ( data is List ) {
for ( final profile in data ) {
final u = Peer . fromJson ( profile ) ;
int index = tmpPeers . indexWhere ( ( e ) = > e . id = = u . id ) ;
if ( index < 0 ) {
tmpPeers . add ( u ) ;
} else {
tmpPeers [ index ] = u ;
}
}
}
}
}
} while ( current * pageSize < total ) ;
return true ;
} catch ( err ) {
2024-03-27 21:28:21 +08:00
if ( ! quiet ) {
pullError . value =
' ${ translate ( ' pull_ab_failed_tip ' ) } : ${ translate ( err . toString ( ) ) } ' ;
}
} finally {
if ( pullError . isNotEmpty ) {
if ( statusCode = = 401 ) {
gFFI . userModel . reset ( resetOther: true ) ;
}
}
2024-03-20 15:05:54 +08:00
}
return false ;
}
2024-03-27 21:28:21 +08:00
Future < bool > _fetchTags ( List < AbTag > tmpTags , { quiet = false } ) async {
2024-03-20 15:05:54 +08:00
final api = " ${ await bind . mainGetApiServer ( ) } /api/ab/tags/ ${ profile . guid } " ;
2024-03-27 21:28:21 +08:00
int ? statusCode ;
2024-03-20 15:05:54 +08:00
try {
var uri0 = Uri . parse ( api ) ;
var uri = Uri (
scheme: uri0 . scheme ,
host: uri0 . host ,
path: uri0 . path ,
port: uri0 . port ,
) ;
var headers = getHttpHeaders ( ) ;
headers [ ' Content-Type ' ] = " application/json " ;
final resp = await http . post ( uri , headers: headers ) ;
2024-03-27 21:28:21 +08:00
statusCode = resp . statusCode ;
2024-03-20 15:05:54 +08:00
List < dynamic > json =
_jsonDecodeRespList ( utf8 . decode ( resp . bodyBytes ) , resp . statusCode ) ;
if ( resp . statusCode ! = 200 ) {
throw ' HTTP ${ resp . statusCode } ' ;
}
for ( final d in json ) {
final t = AbTag . fromJson ( d ) ;
int index = tmpTags . indexWhere ( ( e ) = > e . name = = t . name ) ;
if ( index < 0 ) {
tmpTags . add ( t ) ;
} else {
tmpTags [ index ] = t ;
}
}
return true ;
} catch ( err ) {
2024-03-27 21:28:21 +08:00
if ( ! quiet ) {
pullError . value =
' ${ translate ( ' pull_ab_failed_tip ' ) } : ${ translate ( err . toString ( ) ) } ' ;
}
} finally {
if ( pullError . isNotEmpty ) {
if ( statusCode = = 401 ) {
gFFI . userModel . reset ( resetOther: true ) ;
}
}
2024-03-20 15:05:54 +08:00
}
return false ;
}
// #region Peers
@ override
Future < String ? > addPeers ( List < Map < String , dynamic > > ps ) async {
try {
final api =
" ${ await bind . mainGetApiServer ( ) } /api/ab/peer/add/ ${ profile . guid } " ;
var headers = getHttpHeaders ( ) ;
headers [ ' Content-Type ' ] = " application/json " ;
for ( var p in ps ) {
if ( peers . firstWhereOrNull ( ( e ) = > e . id = = p [ ' id ' ] ) ! = null ) {
continue ;
}
2024-03-22 11:02:22 +08:00
if ( isFull ( ) ) {
2024-03-20 15:05:54 +08:00
return translate ( " exceed_max_devices " ) ;
}
if ( personal ) {
removePassword ( p ) ;
} else {
removeHash ( p ) ;
}
String body = jsonEncode ( p ) ;
final resp =
await http . post ( Uri . parse ( api ) , headers: headers , body: body ) ;
final errMsg = _jsonDecodeActionResp ( resp ) ;
if ( errMsg . isNotEmpty ) {
return errMsg ;
}
}
} catch ( err ) {
return err . toString ( ) ;
}
return null ;
}
@ override
Future < bool > changeTagForPeers ( List < String > ids , List < dynamic > tags ) async {
try {
final api =
" ${ await bind . mainGetApiServer ( ) } /api/ab/peer/update/ ${ profile . guid } " ;
var headers = getHttpHeaders ( ) ;
headers [ ' Content-Type ' ] = " application/json " ;
var ret = true ;
for ( var id in ids ) {
final body = jsonEncode ( { " id " : id , " tags " : tags } ) ;
final resp =
await http . put ( Uri . parse ( api ) , headers: headers , body: body ) ;
final errMsg = _jsonDecodeActionResp ( resp ) ;
if ( errMsg . isNotEmpty ) {
BotToast . showText ( contentColor: Colors . red , text: errMsg ) ;
ret = false ;
break ;
}
}
return ret ;
} catch ( err ) {
debugPrint ( ' changeTagForPeers err: ${ err . toString ( ) } ' ) ;
return false ;
}
}
@ override
Future < bool > changeAlias ( { required String id , required String alias } ) async {
try {
final api =
" ${ await bind . mainGetApiServer ( ) } /api/ab/peer/update/ ${ profile . guid } " ;
var headers = getHttpHeaders ( ) ;
headers [ ' Content-Type ' ] = " application/json " ;
final body = jsonEncode ( { " id " : id , " alias " : alias } ) ;
final resp = await http . put ( Uri . parse ( api ) , headers: headers , body: body ) ;
final errMsg = _jsonDecodeActionResp ( resp ) ;
if ( errMsg . isNotEmpty ) {
BotToast . showText ( contentColor: Colors . red , text: errMsg ) ;
return false ;
}
return true ;
} catch ( err ) {
debugPrint ( ' changeAlias err: ${ err . toString ( ) } ' ) ;
return false ;
}
}
Future < bool > _setPassword ( Object bodyContent ) async {
try {
final api =
" ${ await bind . mainGetApiServer ( ) } /api/ab/peer/update/ ${ profile . guid } " ;
var headers = getHttpHeaders ( ) ;
headers [ ' Content-Type ' ] = " application/json " ;
final body = jsonEncode ( bodyContent ) ;
final resp = await http . put ( Uri . parse ( api ) , headers: headers , body: body ) ;
final errMsg = _jsonDecodeActionResp ( resp ) ;
if ( errMsg . isNotEmpty ) {
BotToast . showText ( contentColor: Colors . red , text: errMsg ) ;
return false ;
}
return true ;
} catch ( err ) {
debugPrint ( ' changeSharedPassword err: ${ err . toString ( ) } ' ) ;
return false ;
}
}
@ override
Future < bool > changePersonalHashPassword ( String id , String hash ) async {
if ( ! personal ) return false ;
if ( ! peers . any ( ( e ) = > e . id = = id ) ) return false ;
2024-04-02 22:08:47 +08:00
return await _setPassword ( { " id " : id , " hash " : hash } ) ;
2024-03-20 15:05:54 +08:00
}
@ override
Future < bool > changeSharedPassword ( String id , String password ) async {
if ( personal ) return false ;
2024-04-02 22:08:47 +08:00
return await _setPassword ( { " id " : id , " password " : password } ) ;
2024-03-20 15:05:54 +08:00
}
@ override
Future < void > syncFromRecent ( List < Peer > recents ) async {
bool uiUpdate = false ;
2024-04-02 22:08:47 +08:00
bool saveCache = false ;
final api =
" ${ await bind . mainGetApiServer ( ) } /api/ab/peer/update/ ${ profile . guid } " ;
var headers = getHttpHeaders ( ) ;
headers [ ' Content-Type ' ] = " application/json " ;
Future < bool > trySyncOnePeer ( Peer p , Peer r ) async {
var map = Map < String , String > . fromEntries ( [ ] ) ;
if ( p . sameServer ! = true & &
r . username . isNotEmpty & &
p . username ! = r . username ) {
p . username = r . username ;
map [ ' username ' ] = r . username ;
}
if ( p . sameServer ! = true & &
r . hostname . isNotEmpty & &
p . hostname ! = r . hostname ) {
p . hostname = r . hostname ;
map [ ' hostname ' ] = r . hostname ;
}
if ( p . sameServer ! = true & &
r . platform . isNotEmpty & &
p . platform ! = r . platform ) {
p . platform = r . platform ;
map [ ' platform ' ] = r . platform ;
}
if ( personal & & r . hash . isNotEmpty & & p . hash ! = r . hash ) {
p . hash = r . hash ;
map [ ' hash ' ] = r . hash ;
saveCache = true ;
}
if ( map . isEmpty ) {
// no need to sync
return false ;
}
map [ ' id ' ] = p . id ;
final body = jsonEncode ( map ) ;
2024-03-20 15:05:54 +08:00
final resp = await http . put ( Uri . parse ( api ) , headers: headers , body: body ) ;
final errMsg = _jsonDecodeActionResp ( resp ) ;
if ( errMsg . isNotEmpty ) {
debugPrint ( ' syncOnePeer errMsg: $ errMsg ' ) ;
return false ;
}
uiUpdate = true ;
return true ;
}
try {
2024-04-02 22:08:47 +08:00
// Not add new peers because IDs that are not on the server can't be synced, then sync will happen every startup.
for ( var p in peers ) {
2024-03-20 15:05:54 +08:00
Peer ? r = recents . firstWhereOrNull ( ( e ) = > e . id = = p . id ) ;
if ( r ! = null ) {
2024-04-02 22:08:47 +08:00
await trySyncOnePeer ( p , r ) ;
2024-03-20 15:05:54 +08:00
}
}
// Pull cannot be used for sync to avoid cyclic sync.
if ( uiUpdate & & gFFI . abModel . currentName . value = = profile . name ) {
peers . refresh ( ) ;
}
2024-04-02 22:08:47 +08:00
if ( saveCache ) {
gFFI . abModel . _saveCache ( ) ;
}
2024-03-20 15:05:54 +08:00
} catch ( err ) {
debugPrint ( ' syncFromRecent err: ${ err . toString ( ) } ' ) ;
}
}
@ override
Future < bool > deletePeers ( List < String > ids ) async {
try {
final api =
" ${ await bind . mainGetApiServer ( ) } /api/ab/peer/ ${ profile . guid } " ;
var headers = getHttpHeaders ( ) ;
headers [ ' Content-Type ' ] = " application/json " ;
final body = jsonEncode ( ids ) ;
final resp =
await http . delete ( Uri . parse ( api ) , headers: headers , body: body ) ;
final errMsg = _jsonDecodeActionResp ( resp ) ;
if ( errMsg . isNotEmpty ) {
BotToast . showText ( contentColor: Colors . red , text: errMsg ) ;
return false ;
}
return true ;
} catch ( err ) {
debugPrint ( ' deletePeers err: ${ err . toString ( ) } ' ) ;
return false ;
}
}
// #endregion
// #region Tags
@ override
Future < bool > addTags (
List < String > tagList , Map < String , int > tagColorMap ) async {
try {
final api =
" ${ await bind . mainGetApiServer ( ) } /api/ab/tag/add/ ${ profile . guid } " ;
var headers = getHttpHeaders ( ) ;
headers [ ' Content-Type ' ] = " application/json " ;
for ( var t in tagList ) {
final body = jsonEncode ( {
" name " : t ,
" color " : tagColorMap [ t ] ? ?
str2color2 ( t , existing: tagColors . values . toList ( ) ) . value ,
} ) ;
final resp =
await http . post ( Uri . parse ( api ) , headers: headers , body: body ) ;
final errMsg = _jsonDecodeActionResp ( resp ) ;
if ( errMsg . isNotEmpty ) {
BotToast . showText ( contentColor: Colors . red , text: errMsg ) ;
return false ;
}
}
return true ;
} catch ( err ) {
debugPrint ( ' addTags err: ${ err . toString ( ) } ' ) ;
return false ;
}
}
@ override
Future < bool > renameTag ( String oldTag , String newTag ) async {
if ( tags . contains ( newTag ) ) {
BotToast . showText (
contentColor: Colors . red , text: ' Tag $ newTag already exists ' ) ;
return false ;
}
try {
final api =
" ${ await bind . mainGetApiServer ( ) } /api/ab/tag/rename/ ${ profile . guid } " ;
var headers = getHttpHeaders ( ) ;
headers [ ' Content-Type ' ] = " application/json " ;
final body = jsonEncode ( {
" old " : oldTag ,
" new " : newTag ,
} ) ;
final resp = await http . put ( Uri . parse ( api ) , headers: headers , body: body ) ;
final errMsg = _jsonDecodeActionResp ( resp ) ;
if ( errMsg . isNotEmpty ) {
BotToast . showText ( contentColor: Colors . red , text: errMsg ) ;
return false ;
}
return true ;
} catch ( err ) {
debugPrint ( ' renameTag err: ${ err . toString ( ) } ' ) ;
return false ;
}
}
@ override
Future < bool > setTagColor ( String tag , Color color ) async {
try {
final api =
" ${ await bind . mainGetApiServer ( ) } /api/ab/tag/update/ ${ profile . guid } " ;
var headers = getHttpHeaders ( ) ;
headers [ ' Content-Type ' ] = " application/json " ;
final body = jsonEncode ( {
" name " : tag ,
" color " : color . value ,
} ) ;
final resp = await http . put ( Uri . parse ( api ) , headers: headers , body: body ) ;
final errMsg = _jsonDecodeActionResp ( resp ) ;
if ( errMsg . isNotEmpty ) {
BotToast . showText ( contentColor: Colors . red , text: errMsg ) ;
return false ;
}
return true ;
} catch ( err ) {
debugPrint ( ' setTagColor err: ${ err . toString ( ) } ' ) ;
return false ;
}
}
@ override
Future < bool > deleteTag ( String tag ) async {
try {
final api = " ${ await bind . mainGetApiServer ( ) } /api/ab/tag/ ${ profile . guid } " ;
var headers = getHttpHeaders ( ) ;
headers [ ' Content-Type ' ] = " application/json " ;
final body = jsonEncode ( [ tag ] ) ;
final resp =
await http . delete ( Uri . parse ( api ) , headers: headers , body: body ) ;
final errMsg = _jsonDecodeActionResp ( resp ) ;
if ( errMsg . isNotEmpty ) {
BotToast . showText ( contentColor: Colors . red , text: errMsg ) ;
return false ;
}
return true ;
} catch ( err ) {
debugPrint ( ' deleteTag err: ${ err . toString ( ) } ' ) ;
return false ;
}
}
// #endregion
}
// DummyAb is for current ab is null
class DummyAb extends BaseAb {
2024-03-22 11:02:22 +08:00
@ override
bool isFull ( ) {
return false ;
}
2024-03-20 15:05:54 +08:00
@ override
Future < String ? > addPeers ( List < Map < String , dynamic > > ps ) async {
2024-03-25 19:59:21 +08:00
return " dummpy " ;
2024-03-20 15:05:54 +08:00
}
@ override
Future < bool > addTags (
List < String > tagList , Map < String , int > tagColorMap ) async {
return false ;
}
@ override
bool canWrite ( ) {
return false ;
}
@ override
bool fullControl ( ) {
return false ;
}
@ override
Future < bool > changeAlias ( { required String id , required String alias } ) async {
return false ;
}
@ override
Future < bool > changePersonalHashPassword ( String id , String hash ) async {
return false ;
}
@ override
Future < bool > changeSharedPassword ( String id , String password ) async {
return false ;
}
@ override
Future < bool > changeTagForPeers ( List < String > ids , List tags ) async {
return false ;
}
@ override
Future < bool > deletePeers ( List < String > ids ) async {
return false ;
}
@ override
Future < bool > deleteTag ( String tag ) async {
return false ;
}
@ override
String name ( ) {
2024-03-25 19:59:21 +08:00
return " dummpy " ;
2024-03-20 15:05:54 +08:00
}
@ override
2024-03-25 19:59:21 +08:00
Future < bool > pullAbImpl ( { quiet = false } ) async {
return false ;
}
2024-03-20 15:05:54 +08:00
@ override
Future < bool > renameTag ( String oldTag , String newTag ) async {
return false ;
}
@ override
Future < bool > setTagColor ( String tag , Color color ) async {
return false ;
}
@ override
AbProfile ? sharedProfile ( ) {
return null ;
}
2024-03-22 23:32:59 +08:00
@ override
void setSharedProfile ( AbProfile profile ) { }
2024-03-20 15:05:54 +08:00
@ override
Future < void > syncFromRecent ( List < Peer > recents ) async { }
}
Map < String , dynamic > _jsonDecodeRespMap ( String body , int statusCode ) {
try {
Map < String , dynamic > json = jsonDecode ( body ) ;
return json ;
} catch ( e ) {
final err = body . isNotEmpty & & body . length < 128 ? body : e . toString ( ) ;
if ( statusCode ! = 200 ) {
throw ' HTTP $ statusCode , $ err ' ;
}
throw err ;
}
}
List < dynamic > _jsonDecodeRespList ( String body , int statusCode ) {
try {
List < dynamic > json = jsonDecode ( body ) ;
return json ;
} catch ( e ) {
final err = body . isNotEmpty & & body . length < 128 ? body : e . toString ( ) ;
if ( statusCode ! = 200 ) {
throw ' HTTP $ statusCode , $ err ' ;
}
throw err ;
}
}
String _jsonDecodeActionResp ( http . Response resp ) {
var errMsg = ' ' ;
if ( resp . statusCode = = 200 & & resp . body . isEmpty ) {
// ok
} else {
try {
errMsg = jsonDecode ( resp . body ) [ ' error ' ] . toString ( ) ;
} catch ( _ ) { }
if ( errMsg . isEmpty ) {
if ( resp . statusCode ! = 200 ) {
errMsg = ' HTTP ${ resp . statusCode } ' ;
}
if ( resp . body . isNotEmpty ) {
if ( errMsg . isNotEmpty ) {
errMsg + = ' , ' ;
}
errMsg + = resp . body ;
}
if ( errMsg . isEmpty ) {
errMsg = " unknown error " ;
}
2023-09-21 16:34:04 +08:00
}
}
2024-03-20 15:05:54 +08:00
return errMsg ;
2022-07-25 16:26:51 +08:00
}