(
42 | P Function(T) transformation, RxCommand command) {
43 | _worker.isolateResponseCommand
44 | .mergeWith([_backgroundWorker.isolateResponseCommand])
45 | .where((event) => event is T)
46 | .map((event) => event as T)
47 | .map(transformation)
48 | .listen((msg) => command(msg));
49 | }
50 |
51 | _registerWorkerMessage(RxCommand command) {
52 | _registerWorkerMessageWithTransformation((T msg) => msg, command);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/bridges/nextcloud_manager_bridge.dart:
--------------------------------------------------------------------------------
1 | import 'package:rx_command/rx_command.dart';
2 | import 'package:yaga/managers/file_service_manager/isolateable/nextcloud_file_manger.dart';
3 | import 'package:yaga/managers/nextcloud_manager.dart';
4 | import 'package:yaga/model/nc_file.dart';
5 | import 'package:yaga/utils/forground_worker/foreground_worker.dart';
6 | import 'package:yaga/utils/forground_worker/messages/download_preview_complete.dart';
7 | import 'package:yaga/utils/forground_worker/messages/download_preview_request.dart';
8 | import 'package:yaga/utils/forground_worker/messages/login_state_msg.dart';
9 |
10 | class NextcloudManagerBridge {
11 | final NextCloudManager _nextCloudManager;
12 | final ForegroundWorker _worker;
13 | final NextcloudFileManager _nextcloudFileManager;
14 |
15 | RxCommand downloadPreviewCommand =
16 | RxCommand.createSync((param) => param);
17 |
18 | NextcloudManagerBridge(
19 | this._nextCloudManager,
20 | this._worker,
21 | this._nextcloudFileManager,
22 | ) {
23 | //todo: update loginStateCommand has no logout values... see todo in ncManager
24 | _nextCloudManager.updateLoginStateCommand.listen((value) {
25 | _worker.sendRequest(LoginStateMsg("", value));
26 | });
27 |
28 | _worker.isolateResponseCommand
29 | .where((event) => event is DownloadPreviewComplete)
30 | .map((event) => event as DownloadPreviewComplete)
31 | .listen(
32 | (value) => value.success
33 | ? _nextcloudFileManager.updatePreviewCommand(value.file)
34 | : _nextcloudFileManager.downloadPreviewFaildCommand(value.file),
35 | );
36 |
37 | downloadPreviewCommand.listen((ncFile) {
38 | _worker.sendRequest(DownloadPreviewRequest("", ncFile));
39 | });
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/bridges/settings_manager_bridge.dart:
--------------------------------------------------------------------------------
1 | import 'package:rxdart/rxdart.dart';
2 | import 'package:yaga/managers/settings_manager.dart';
3 | import 'package:yaga/utils/forground_worker/foreground_worker.dart';
4 | import 'package:yaga/utils/forground_worker/messages/preference_msg.dart';
5 | import 'package:yaga/utils/logger.dart';
6 |
7 | class SettingsManagerBridge {
8 | final _logger = YagaLogger.getLogger(SettingsManagerBridge);
9 | final SettingsManager _settingsManager;
10 | final ForegroundWorker _worker;
11 |
12 | SettingsManagerBridge(this._settingsManager, this._worker);
13 |
14 | Future init() async {
15 | _settingsManager.updateSettingCommand.doOnData((event) {
16 | _logger.warning(event);
17 | }).listen((event) => _worker.sendRequest(PreferenceMsg("", event)));
18 |
19 | return this;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/handlers/user_handler.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:isolate';
3 |
4 | import 'package:yaga/managers/isolateable/isolated_settings_manager.dart';
5 | import 'package:yaga/services/isolateable/nextcloud_service.dart';
6 | import 'package:yaga/utils/forground_worker/isolate_handler_regestry.dart';
7 | import 'package:yaga/utils/forground_worker/isolate_msg_handler.dart';
8 | import 'package:yaga/utils/forground_worker/messages/init_msg.dart';
9 | import 'package:yaga/utils/forground_worker/messages/login_state_msg.dart';
10 | import 'package:yaga/utils/forground_worker/messages/preference_msg.dart';
11 | import 'package:yaga/utils/self_signed_cert_handler.dart';
12 | import 'package:yaga/utils/service_locator.dart';
13 |
14 | class UserHandler implements IsolateMsgHandler {
15 | @override
16 | Future initIsolated(InitMsg init, SendPort isolateToMain,
17 | IsolateHandlerRegistry registry) async {
18 | registry
19 | .registerHandler((msg) => handleLoginStateChanged(msg));
20 | registry.registerHandler(
21 | (msg) => getIt
22 | .get()
23 | .updateSettingCommand(msg.preference),
24 | );
25 | return this;
26 | }
27 |
28 | void handleLoginStateChanged(LoginStateMsg message) {
29 | final NextCloudService ncService = getIt.get();
30 |
31 | if (message.loginData.server == null) {
32 | getIt.get().revokeCert();
33 | return ncService.logout();
34 | }
35 |
36 | ncService.login(message.loginData);
37 | return;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/isolate_handler_regestry.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/utils/forground_worker/messages/message.dart';
2 | import 'package:yaga/utils/logger.dart';
3 |
4 | class IsolateHandlerRegistry {
5 | final logger = YagaLogger.getLogger(IsolateHandlerRegistry);
6 | final Map> handlers = {};
7 |
8 | void registerHandler(Function(M) handler) {
9 | handlers.putIfAbsent(M, () => []);
10 | handlers[M]!.add((Message msg) => handler(msg as M));
11 | }
12 |
13 | void handleMessage(Message msg) {
14 | if (handlers.containsKey(msg.runtimeType)) {
15 | for (final handler in handlers[msg.runtimeType]!) {
16 | handler(msg);
17 | }
18 | } else {
19 | logger.shout("No handler registered for ${msg.runtimeType}");
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/isolate_msg_handler.dart:
--------------------------------------------------------------------------------
1 | import 'dart:isolate';
2 |
3 | import 'package:yaga/utils/forground_worker/isolate_handler_regestry.dart';
4 | import 'package:yaga/utils/forground_worker/messages/init_msg.dart';
5 |
6 | abstract class IsolateMsgHandler> {
7 | Future initIsolated(
8 | InitMsg init,
9 | SendPort isolateToMain,
10 | IsolateHandlerRegistry registry,
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/isolateable.dart:
--------------------------------------------------------------------------------
1 | import 'dart:isolate';
2 |
3 | import 'package:yaga/utils/forground_worker/messages/init_msg.dart';
4 |
5 | mixin Isolateable> {
6 | Future initIsolated(InitMsg init, SendPort isolateToMain) async => this as T;
7 | }
8 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/download_file_request.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/nc_file.dart';
2 | import 'package:yaga/utils/forground_worker/messages/single_file_message.dart';
3 |
4 | class DownloadFileRequest extends SingleFileMessage {
5 | static const String jsonTypeConst = "DownloadFileRequest";
6 | static const String _jsonForceDownload = "forceDownload";
7 | static const String _jsonPersist = "persist";
8 |
9 | bool forceDownload;
10 | bool persist;
11 |
12 | DownloadFileRequest(
13 | NcFile file, {
14 | this.forceDownload = false,
15 | this.persist = false,
16 | }) : super(jsonTypeConst, jsonTypeConst, file);
17 |
18 | DownloadFileRequest.fromJson(Map json)
19 | : forceDownload = json[_jsonForceDownload] as bool,
20 | persist = json[_jsonPersist] as bool,
21 | super.fromJson(json);
22 |
23 | @override
24 | Map toJson() {
25 | final superMap = super.toJson();
26 | superMap[_jsonForceDownload] = forceDownload;
27 | superMap[_jsonPersist] = persist;
28 | return superMap;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/download_preview_complete.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/nc_file.dart';
2 | import 'package:yaga/utils/forground_worker/messages/message.dart';
3 |
4 | class DownloadPreviewComplete extends Message {
5 | final NcFile file;
6 | final bool success;
7 |
8 | DownloadPreviewComplete(
9 | String key,
10 | this.file, {
11 | this.success = true,
12 | }) : super(key);
13 | }
14 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/download_preview_request.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/nc_file.dart';
2 | import 'package:yaga/utils/forground_worker/messages/message.dart';
3 |
4 | class DownloadPreviewRequest extends Message {
5 | final NcFile file;
6 |
7 | DownloadPreviewRequest(String key, this.file) : super(key);
8 | }
9 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/file_list_done.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/utils/forground_worker/messages/file_list_message.dart';
2 |
3 | class FileListDone extends FileListMessage {
4 | FileListDone(String key, Uri uri, {bool recursive = false})
5 | : super(key, uri, recursive: recursive);
6 | }
7 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/file_list_message.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/utils/forground_worker/messages/message.dart';
2 |
3 | abstract class FileListMessage extends Message {
4 | final Uri uri;
5 | final bool recursive;
6 | FileListMessage(String key, this.uri, {this.recursive = false}) : super(key);
7 | }
8 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/file_list_request.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/sort_config.dart';
2 | import 'package:yaga/utils/forground_worker/messages/message.dart';
3 |
4 | class FileListRequest extends Message {
5 | final Uri uri;
6 | final bool recursive;
7 | final bool favorites;
8 | final SortConfig config;
9 |
10 | FileListRequest(
11 | String key,
12 | this.uri,
13 | this.config, {
14 | this.recursive = false,
15 | this.favorites = false,
16 | }) : super(key);
17 | }
18 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/file_list_response.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/sorted_file_list.dart';
2 | import 'package:yaga/utils/forground_worker/messages/file_list_message.dart';
3 |
4 | class FileListResponse extends FileListMessage {
5 | final SortedFileList files;
6 |
7 | FileListResponse(String key, Uri uri, this.files, {bool recursive = false})
8 | : super(key, uri, recursive: recursive);
9 | }
10 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/file_update_msg.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/nc_file.dart';
2 | import 'package:yaga/utils/forground_worker/messages/single_file_message.dart';
3 |
4 | class FileUpdateMsg extends SingleFileMessage {
5 | static const String jsonTypeConst = "FileUpdateMsg";
6 |
7 | FileUpdateMsg(String key, NcFile file) : super(key, jsonTypeConst, file);
8 | FileUpdateMsg.fromJson(Map json) : super.fromJson(json);
9 | }
10 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/files_action/delete_files_request.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/nc_file.dart';
2 | import 'package:yaga/utils/forground_worker/messages/files_action/files_action_request.dart';
3 |
4 | class DeleteFilesRequest extends FilesActionRequest {
5 | static const String jsonTypeConst = "DeleteFilesRequest";
6 | static const String _jsonLocal = "local";
7 | final bool local;
8 |
9 | DeleteFilesRequest({required super.key, required super.files, required super.sourceDir, required this.local})
10 | : super(jsonType: jsonTypeConst);
11 |
12 | DeleteFilesRequest.fromJson(super.json)
13 | : local = json[_jsonLocal] as bool,
14 | super.fromJson();
15 |
16 | @override
17 | Map toJson() {
18 | return super.toJson()
19 | ..[_jsonLocal] = local;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/files_action/destination_action_files_request.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/nc_file.dart';
2 | import 'package:yaga/utils/forground_worker/messages/files_action/files_action_request.dart';
3 |
4 | enum DestinationAction { copy, move }
5 |
6 | class DestinationActionFilesRequest extends FilesActionRequest {
7 | static const String jsonTypeConst = "DestinationActionFilesRequest";
8 | static const String _jsonDestination = "destination";
9 | static const String _jsonAction = "action";
10 | static const String _jsonOverwrite = "overwrite";
11 |
12 | final Uri destination;
13 | final DestinationAction action;
14 | final bool overwrite;
15 |
16 | DestinationActionFilesRequest({
17 | required super.key,
18 | required super.files,
19 | required this.destination,
20 | required super.sourceDir,
21 | this.action = DestinationAction.copy,
22 | this.overwrite = false,
23 | }) : super(jsonType: jsonTypeConst);
24 |
25 | DestinationActionFilesRequest.fromJson(super.json)
26 | : destination = Uri.parse(json[_jsonDestination] as String),
27 | action = DestinationAction.values.firstWhere((element) => (json[_jsonAction] as String) == element.name),
28 | overwrite = json[_jsonOverwrite] as bool,
29 | super.fromJson();
30 |
31 | @override
32 | Map toJson() {
33 | final map = super.toJson();
34 | map[_jsonDestination] = destination.toString();
35 | map[_jsonAction] = action.name;
36 | map[_jsonOverwrite] = overwrite;
37 | return map;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/files_action/favorite_files_request.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/nc_file.dart';
2 | import 'package:yaga/utils/forground_worker/messages/files_action/files_action_request.dart';
3 |
4 | class FavoriteFilesRequest extends FilesActionRequest {
5 | static const String jsonTypeConst = "FavoriteFilesRequest";
6 |
7 | FavoriteFilesRequest({required super.key, required super.files, required super.sourceDir})
8 | : super(jsonType: jsonTypeConst);
9 |
10 | FavoriteFilesRequest.fromJson(super.json) : super.fromJson();
11 | }
12 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/files_action/files_action_done.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/utils/background_worker/json_convertable.dart';
2 |
3 | class FilesActionDone extends JsonConvertable {
4 | static const String jsonTypeConst = "FilesActionDone";
5 | static const String _jsonDestination = "destination";
6 | final Uri destination;
7 |
8 | //todo: background: not sure adding destination was the best solution; it only matters for actionDone follow up
9 | FilesActionDone(String key, this.destination) : super(key, jsonTypeConst);
10 |
11 | FilesActionDone.fromJson(Map json)
12 | : destination = Uri.parse(json[_jsonDestination] as String),
13 | super.fromJson(json);
14 |
15 | @override
16 | Map toJson() {
17 | return super.toJson()..[_jsonDestination] = destination.toString();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/files_action/files_action_request.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/nc_file.dart';
2 | import 'package:yaga/utils/background_worker/json_convertable.dart';
3 |
4 | abstract class FilesActionRequest extends JsonConvertable {
5 | static const String _jsonSourceDir = "sourceDir";
6 | final Uri sourceDir;
7 | static const String _jsonFiles = "files";
8 | final List files;
9 |
10 | FilesActionRequest({
11 | required String key,
12 | required String jsonType,
13 | required this.sourceDir,
14 | required this.files,
15 | }) : super(key, jsonType);
16 |
17 | FilesActionRequest.fromJson(super.json)
18 | : sourceDir = Uri.parse(json[_jsonSourceDir] as String),
19 | files = (json[_jsonFiles] as List).map((e) => NcFile.fromJson(e as Map)).toList(),
20 | super.fromJson();
21 |
22 | @override
23 | Map toJson() {
24 | final map = super.toJson();
25 | map[_jsonSourceDir] = sourceDir.toString();
26 | map[_jsonFiles] = files.map((e) => e.toJson()).toList();
27 | return map;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/flush_logs_message.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/utils/forground_worker/messages/message.dart';
2 |
3 | class FlushLogsMessage extends Message {
4 | final bool flushed;
5 | FlushLogsMessage({this.flushed = false}) : super("flush-log");
6 | }
7 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/image_update_msg.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/nc_file.dart';
2 | import 'package:yaga/utils/forground_worker/messages/single_file_message.dart';
3 |
4 | class ImageUpdateMsg extends SingleFileMessage {
5 | static const String jsonTypeConst = "ImageUpdateMsg";
6 |
7 | ImageUpdateMsg(String key, NcFile file) : super(key, jsonTypeConst, file);
8 | ImageUpdateMsg.fromJson(Map json) : super.fromJson(json);
9 | }
10 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/init_msg.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 | import 'dart:isolate';
3 |
4 | import 'package:yaga/model/nc_login_data.dart';
5 | import 'package:yaga/model/preferences/bool_preference.dart';
6 | import 'package:yaga/model/preferences/mapping_preference.dart';
7 | import 'package:yaga/utils/background_worker/messages/background_init_msg.dart';
8 |
9 | class InitMsg extends BackgroundInitMsg {
10 | final SendPort sendPort;
11 | final Directory externalPath;
12 | final Directory tmpPath;
13 | final List externalPaths;
14 | final MappingPreference? mapping;
15 | final BoolPreference autoPersist;
16 |
17 | InitMsg(
18 | this.sendPort,
19 | this.externalPath,
20 | this.tmpPath,
21 | this.externalPaths,
22 | NextCloudLoginData lastLoginData,
23 | this.mapping,
24 | String fingerprint,
25 | this.autoPersist,
26 | ): super(lastLoginData, fingerprint);
27 | }
28 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/login_state_msg.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/nc_login_data.dart';
2 | import 'package:yaga/utils/forground_worker/messages/message.dart';
3 |
4 | class LoginStateMsg extends Message {
5 | final NextCloudLoginData loginData;
6 |
7 | LoginStateMsg(String key, this.loginData) : super(key);
8 | }
9 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/merge_sort_done.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/sorted_file_list.dart';
2 | import 'package:yaga/utils/forground_worker/messages/message.dart';
3 |
4 | class MergeSortDone extends Message {
5 | final SortedFileList sorted;
6 | final bool updateLoading;
7 |
8 | MergeSortDone(
9 | String key,
10 | this.sorted, {
11 | this.updateLoading = false,
12 | }) : super(key);
13 | }
14 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/merge_sort_request.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/sorted_file_list.dart';
2 | import 'package:yaga/utils/forground_worker/messages/message.dart';
3 |
4 | class MergeSortRequest extends Message {
5 | final SortedFileList main;
6 | final SortedFileList addition;
7 | final Uri? uri;
8 | final bool recursive;
9 | final bool updateLoading;
10 |
11 | MergeSortRequest(
12 | String key,
13 | this.main,
14 | this.addition, {
15 | this.uri,
16 | this.recursive = false,
17 | this.updateLoading = false,
18 | }) : super(key);
19 | }
20 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/message.dart:
--------------------------------------------------------------------------------
1 | abstract class Message {
2 | final String key;
3 |
4 | Message(this.key);
5 | }
6 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/preference_msg.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/preferences/preference.dart';
2 | import 'package:yaga/utils/forground_worker/messages/message.dart';
3 |
4 | class PreferenceMsg extends Message {
5 | final Preference preference;
6 |
7 | PreferenceMsg(String key, this.preference) : super(key);
8 | }
9 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/single_file_message.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/nc_file.dart';
2 | import 'package:yaga/utils/background_worker/json_convertable.dart';
3 |
4 | abstract class SingleFileMessage extends JsonConvertable {
5 | static const String _jsonFile = "file";
6 |
7 | final NcFile file;
8 |
9 | SingleFileMessage(String key, String type, this.file) : super(key, type);
10 |
11 | SingleFileMessage.fromJson(Map json)
12 | : file = NcFile.fromJson(json[_jsonFile] as Map),
13 | super.fromJson(json);
14 |
15 | @override
16 | Map toJson() {
17 | final superMap = super.toJson();
18 | superMap[_jsonFile] = file.toJson();
19 | return superMap;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/lib/utils/forground_worker/messages/sort_request.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/nc_file.dart';
2 | import 'package:yaga/utils/forground_worker/messages/file_list_request.dart';
3 | import 'package:yaga/utils/forground_worker/messages/message.dart';
4 |
5 | class SortRequest extends Message {
6 | final List files;
7 | final FileListRequest fileListRequest;
8 |
9 | SortRequest(String key, this.files, this.fileListRequest) : super(key);
10 | }
11 |
--------------------------------------------------------------------------------
/lib/utils/navigation/yaga_route_information_parser.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class YagaRouteInformationParser extends RouteInformationParser {
4 | @override
5 | Future parseRouteInformation(RouteInformation routeInformation) async {
6 | return Uri.parse(routeInformation.location??'');
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/lib/utils/navigation/yaga_router_delegate.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:yaga/managers/navigation_manager.dart';
3 | import 'package:yaga/managers/tab_manager.dart';
4 | import 'package:yaga/model/route_args/directory_navigation_screen_arguments.dart';
5 | import 'package:yaga/services/intent_service.dart';
6 | import 'package:yaga/utils/service_locator.dart';
7 | import 'package:yaga/utils/navigation/yaga_router.dart';
8 | import 'package:yaga/views/screens/directory_traversal_screen.dart';
9 | import 'package:yaga/views/screens/yaga_home_screen.dart';
10 |
11 | class YagaRouterDelegate extends RouterDelegate
12 | with ChangeNotifier, PopNavigatorRouterDelegateMixin {
13 | @override
14 | final GlobalKey navigatorKey = GlobalKey();
15 | final NavigationManager _navigationManager = getIt.get();
16 | IntentService intentService = getIt.get();
17 |
18 | YagaRouterDelegate() {
19 | _navigationManager.showDirectoryNavigation.listen((value) {
20 | notifyListeners();
21 | });
22 |
23 | getIt
24 | .get()
25 | .tabChangedCommand
26 | .listen((value) => _navigationManager.showDirectoryNavigation(null));
27 | }
28 |
29 | @override
30 | Widget build(BuildContext context) {
31 | return Navigator(
32 | key: navigatorKey,
33 | onGenerateRoute: generateRoute,
34 | pages: [getInitialPage(), ..._buildDirectoryNavigationPage()],
35 | onPopPage: (route, result) {
36 | if (!route.didPop(result)) {
37 | return false;
38 | }
39 |
40 | _navigationManager.showDirectoryNavigation(null);
41 |
42 | return true;
43 | },
44 | );
45 | }
46 |
47 | @override
48 | Future setNewRoutePath(Uri configuration) async {
49 | //todo: we are not yet handling roots from the system
50 | }
51 |
52 | List _buildDirectoryNavigationPage() {
53 | final DirectoryNavigationScreenArguments? args =
54 | _navigationManager.showDirectoryNavigation.lastResult;
55 |
56 | if (args == null) {
57 | return [];
58 | }
59 |
60 | return [
61 | MaterialPage(
62 | key: ValueKey(args.uri.toString()),
63 | child: DirectoryTraversalScreen(args),
64 | )
65 | ];
66 | }
67 |
68 | Page getInitialPage() {
69 | return MaterialPage(
70 | key: const ValueKey(YagaHomeScreen.route),
71 | child: YagaHomeScreen(),
72 | );
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/lib/utils/ncfile_stream_extensions.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/nc_file.dart';
2 | import 'package:rxdart/rxdart.dart';
3 |
4 | extension NcFileStreamExtensions on Stream {
5 | Stream> collectToList() {
6 | return toList().asStream().onErrorReturn([]);
7 | }
8 |
9 | Stream recursively(Stream Function(Uri, {bool favorites}) listFilesFromUpstream,
10 | {required bool recursive, bool favorites = false}) {
11 | return flatMap(
12 | (file) => Rx.merge([
13 | Stream.value(file),
14 | Stream.value(file)
15 | .where((file) => file.isDirectory)
16 | .where((_) => recursive)
17 | .flatMap(
18 | (file) => listFilesFromUpstream(
19 | file.uri,
20 | favorites: favorites,
21 | ).recursively(
22 | listFilesFromUpstream,
23 | recursive: recursive,
24 | favorites: favorites,
25 | ),
26 | )
27 | ]),
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/utils/nextcloud_client_factory.dart:
--------------------------------------------------------------------------------
1 | import 'package:nextcloud/nextcloud.dart';
2 | import 'package:yaga/utils/self_signed_cert_handler.dart';
3 |
4 | class NextCloudClientFactory {
5 | // we do not actually need the SelfSignedCertHandler,
6 | // however we have to make sure it was initialized
7 | // before allowing anyone to create Nextcloud clients
8 | // ignore: avoid_unused_constructor_parameters
9 | NextCloudClientFactory(SelfSignedCertHandler handler);
10 |
11 | //todo: is this really the right place for this?
12 | String get userAgent => "Nextcloud Yaga";
13 |
14 | NextcloudClient createNextCloudClient(
15 | Uri host,
16 | String username,
17 | String password,
18 | ) =>
19 | NextcloudClient(
20 | host,
21 | loginName: username,
22 | password: password,
23 | userAgentOverride: userAgent,
24 |
25 | );
26 |
27 | NextcloudClient createUnauthenticatedClient(Uri host) =>
28 | NextcloudClient(
29 | host,
30 | userAgentOverride: userAgent,
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/lib/utils/nextcloud_colors.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/rendering.dart';
2 |
3 | class NextcloudColors {
4 | NextcloudColors._();
5 |
6 | static const Color darkBlue = Color(0xFF0082C9);
7 |
8 | static const Color lightBlue = Color(0xFF1CAFFF);
9 | }
10 |
--------------------------------------------------------------------------------
/lib/utils/uri_utils.dart:
--------------------------------------------------------------------------------
1 | import 'package:validators/sanitizers.dart';
2 | import 'package:yaga/model/nc_file.dart';
3 |
4 | //todo: refactor into non static functions in util class UriUtils
5 |
6 | Uri fromUri({
7 | required Uri uri,
8 | String? scheme,
9 | String? userInfo,
10 | String? host,
11 | int? port,
12 | String? path,
13 | }) =>
14 | Uri(
15 | scheme: scheme ?? uri.scheme,
16 | userInfo: userInfo ?? uri.userInfo,
17 | host: host ?? uri.host,
18 | port: port ?? uri.port,
19 | path: path ?? uri.path,
20 | );
21 |
22 | Uri fromPathList({required Uri uri, required List paths}) {
23 | String path = "";
24 | for (final element in paths) {
25 | path = chainPathSegments(path, element);
26 | }
27 | // do not double encode here because paths are already double encoded
28 | return fromUri(uri: uri, path: path);
29 | }
30 |
31 | bool compareFilePathToTargetFilePath(NcFile file, Uri destination) {
32 | return file.uri.path ==
33 | chainPathSegments(
34 | destination.path,
35 | Uri.encodeComponent(file.name),
36 | );
37 | }
38 |
39 | String chainPathSegments(String first, String second) {
40 | String firstNormalized = first;
41 | if (first.endsWith("/")) {
42 | firstNormalized = rtrim(first, "/");
43 | }
44 |
45 | String secondNormalized = second;
46 | if (second.startsWith("/")) {
47 | secondNormalized = ltrim(second, "/");
48 | }
49 |
50 | return "$firstNormalized/$secondNormalized";
51 | }
52 |
53 | Uri getRootFromUri(Uri uri) => fromUri(uri: uri, path: "/");
54 |
55 | Uri fromUriPathSegments(Uri uri, int index) {
56 | final buffer = StringBuffer();
57 | buffer.write("/");
58 | for (int i = 0; i <= index; i++) {
59 | // in cases where we have encoded chars in the folder name we have to re-encode
60 | // to make sure we do not change the meaning, since pathSegments does auto-decoding
61 | buffer.write("${Uri.encodeComponent(uri.pathSegments[i])}/");
62 | }
63 |
64 | return fromUri(uri: uri, path: buffer.toString());
65 | }
66 |
67 | String getNameFromUri(Uri uri) {
68 | if (uri.pathSegments.isEmpty) {
69 | return uri.host;
70 | }
71 |
72 | //resolving any encoded chars in the name of a file/folder to improve readability should be avoided
73 | //this would not correspond anymore to the displayed name in nextcloud
74 | //furthermore you get problems when trying to double decode DE chars
75 | if (uri.pathSegments.last.isNotEmpty) {
76 | return uri.pathSegments.last;
77 | }
78 | return uri.pathSegments[uri.pathSegments.length - 2];
79 | }
80 |
--------------------------------------------------------------------------------
/lib/views/screens/choice_selector_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:yaga/model/preferences/choice_preference.dart';
3 | import 'package:yaga/views/widgets/select_cancel_bottom_navigation.dart';
4 |
5 | class ChoiceSelectorScreen extends StatefulWidget {
6 | static const String route = "/choiceSelectorScreen";
7 |
8 | final ChoicePreference _choicePreference;
9 | final void Function() _onCancel;
10 | final void Function(String) _onSelect;
11 |
12 | const ChoiceSelectorScreen(
13 | this._choicePreference, this._onSelect, this._onCancel);
14 |
15 | @override
16 | _ChoiceSelectorScreenState createState() => _ChoiceSelectorScreenState();
17 | }
18 |
19 | class _ChoiceSelectorScreenState extends State {
20 | late String _choice;
21 |
22 | @override
23 | void initState() {
24 | super.initState();
25 | _choice = widget._choicePreference.value;
26 | }
27 |
28 | @override
29 | Widget build(BuildContext context) {
30 | return Scaffold(
31 | appBar: AppBar(
32 | title: Text(widget._choicePreference.title!),
33 | ),
34 | body: ListView.separated(
35 | itemBuilder: (context, index) => RadioListTile(
36 | title: Text(widget._choicePreference.choices[
37 | widget._choicePreference.choices.keys.elementAt(index)]!),
38 | value: widget._choicePreference.choices.keys.elementAt(index),
39 | groupValue: _choice,
40 | onChanged: (String? value) =>
41 | setState(() => _choice = value ?? _choice)),
42 | separatorBuilder: (context, index) => const Divider(),
43 | itemCount: widget._choicePreference.choices.length),
44 | bottomNavigationBar: SelectCancelBottomNavigation(
45 | onCommit: () {
46 | widget._onSelect(_choice);
47 | },
48 | onCancel: widget._onCancel),
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/lib/views/screens/favorites_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/views/screens/browse_view.dart';
2 |
3 | class FavoritesView extends BrowseView {
4 | @override
5 | String get pref => "favorites";
6 |
7 | const FavoritesView() : super(favorites: true);
8 | }
9 |
--------------------------------------------------------------------------------
/lib/views/screens/focus_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/category_view_config.dart';
2 | import 'package:yaga/utils/uri_utils.dart';
3 | import 'package:yaga/views/screens/yaga_home_screen.dart';
4 | import 'package:yaga/views/screens/category_view_screen.dart';
5 |
6 | class FocusView extends CategoryViewScreen {
7 | static const String route = "/focus";
8 |
9 | FocusView(Uri path, bool favorites, YagaHomeTab selectedTab, String prefPrefix)
10 | : super(
11 | CategoryViewConfig(
12 | defaultPath: path,
13 | pref: "$prefPrefix/focus",
14 | pathEnabled: false,
15 | hasDrawer: false,
16 | selectedTab: selectedTab,
17 | title: getNameFromUri(path),
18 | favorites: favorites,
19 | ),
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/lib/views/screens/home_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:yaga/model/category_view_config.dart';
2 | import 'package:yaga/services/isolateable/system_location_service.dart';
3 | import 'package:yaga/services/intent_service.dart';
4 | import 'package:yaga/utils/service_locator.dart';
5 | import 'package:yaga/views/screens/yaga_home_screen.dart';
6 | import 'package:yaga/views/screens/category_view_screen.dart';
7 |
8 | class HomeView extends CategoryViewScreen {
9 | static const String pref = "category";
10 |
11 | HomeView()
12 | : super(
13 | CategoryViewConfig(
14 | defaultPath:
15 | getIt.get().internalStorage.uri,
16 | pref: pref,
17 | pathEnabled: true,
18 | hasDrawer: true,
19 | selectedTab: YagaHomeTab.grid,
20 | title: _getTitle()),
21 | );
22 |
23 | //todo: unify this
24 | static String _getTitle() {
25 | if (getIt.get().isOpenForSelect) {
26 | return "Selecte image...";
27 | }
28 |
29 | return "Nextcloud Yaga";
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/lib/views/screens/nc_login_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:webview_flutter/webview_flutter.dart';
3 | import 'package:yaga/managers/nextcloud_manager.dart';
4 | import 'package:yaga/model/nc_login_data.dart';
5 | import 'package:yaga/utils/nextcloud_client_factory.dart';
6 | import 'package:yaga/utils/service_locator.dart';
7 | import 'package:yaga/views/screens/yaga_home_screen.dart';
8 |
9 | class NextCloudLoginScreen extends StatelessWidget {
10 | static const String route = "/nc/login";
11 | final Uri _url;
12 |
13 | const NextCloudLoginScreen(this._url);
14 |
15 | @override
16 | Widget build(BuildContext context) {
17 | final WebViewController controller = WebViewController()
18 | ..setUserAgent(getIt.get().userAgent)
19 | ..setJavaScriptMode(JavaScriptMode.unrestricted)
20 | ..setNavigationDelegate(
21 | NavigationDelegate(
22 | onNavigationRequest: (NavigationRequest request) async {
23 | if (request.url.startsWith("nc")) {
24 | //string of type: nc://login/server:&user:&password:
25 | final Map ncParas = request.url
26 | .split("nc://login/")[1]
27 | .split("&")
28 | .map((e) => e.split(":"))
29 | .map((e) => {e.removeAt(0): e.join(":")})
30 | .reduce((value, element) {
31 | value.addAll(element);
32 | return value;
33 | });
34 |
35 | getIt.get().loginCommand(NextCloudLoginData(
36 | Uri.parse(ncParas[NextCloudLoginDataKeys.server]!),
37 | Uri.decodeComponent(ncParas[NextCloudLoginDataKeys.user]!),
38 | ncParas[NextCloudLoginDataKeys.password]!,
39 | ));
40 |
41 | Navigator.popUntil(
42 | context, ModalRoute.withName(YagaHomeScreen.route));
43 | return NavigationDecision.prevent;
44 | }
45 |
46 | return NavigationDecision.navigate;
47 | },
48 | ),
49 | )
50 | ..loadRequest(
51 | Uri.parse("$_url/index.php/login/flow"),
52 | headers: {"OCS-APIREQUEST": "true"},
53 | );
54 |
55 | return Scaffold(
56 | appBar: AppBar(
57 | title: const Text("Sign in..."),
58 | ),
59 | body: WebViewWidget(
60 | controller: controller,
61 | ),
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/lib/views/screens/path_selector_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:yaga/model/nc_file.dart';
3 | import 'package:yaga/model/route_args/directory_navigation_screen_arguments.dart';
4 | import 'package:yaga/views/screens/directory_traversal_screen.dart';
5 | import 'package:yaga/views/widgets/image_views/nc_list_view.dart';
6 | import 'package:yaga/views/widgets/image_views/utils/view_configuration.dart';
7 | import 'package:yaga/views/widgets/select_cancel_bottom_navigation.dart';
8 |
9 | //todo: is it a good idea to merge PathSelectorScreen and DirectoryTraversalScreen?
10 | class PathSelectorScreen extends StatelessWidget {
11 | static const String route = "/pathSelector";
12 |
13 | final Uri _uri;
14 | final void Function(Uri)? _onSelect;
15 | final void Function(List, int)? onFileTap;
16 | final String? title;
17 | final bool fixedOrigin;
18 | final String schemeFilter;
19 |
20 | const PathSelectorScreen(
21 | this._uri,
22 | this._onSelect, {
23 | this.onFileTap,
24 | this.title,
25 | this.fixedOrigin = false,
26 | this.schemeFilter = "",
27 | });
28 |
29 | @override
30 | Widget build(BuildContext context) {
31 | return DirectoryTraversalScreen(_getArgs(context));
32 | }
33 |
34 | DirectoryNavigationScreenArguments _getArgs(BuildContext context) {
35 | Widget Function(BuildContext, Uri)? bottomBarBuilder;
36 |
37 | //todo: can't we simply build the bottomBar every time in this screen?
38 | if (_onSelect != null) {
39 | bottomBarBuilder =
40 | (BuildContext context, Uri uri) => SelectCancelBottomNavigation(
41 | onCommit: () {
42 | Navigator.of(context)
43 | .pop(DirectoryTraversalScreenNavActions.cancel);
44 | _onSelect!(uri);
45 | },
46 | onCancel: () => Navigator.of(context)
47 | .pop(DirectoryTraversalScreenNavActions.cancel),
48 | );
49 | }
50 |
51 | final ViewConfiguration viewConfig = ViewConfiguration.browse(
52 | route: route,
53 | defaultView: NcListView.viewKey,
54 | onFolderTap: null,
55 | onFileTap: onFileTap,
56 | onSelect: null,
57 | );
58 |
59 | return DirectoryNavigationScreenArguments(
60 | uri: _uri,
61 | title: title ?? "Select path...",
62 | viewConfig: viewConfig,
63 | fixedOrigin: fixedOrigin,
64 | schemeFilter: schemeFilter,
65 | bottomBarBuilder: bottomBarBuilder);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/lib/views/screens/splash_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_svg/flutter_svg.dart';
3 | import 'package:yaga/utils/nextcloud_colors.dart';
4 |
5 | class SplashScreen extends StatelessWidget {
6 | @override
7 | Widget build(BuildContext context) {
8 | final scaffold = Scaffold(
9 | backgroundColor: Colors.transparent,
10 | body: Row(
11 | mainAxisAlignment: MainAxisAlignment.center,
12 | // mainAxisSize: MainAxisSize.max,
13 | children: [
14 | Align(
15 | // alignment: Alignment.center,
16 | child: SvgPicture.asset(
17 | "assets/icon/foreground.svg",
18 | semanticsLabel: 'Yaga Logo',
19 | // alignment: Alignment.center,
20 | width: 108,
21 | ),
22 | ),
23 | ],
24 | ),
25 | );
26 |
27 | return Container(
28 | decoration: const BoxDecoration(
29 | gradient: LinearGradient(
30 | begin: Alignment.topRight,
31 | end: Alignment.bottomLeft,
32 | colors: [
33 | NextcloudColors.lightBlue,
34 | NextcloudColors.darkBlue,
35 | ],
36 | ),
37 | ),
38 | child: scaffold,
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/lib/views/widgets/action_danger_dialog.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class ActionDangerDialog extends StatelessWidget {
4 | final String title;
5 | final String cancelButton;
6 | final String? normalAction;
7 | final String aggressiveAction;
8 | final Function(bool) action;
9 | final List Function(BuildContext) bodyBuilder;
10 |
11 | const ActionDangerDialog({
12 | required this.title,
13 | required this.cancelButton,
14 | this.normalAction,
15 | required this.aggressiveAction,
16 | required this.action,
17 | required this.bodyBuilder,
18 | });
19 |
20 | @override
21 | Widget build(BuildContext context) {
22 | final actions = [];
23 |
24 | actions.add(
25 | TextButton(
26 | onPressed: () {
27 | Navigator.pop(context);
28 | },
29 | child: Text(cancelButton),
30 | ),
31 | );
32 |
33 | if (normalAction != null) {
34 | actions.add(
35 | TextButton(
36 | onPressed: () {
37 | Navigator.pop(context);
38 | action(false);
39 | },
40 | child: Text(normalAction!),
41 | ),
42 | );
43 | }
44 |
45 | actions.add(
46 | TextButton(
47 | style: TextButton.styleFrom(primary: Colors.red),
48 | onPressed: () {
49 | Navigator.pop(context);
50 | action(true);
51 | },
52 | child: Text(aggressiveAction),
53 | ),
54 | );
55 |
56 | return AlertDialog(
57 | title: Text(title),
58 | content: SingleChildScrollView(
59 | child: ListBody(
60 | children: bodyBuilder(context),
61 | ),
62 | ),
63 | actions: actions,
64 | );
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/lib/views/widgets/address_form_advanced.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:validators/sanitizers.dart';
3 |
4 | class AddressFormAdvanced extends StatelessWidget {
5 | final GlobalKey _formKey;
6 | final Function(Uri) _onSave;
7 |
8 | const AddressFormAdvanced(this._formKey, this._onSave);
9 |
10 | @override
11 | Widget build(BuildContext context) {
12 | return Form(
13 | key: _formKey,
14 | autovalidateMode: AutovalidateMode.onUserInteraction,
15 | child: TextFormField(
16 | decoration: const InputDecoration(
17 | labelText: "Fully qualified Nextcloud Server address...",
18 | icon: Icon(Icons.cloud_queue),
19 | helperStyle: TextStyle(color: Colors.orange),
20 | helperText: "Validation is disabled."),
21 | onSaved: (value) => _onSave(
22 | Uri.parse(rtrim(value?.trim()??'', "/")),
23 | ),
24 | initialValue: "https://",
25 | ),
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/lib/views/widgets/address_form_simple.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:validators/sanitizers.dart';
3 | import 'package:validators/validators.dart';
4 |
5 | class AddressFormSimple extends StatelessWidget {
6 | final GlobalKey _formKey;
7 | final Function(Uri) _onSave;
8 |
9 | const AddressFormSimple(this._formKey, this._onSave);
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | return Form(
14 | key: _formKey,
15 | autovalidateMode: AutovalidateMode.onUserInteraction,
16 | child: TextFormField(
17 | decoration: const InputDecoration(
18 | labelText: "Nextcloud Server address https://...",
19 | icon: Icon(Icons.cloud_queue)),
20 | onSaved: (value) => _onSave(
21 | Uri.parse('https://${rtrim(value?.trim()??"", "/")}'),
22 | ),
23 | validator: (value) {
24 | value = value?.trim()??"";
25 | if (value.startsWith("https://") || value.startsWith("http://")) {
26 | return "Https will be added automaically.";
27 | }
28 | return isURL("https://$value") ? null : "Please enter a valid URL.";
29 | },
30 | ),
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/views/widgets/avatar_widget.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:rx_command/rx_command.dart';
5 |
6 | class AvatarWidget extends StatelessWidget {
7 | final File? _avatar;
8 | final RxCommand? _command;
9 | final IconData? _iconData;
10 | final double _radius;
11 | final bool border;
12 |
13 | const AvatarWidget(this._avatar, {double radius = 14, this.border = true})
14 | : _command = null,
15 | _iconData = null,
16 | _radius = radius;
17 | const AvatarWidget.command(this._command,
18 | {double radius = 14, this.border = true})
19 | : _avatar = null,
20 | _iconData = null,
21 | _radius = radius;
22 | const AvatarWidget.icon(this._iconData,
23 | {double radius = 14, this.border = true})
24 | : _avatar = null,
25 | _command = null,
26 | _radius = radius;
27 | const AvatarWidget.phone({double radius = 14, this.border = true})
28 | : _avatar = null,
29 | _iconData = Icons.phone_android,
30 | _command = null,
31 | _radius = radius;
32 | const AvatarWidget.sd({double radius = 14, this.border = true})
33 | : _avatar = null,
34 | _iconData = Icons.sd_card_outlined,
35 | _command = null,
36 | _radius = radius;
37 |
38 | Widget _buildAvatar(BuildContext context, File? data) {
39 | if (border) {
40 | return CircleAvatar(
41 | radius: _radius + 1,
42 | backgroundColor: Theme.of(context).primaryColor,
43 | child: _getInnerAvatar(context, data),
44 | );
45 | }
46 |
47 | return _getInnerAvatar(context, data);
48 | }
49 |
50 | Widget _getInnerAvatar(BuildContext context, File? data) {
51 | if (data != null && data.existsSync()) {
52 | return CircleAvatar(
53 | radius: _radius,
54 | backgroundImage: FileImage(data),
55 | );
56 | }
57 |
58 | if (_iconData != null) {
59 | return _getIconAvatar(context, _iconData!);
60 | }
61 |
62 | return _getIconAvatar(context, Icons.cloud);
63 | }
64 |
65 | Widget _getIconAvatar(BuildContext context, IconData iconData) {
66 | return CircleAvatar(
67 | radius: _radius,
68 | backgroundColor: Theme.of(context).primaryIconTheme.color,
69 | child: Icon(
70 | iconData,
71 | size: _radius + 10,
72 | color: Colors.black,
73 | ),
74 | );
75 | }
76 |
77 | Widget _buildAvatarFromStream(BuildContext context) {
78 | return StreamBuilder(
79 | stream: _command,
80 | initialData: _command!.lastResult,
81 | builder: (context, snapshot) => _buildAvatar(context, snapshot.data),
82 | );
83 | }
84 |
85 | @override
86 | Widget build(BuildContext context) {
87 | return _command == null
88 | ? _buildAvatar(context, _avatar)
89 | : _buildAvatarFromStream(context);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/lib/views/widgets/circle_avatar_icon.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class CircleAvatarIcon extends StatelessWidget {
4 | final Icon icon;
5 | final double radius;
6 |
7 | const CircleAvatarIcon({
8 | required this.icon,
9 | this.radius = 13,
10 | });
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | return Stack(
15 | alignment: Alignment.center,
16 | children: [
17 | CircleAvatar(
18 | radius: radius,
19 | backgroundColor: Colors.white,
20 | ),
21 | icon,
22 | ],
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/views/widgets/favorite_icon.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:yaga/views/widgets/circle_avatar_icon.dart';
3 |
4 | class FavoriteIcon extends StatelessWidget {
5 | @override
6 | Widget build(BuildContext context) {
7 | return const Align(
8 | alignment: Alignment.bottomLeft,
9 | child: CircleAvatarIcon(
10 | icon: Icon(
11 | Icons.stars,
12 | color: Colors.amber,
13 | ),
14 | ),
15 | );
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/lib/views/widgets/folder_icon.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:yaga/model/nc_file.dart';
3 | import 'package:yaga/views/widgets/favorite_icon.dart';
4 |
5 | class FolderIcon extends StatelessWidget {
6 | final NcFile dir;
7 | final double size;
8 |
9 | const FolderIcon({super.key, required this.dir, this.size = 48});
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | Stack stack = Stack(children: [
14 | Icon(
15 | Icons.folder,
16 | size: size,
17 | ),
18 | ]);
19 |
20 | if (dir.favorite) {
21 | stack.children.add(FavoriteIcon());
22 | }
23 |
24 | return SizedBox(
25 | height: size,
26 | width: size,
27 | child: stack,
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/views/widgets/image_search.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:yaga/model/nc_file.dart';
3 | import 'package:yaga/managers/widget_local/file_list_local_manager.dart';
4 | import 'package:yaga/views/widgets/image_view_container.dart';
5 | import 'package:yaga/views/widgets/image_views/utils/view_configuration.dart';
6 |
7 | class ImageSearch extends SearchDelegate {
8 | final FileListLocalManager _fileListLocalManager;
9 | final ViewConfiguration _viewConfig;
10 |
11 | ImageSearch(this._fileListLocalManager, this._viewConfig);
12 |
13 | @override
14 | ThemeData appBarTheme(BuildContext context) {
15 | //todo: keep track of this issue and improve: https://github.com/flutter/flutter/issues/45498
16 | assert(context != null);
17 | final ThemeData theme = Theme.of(context);
18 | assert(theme != null);
19 | return theme.copyWith(
20 | inputDecorationTheme: InputDecorationTheme(
21 | hintStyle:
22 | TextStyle(color: theme.primaryTextTheme.headline5?.color)),
23 | textTheme: theme.textTheme.copyWith(
24 | headline5: theme.textTheme.headline5
25 | ?.copyWith(color: theme.primaryTextTheme.headline5?.color),
26 | headline6: const TextStyle(
27 | color: Colors.white,
28 | fontWeight: FontWeight.normal,
29 | fontSize: 18,
30 | ),
31 | ));
32 | }
33 |
34 | @override
35 | List buildActions(BuildContext context) {
36 | return [
37 | IconButton(icon: const Icon(Icons.close), onPressed: () => query = "")
38 | ];
39 | }
40 |
41 | @override
42 | Widget buildLeading(BuildContext context) {
43 | return IconButton(
44 | icon: const Icon(Icons.arrow_back),
45 | onPressed: () => Navigator.pop(context));
46 | }
47 |
48 | @override
49 | Widget buildResults(BuildContext context) {
50 | return ImageViewContainer(
51 | fileListLocalManager: _fileListLocalManager,
52 | viewConfig: ViewConfiguration.fromViewConfig(
53 | viewConfig: _viewConfig,
54 | onFolderTap: (NcFile file) => close(context, file),
55 | ),
56 | filter: (NcFile file) =>
57 | file.name.toLowerCase().contains(query.toLowerCase()) ||
58 | file.lastModified.toString().contains(query),
59 | );
60 | }
61 |
62 | @override
63 | Widget buildSuggestions(BuildContext context) {
64 | return ListView(
65 | //children: [],
66 | );
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/lib/views/widgets/image_views/nc_grid_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:yaga/model/sorted_file_folder_list.dart';
3 | import 'package:yaga/views/widgets/image_views/utils/grid_delegate.dart';
4 | import 'package:yaga/views/widgets/image_views/utils/view_configuration.dart';
5 | import 'package:yaga/views/widgets/remote_folder_widget.dart';
6 | import 'package:yaga/views/widgets/remote_image_widget.dart';
7 |
8 | class NcGridView extends StatelessWidget with GridDelegate {
9 | static const String viewKey = "grid";
10 | final SortedFileFolderList sorted;
11 | final ViewConfiguration viewConfig;
12 |
13 | const NcGridView(this.sorted, this.viewConfig);
14 |
15 | Widget _buildImage(int key, BuildContext context) {
16 | return InkWell(
17 | onTap: () => viewConfig.onFileTap?.call(sorted.files, key),
18 | onLongPress: () => viewConfig.onSelect?.call(sorted.files, key),
19 | child: RemoteImageWidget(
20 | sorted.files[key],
21 | key: ValueKey(sorted.files[key].uri.path),
22 | cacheWidth: 256,
23 | // cacheHeight: 256,
24 | ),
25 | );
26 | }
27 |
28 | Widget _buildFolder(int key, BuildContext context) {
29 | return Card(
30 | borderOnForeground: true,
31 | elevation: 2.0,
32 | child: Center(
33 | child: RemoteFolderWidget(index: key, sorted: sorted, viewConfig: viewConfig,)
34 | ),
35 | );
36 | }
37 |
38 | @override
39 | Widget build(BuildContext context) {
40 | final SliverGrid folderGrid = SliverGrid(
41 | delegate: SliverChildBuilderDelegate(
42 | (context, index) => _buildFolder(index, context),
43 | childCount: sorted.folders.length,
44 | ),
45 | gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
46 | maxCrossAxisExtent: 300.0,
47 | crossAxisSpacing: 2,
48 | mainAxisSpacing: 2,
49 | mainAxisExtent: 70,
50 | ),
51 | );
52 |
53 | final SliverPadding paddedFolders = SliverPadding(
54 | padding: const EdgeInsets.all(5),
55 | sliver: folderGrid,
56 | );
57 |
58 | final SliverGrid fileGrid = SliverGrid(
59 | gridDelegate: buildImageGridDelegate(context),
60 | delegate: SliverChildBuilderDelegate(
61 | (context, index) => _buildImage(index, context),
62 | childCount: sorted.files.length,
63 | ),
64 | );
65 |
66 | final SliverPadding paddedFiles = SliverPadding(
67 | padding: const EdgeInsets.all(5),
68 | sliver: fileGrid,
69 | );
70 |
71 | return CustomScrollView(
72 | physics: const AlwaysScrollableScrollPhysics(),
73 | slivers: viewConfig.showFolders.value
74 | ? [paddedFolders, paddedFiles]
75 | : [paddedFiles],
76 | );
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/lib/views/widgets/image_views/nc_list_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:yaga/model/sorted_file_folder_list.dart';
3 | import 'package:yaga/views/widgets/image_views/utils/view_configuration.dart';
4 | import 'package:yaga/views/widgets/remote_folder_widget.dart';
5 | import 'package:yaga/views/widgets/remote_image_widget.dart';
6 |
7 | class NcListView extends StatelessWidget {
8 | static const String viewKey = "list";
9 | final SortedFileFolderList sorted;
10 | final ViewConfiguration viewConfig;
11 |
12 | const NcListView({
13 | required this.sorted,
14 | required this.viewConfig,
15 | });
16 |
17 | @override
18 | Widget build(BuildContext context) {
19 | final slivers = [];
20 |
21 | const Widget divider = Divider(
22 | thickness: 2,
23 | );
24 |
25 | if (viewConfig.showFolders.value) {
26 | slivers.add(
27 | SliverList.separated(
28 | separatorBuilder: (context, index) => divider,
29 | itemBuilder: (context, index) => RemoteFolderWidget(sorted: sorted, index: index, viewConfig: viewConfig),
30 | itemCount: sorted.folders.length,
31 | ),
32 | );
33 | }
34 |
35 | slivers.add(
36 | SliverList.list(
37 | children: const [
38 | divider,
39 | ],
40 | ),
41 | );
42 |
43 | slivers.add(
44 | SliverList.separated(
45 | separatorBuilder: (context, index) => divider,
46 | itemBuilder: (context, index) => ListTile(
47 | leading: SizedBox(
48 | width: 64,
49 | height: 64,
50 | child: RemoteImageWidget(
51 | sorted.files[index],
52 | key: ValueKey(sorted.files[index].uri.path),
53 | cacheWidth: 128,
54 | showFileEnding: false,
55 | ),
56 | ),
57 | title: Text(sorted.files[index].name),
58 | onTap: () => viewConfig.onFileTap?.call(sorted.files, index),
59 | onLongPress: () => viewConfig.onSelect?.call(sorted.files, index),
60 | ),
61 | itemCount: sorted.files.length,
62 | ),
63 | );
64 |
65 | return CustomScrollView(
66 | physics: const AlwaysScrollableScrollPhysics(),
67 | slivers: slivers,
68 | );
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/lib/views/widgets/image_views/utils/grid_delegate.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | mixin class GridDelegate {
4 | SliverGridDelegate buildImageGridDelegate(BuildContext context) {
5 | return const SliverGridDelegateWithMaxCrossAxisExtent(
6 | maxCrossAxisExtent: 175.0,
7 | crossAxisSpacing: 2,
8 | mainAxisSpacing: 2,
9 | );
10 | }
11 | }
--------------------------------------------------------------------------------
/lib/views/widgets/list_menu_entry.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class ListMenuEntry extends StatelessWidget {
4 | final IconData _iconData;
5 | final String _text;
6 |
7 | const ListMenuEntry(this._iconData, this._text);
8 |
9 | @override
10 | Widget build(BuildContext context) {
11 | return ListTile(
12 | leading: Icon(_iconData),
13 | title: Text(_text),
14 | // isThreeLine: false,
15 | contentPadding: const EdgeInsets.all(0),
16 | );
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/lib/views/widgets/preferences/action_preference_widget.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:yaga/model/preferences/action_preference.dart';
3 |
4 | class ActionPreferenceWidget extends StatelessWidget {
5 | final ActionPreference _pref;
6 |
7 | const ActionPreferenceWidget(this._pref);
8 |
9 | @override
10 | Widget build(BuildContext context) {
11 | return ListTile(
12 | title: Text(
13 | _pref.title!,
14 | ),
15 | onTap: _pref.action,
16 | enabled: _pref.enabled!,
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/views/widgets/preferences/bool_preference_widget.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:rx_command/rx_command.dart';
3 | import 'package:yaga/managers/settings_manager.dart';
4 | import 'package:yaga/model/preferences/bool_preference.dart';
5 | import 'package:yaga/model/preferences/preference.dart';
6 | import 'package:yaga/services/shared_preferences_service.dart';
7 | import 'package:yaga/utils/service_locator.dart';
8 | import 'package:yaga/views/widgets/preferences/preference_list_tile_widget.dart';
9 |
10 | class BoolPreferenceWidget extends StatelessWidget {
11 | final BoolPreference _defaultPreference;
12 | final RxCommand? onChangeCommand;
13 |
14 | const BoolPreferenceWidget(this._defaultPreference, {this.onChangeCommand});
15 |
16 | //todo: generalize this for all preferences
17 | void _notifyChange(BoolPreference pref) {
18 | if (onChangeCommand != null) {
19 | onChangeCommand!(pref);
20 | return;
21 | }
22 | getIt.get().persistBoolSettingCommand(pref);
23 | }
24 |
25 | @override
26 | Widget build(BuildContext context) {
27 | return PreferenceListTileWidget(
28 | initData: getIt
29 | .get()
30 | .loadPreferenceFromBool(_defaultPreference),
31 | listTileBuilder: (context, pref) => SwitchListTile(
32 | title: Text(pref.title!),
33 | value: pref.value,
34 | onChanged: pref.enabled! ?
35 | (value) => _notifyChange(pref.rebuild((b) => b..value = value)) :
36 | null,
37 | ),
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/views/widgets/preferences/choice_preference_widget.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:rx_command/rx_command.dart';
3 | import 'package:yaga/managers/settings_manager.dart';
4 | import 'package:yaga/model/preferences/choice_preference.dart';
5 | import 'package:yaga/model/preferences/preference.dart';
6 | import 'package:yaga/model/route_args/choice_selector_screen_arguments.dart';
7 | import 'package:yaga/services/shared_preferences_service.dart';
8 | import 'package:yaga/utils/service_locator.dart';
9 | import 'package:yaga/views/screens/choice_selector_screen.dart';
10 | import 'package:yaga/views/widgets/preferences/preference_list_tile_widget.dart';
11 |
12 | class ChoicePreferenceWidget extends StatelessWidget {
13 | final ChoicePreference _choicePreference;
14 | final RxCommand? onChangeCommand;
15 |
16 | const ChoicePreferenceWidget(this._choicePreference, this.onChangeCommand);
17 |
18 | //todo: generalize this for all preferences
19 | void _notifyChange(ChoicePreference pref) {
20 | if (onChangeCommand != null) {
21 | onChangeCommand!(pref);
22 | return;
23 | }
24 | getIt.get().persistStringSettingCommand(pref);
25 | }
26 |
27 | @override
28 | Widget build(BuildContext context) {
29 | return PreferenceListTileWidget(
30 | initData: getIt
31 | .get()
32 | .loadPreferenceFromString(_choicePreference),
33 | listTileBuilder: (context, pref) => ListTile(
34 | title: Text(pref.title!),
35 | subtitle: Text(pref.choices[pref.value]!),
36 | onTap: () => Navigator.pushNamed(
37 | context, ChoiceSelectorScreen.route,
38 | arguments:
39 | ChoiceSelectorScreenArguments(pref, (String value) {
40 | Navigator.pop(context);
41 | _notifyChange(pref.rebuild((b) => b..value = value));
42 | }, () => Navigator.pop(context))),
43 | ));
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/lib/views/widgets/preferences/preference_list_tile_widget.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:yaga/managers/settings_manager.dart';
4 | import 'package:yaga/model/preferences/preference.dart';
5 | import 'package:yaga/utils/service_locator.dart';
6 |
7 | class PreferenceListTileWidget extends StatelessWidget {
8 | final T initData;
9 | final Widget Function(BuildContext, T) listTileBuilder;
10 |
11 | const PreferenceListTileWidget({
12 | required this.initData,
13 | required this.listTileBuilder,
14 | });
15 |
16 | @override
17 | Widget build(BuildContext context) {
18 | return StreamBuilder(
19 | stream: getIt
20 | .get()
21 | .updateSettingCommand
22 | .where((event) => event.key == initData.key)
23 | .map((event) => event as T),
24 | initialData: initData,
25 | builder: (BuildContext context, AsyncSnapshot snapshot) {
26 | return listTileBuilder(context, snapshot.data!);
27 | },
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/views/widgets/preferences/section_preference_widget.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:yaga/model/preferences/preference.dart';
3 |
4 | class SectionPreferenceWidget extends StatelessWidget {
5 | final Preference _pref;
6 |
7 | const SectionPreferenceWidget(this._pref);
8 |
9 | @override
10 | Widget build(BuildContext context) {
11 | return ListTile(
12 | title: Text(
13 | _pref.title!,
14 | style: TextStyle(
15 | color: Theme.of(context).colorScheme.secondary, fontWeight: FontWeight.bold),
16 | ),
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/views/widgets/preferences/string_preference_widget.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:yaga/model/preferences/string_preference.dart';
3 | import 'package:yaga/services/shared_preferences_service.dart';
4 | import 'package:yaga/utils/service_locator.dart';
5 | import 'package:yaga/views/widgets/preferences/preference_list_tile_widget.dart';
6 |
7 | class StringPreferenceWidget extends StatelessWidget {
8 | final StringPreference _defaultPreference;
9 | final Function(StringPreference) _onTap;
10 |
11 | const StringPreferenceWidget(this._defaultPreference, this._onTap);
12 |
13 | @override
14 | Widget build(BuildContext context) {
15 | return PreferenceListTileWidget(
16 | initData: getIt
17 | .get()
18 | .loadPreferenceFromString(_defaultPreference),
19 | listTileBuilder: (context, pref) => ListTile(
20 | title: Text(pref.title!),
21 | subtitle: Text(pref.value),
22 | onTap: () => _onTap(pref),
23 | ));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/views/widgets/preferences/uri_preference_widget.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:rx_command/rx_command.dart';
3 | import 'package:yaga/managers/settings_manager.dart';
4 | import 'package:yaga/model/preferences/preference.dart';
5 | import 'package:yaga/model/preferences/uri_preference.dart';
6 | import 'package:yaga/model/route_args/path_selector_screen_arguments.dart';
7 | import 'package:yaga/services/name_exchange_service.dart';
8 | import 'package:yaga/services/shared_preferences_service.dart';
9 | import 'package:yaga/utils/service_locator.dart';
10 | import 'package:yaga/views/screens/path_selector_screen.dart';
11 | import 'package:yaga/views/widgets/preferences/preference_list_tile_widget.dart';
12 |
13 | class UriPreferenceWidget extends StatelessWidget {
14 | final UriPreference _defaultPref;
15 | final RxCommand? onChangeCommand;
16 |
17 | const UriPreferenceWidget(this._defaultPref, {this.onChangeCommand});
18 |
19 | void _notifyChange(UriPreference pref) {
20 | if (onChangeCommand != null) {
21 | onChangeCommand!(pref);
22 | return;
23 | }
24 | getIt.get().persistStringSettingCommand(pref);
25 | }
26 |
27 | void _pushToNavigation(BuildContext context, UriPreference pref, Uri uri) {
28 | Navigator.pushNamed(
29 | context,
30 | PathSelectorScreen.route,
31 | arguments: PathSelectorScreenArguments(
32 | uri: uri,
33 | onSelect: (Uri path) => _notifyChange(
34 | pref.rebuild((b) => b..value = path),
35 | ),
36 | fixedOrigin: pref.fixedOrigin,
37 | schemeFilter: pref.schemeFilter,
38 | ),
39 | );
40 | }
41 |
42 | @override
43 | Widget build(BuildContext context) {
44 | return PreferenceListTileWidget(
45 | initData: getIt
46 | .get()
47 | .loadPreferenceFromString(_defaultPref),
48 | listTileBuilder: (context, pref) => ListTile(
49 | enabled: pref.enabled!,
50 | title: Text(pref.title!),
51 | subtitle: Text(Uri.decodeComponent(getIt.get().convertUriToHumanReadableUri(pref.value).toString())),
52 | onTap: () => _pushToNavigation(context, pref, pref.value),
53 | ),
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/lib/views/widgets/remote_folder_widget.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:yaga/managers/file_manager/file_manager.dart';
3 | import 'package:yaga/model/nc_file.dart';
4 | import 'package:yaga/model/sorted_file_folder_list.dart';
5 | import 'package:yaga/utils/service_locator.dart';
6 | import 'package:yaga/views/widgets/folder_icon.dart';
7 | import 'package:yaga/views/widgets/image_views/utils/view_configuration.dart';
8 |
9 | class RemoteFolderWidget extends StatelessWidget {
10 |
11 | final SortedFileFolderList sorted;
12 | final int index;
13 | final ViewConfiguration viewConfig;
14 |
15 | const RemoteFolderWidget({super.key, required this.sorted, required this.index, required this.viewConfig});
16 |
17 | @override
18 | Widget build(BuildContext context) {
19 | final folder = sorted.folders[index];
20 | return StreamBuilder(stream: getIt.get().updateImageCommand
21 | .where((event) => event.uri.path == folder.uri.path),
22 | initialData: folder,
23 | builder: (context, snapshot) => ListTile(
24 | onLongPress: () => viewConfig.onSelect?.call(sorted.folders, index),
25 | onTap: () => viewConfig.onFolderTap?.call(snapshot.data!),
26 | leading: FolderIcon(dir: snapshot.data!),
27 | trailing: snapshot.data!.selected ? const Icon(Icons.check_circle) : null,
28 | title: Text(
29 | folder.name,
30 | ),
31 | ),
32 | );
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/lib/views/widgets/search_icon_button.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:yaga/managers/widget_local/file_list_local_manager.dart';
3 | import 'package:yaga/model/nc_file.dart';
4 | import 'package:yaga/views/widgets/image_search.dart';
5 | import 'package:yaga/views/widgets/image_views/utils/view_configuration.dart';
6 |
7 | class SearchIconButton extends StatelessWidget {
8 | final FileListLocalManager fileListLocalManager;
9 | final ViewConfiguration viewConfig;
10 | final Function(NcFile?)? searchResultHandler;
11 |
12 | const SearchIconButton({
13 | required this.fileListLocalManager,
14 | required this.viewConfig,
15 | this.searchResultHandler,
16 | });
17 |
18 | @override
19 | Widget build(BuildContext context) {
20 | return IconButton(
21 | icon: const Icon(Icons.search),
22 | onPressed: () async {
23 | final NcFile? file = await showSearch(
24 | context: context,
25 | delegate: ImageSearch(fileListLocalManager, viewConfig),
26 | );
27 | searchResultHandler?.call(file);
28 | },
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/lib/views/widgets/select_cancel_bottom_navigation.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class SelectCancelBottomNavigation extends StatelessWidget {
4 | final Function onCommit;
5 | final Function onCancel;
6 | final String labelSelect;
7 | final String labelCancel;
8 | final IconData iconSelect;
9 | final List betweenItems;
10 | final List betweenItemsCallbacks;
11 |
12 | const SelectCancelBottomNavigation({
13 | required this.onCommit,
14 | required this.onCancel,
15 | this.labelSelect = "Select",
16 | this.labelCancel = "Cancel",
17 | this.iconSelect = Icons.check,
18 | this.betweenItems = const [],
19 | this.betweenItemsCallbacks = const [],
20 | });
21 |
22 | @override
23 | Widget build(BuildContext context) {
24 | final List items = [];
25 | items.add(BottomNavigationBarItem(
26 | icon: const Icon(Icons.close),
27 | label: labelCancel,
28 | ));
29 | items.addAll(betweenItems);
30 | items.add(BottomNavigationBarItem(
31 | icon: Icon(iconSelect),
32 | label: labelSelect,
33 | ));
34 |
35 | return BottomNavigationBar(
36 | currentIndex: items.length - 1,
37 | onTap: (index) {
38 | if (index == items.length - 1) {
39 | onCommit();
40 | return;
41 | }
42 |
43 | if (index == 0) {
44 | onCancel();
45 | return;
46 | }
47 |
48 | final betweenItemsIndex = index - 1;
49 | if (betweenItemsIndex >= 0) {
50 | betweenItemsCallbacks[betweenItemsIndex].call();
51 | return;
52 | }
53 | },
54 | items: items,
55 | );
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/lib/views/widgets/selection_action_cancel_dialog.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class SelectionActionCancelDialog extends StatelessWidget {
4 | final String title;
5 | final Function() cancelAction;
6 |
7 | const SelectionActionCancelDialog(this.title, this.cancelAction);
8 |
9 | @override
10 | Widget build(BuildContext context) {
11 | return AlertDialog(
12 | title: Text(title),
13 | content: const SingleChildScrollView(
14 | child: Center(
15 | child: CircularProgressIndicator(),
16 | ),
17 | ),
18 | actions: [
19 | TextButton(
20 | onPressed: () => cancelAction(),
21 | child: const Text('Cancel'),
22 | ),
23 | ],
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/lib/views/widgets/selection_app_bar.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:yaga/managers/widget_local/file_list_local_manager.dart';
3 | import 'package:yaga/model/nc_file.dart';
4 | import 'package:yaga/views/widgets/image_views/utils/view_configuration.dart';
5 | import 'package:yaga/views/widgets/search_icon_button.dart';
6 | import 'package:yaga/views/widgets/selection_popup_menu_button.dart';
7 | import 'package:yaga/views/widgets/selection_select_all.dart';
8 |
9 | class SelectionAppBar extends PreferredSize {
10 | factory SelectionAppBar({
11 | required FileListLocalManager fileListLocalManager,
12 | required ViewConfiguration viewConfig,
13 | required Widget Function(BuildContext, List) appBarBuilder,
14 | double bottomHeight = 0,
15 | Function(NcFile?)? searchResultHandler,
16 | }) {
17 | final Widget child = StreamBuilder(
18 | initialData: fileListLocalManager.selectionModeChanged.lastResult,
19 | stream: fileListLocalManager.selectionModeChanged,
20 | builder: (context, snapshot) {
21 | final List actions = [];
22 | if (fileListLocalManager.isInSelectionMode) {
23 | actions.add(SelectionSelectAll(fileListLocalManager));
24 | }
25 |
26 | actions.add(SearchIconButton(
27 | fileListLocalManager: fileListLocalManager,
28 | viewConfig: viewConfig,
29 | searchResultHandler: searchResultHandler,
30 | ));
31 |
32 | if (fileListLocalManager.isInSelectionMode) {
33 | actions.add(SelectionPopupMenuButton(
34 | fileListLocalManager: fileListLocalManager,
35 | ));
36 | }
37 |
38 | return appBarBuilder(context, actions);
39 | },
40 | );
41 |
42 | return SelectionAppBar._internal(
43 | preferredSize: Size.fromHeight(kToolbarHeight + bottomHeight),
44 | child: child,
45 | );
46 | }
47 |
48 | const SelectionAppBar._internal({
49 | required Size preferredSize,
50 | required Widget child,
51 | }) : super(preferredSize: preferredSize, child: child);
52 | }
53 |
--------------------------------------------------------------------------------
/lib/views/widgets/selection_select_all.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:yaga/managers/widget_local/file_list_local_manager.dart';
3 |
4 | class SelectionSelectAll extends StatelessWidget {
5 | final FileListLocalManager _fileListLocalManager;
6 |
7 | const SelectionSelectAll(this._fileListLocalManager);
8 |
9 | @override
10 | Widget build(BuildContext context) {
11 | return IconButton(
12 | icon: const Icon(Icons.select_all),
13 | onPressed: _fileListLocalManager.toggleSelect,
14 | );
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/lib/views/widgets/selection_title.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:yaga/managers/widget_local/file_list_local_manager.dart';
3 |
4 | class SelectionTitle extends StatelessWidget {
5 | final FileListLocalManager _fileListLocalManager;
6 | final Widget? defaultTitel;
7 |
8 | const SelectionTitle(this._fileListLocalManager, {this.defaultTitel});
9 |
10 | @override
11 | Widget build(BuildContext context) {
12 | return StreamBuilder(
13 | stream: _fileListLocalManager.selectionChangedCommand,
14 | builder: (context, snapshot) {
15 | if (defaultTitel != null && !_fileListLocalManager.isInSelectionMode) {
16 | return defaultTitel!;
17 | }
18 | return Text(
19 | "${_fileListLocalManager.selected.length}/${_fileListLocalManager.sorted.length}",
20 | overflow: TextOverflow.fade,
21 | );
22 | },
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/views/widgets/selection_will_pop_scope.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:yaga/managers/widget_local/file_list_local_manager.dart';
3 |
4 | class SelectionWillPopScope extends StatelessWidget {
5 | final Widget child;
6 | final FileListLocalManager fileListLocalManager;
7 |
8 | const SelectionWillPopScope({
9 | required this.child,
10 | required this.fileListLocalManager,
11 | });
12 |
13 | @override
14 | Widget build(BuildContext context) {
15 | return WillPopScope(
16 | onWillPop: () async {
17 | if (fileListLocalManager.selectionModeChanged.lastResult!) {
18 | fileListLocalManager.deselectAll();
19 | return false;
20 | }
21 |
22 | return true;
23 | },
24 | child: child,
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/lib/views/widgets/yaga_about_dialog.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_markdown/flutter_markdown.dart';
3 | import 'package:flutter_svg/svg.dart';
4 | import 'package:markdown/markdown.dart' as mk;
5 | import 'package:package_info_plus/package_info_plus.dart';
6 | import 'package:url_launcher/url_launcher_string.dart';
7 | import 'package:yaga/utils/service_locator.dart';
8 |
9 | class YagaAboutDialog extends StatelessWidget {
10 | @override
11 | Widget build(BuildContext context) {
12 | return AboutDialog(
13 | applicationVersion: "v${getIt.get().version}",
14 | applicationIcon: SvgPicture.asset(
15 | "assets/icon/icon.svg",
16 | semanticsLabel: 'Yaga Logo',
17 | width: 56,
18 | ),
19 | children: [
20 | FutureBuilder(
21 | future: DefaultAssetBundle.of(context).loadString("assets/news.md"),
22 | builder: (context, snapshot) {
23 | final sc = ScrollController();
24 | return SizedBox(
25 | height: 300,
26 | width: 400,
27 | child: Scrollbar(
28 | thumbVisibility: true,
29 | controller: sc,
30 | child: Markdown(
31 | padding: const EdgeInsets.fromLTRB(0.0, 0.0, 5.0, 0.0),
32 | data: snapshot.data?.toString() ?? "",
33 | shrinkWrap: true,
34 | controller: sc,
35 | extensionSet: mk.ExtensionSet(
36 | mk.ExtensionSet.gitHubFlavored.blockSyntaxes,
37 | [
38 | mk.EmojiSyntax(),
39 | ...mk.ExtensionSet.gitHubFlavored.inlineSyntaxes
40 | ],
41 | ),
42 | ),
43 | ),
44 | );
45 | },
46 | ),
47 | Align(
48 | alignment: Alignment.topLeft,
49 | child: TextButton.icon(
50 | onPressed: () =>
51 | launchUrlString("https://vauvenal5.github.io/yaga-docs/"),
52 | icon: const Icon(Icons.book_outlined),
53 | label: const Text("Read the docs"),
54 | ),
55 | ),
56 | Align(
57 | alignment: Alignment.topLeft,
58 | child: TextButton.icon(
59 | onPressed: () => launchUrlString(
60 | "https://vauvenal5.github.io/yaga-docs/privacy/",
61 | ),
62 | icon: const Icon(Icons.policy_outlined),
63 | label: const Text("Privacy Policy"),
64 | ),
65 | ),
66 | Align(
67 | alignment: Alignment.topLeft,
68 | child: TextButton.icon(
69 | onPressed: () =>
70 | launchUrlString("https://github.com/vauvenal5/yaga/issues"),
71 | icon: const Icon(Icons.bug_report_outlined),
72 | label: const Text("Report a bug"),
73 | ),
74 | ),
75 | ],
76 | );
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/lib/views/widgets/yaga_bottom_nav_bar.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:yaga/managers/tab_manager.dart';
3 | import 'package:yaga/utils/service_locator.dart';
4 | import 'package:yaga/views/screens/yaga_home_screen.dart';
5 |
6 | class YagaBottomNavBar extends StatelessWidget {
7 | final YagaHomeTab _selectedTab;
8 |
9 | const YagaBottomNavBar(this._selectedTab);
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | return BottomNavigationBar(
14 | currentIndex: _getCurrentIndex(),
15 | onTap: (index) {
16 | switch (index) {
17 | case 2:
18 | getIt.get().tabChangedCommand(YagaHomeTab.folder);
19 | return;
20 | case 1:
21 | getIt.get().tabChangedCommand(YagaHomeTab.favorites);
22 | return;
23 | default:
24 | getIt.get().tabChangedCommand(YagaHomeTab.grid);
25 | }
26 | },
27 | items: const [
28 | BottomNavigationBarItem(
29 | icon: Icon(Icons.home),
30 | label: 'Home',
31 | ),
32 | BottomNavigationBarItem(
33 | icon: Icon(Icons.star),
34 | label: 'Favorites',
35 | ),
36 | BottomNavigationBarItem(
37 | icon: Icon(Icons.folder),
38 | label: 'Browse',
39 | ),
40 | ],
41 | );
42 | }
43 |
44 | int _getCurrentIndex() {
45 | switch (_selectedTab) {
46 | case YagaHomeTab.folder:
47 | return 2;
48 | case YagaHomeTab.favorites:
49 | return 1;
50 | default:
51 | return 0;
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/lib/views/widgets/yaga_popup_menu_button.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class YagaPopupMenuButton extends StatelessWidget {
4 | final List> Function(BuildContext context) itemBuilder;
5 | final void Function(BuildContext context, T result) handler;
6 |
7 | const YagaPopupMenuButton(this.itemBuilder, this.handler);
8 |
9 | @override
10 | Widget build(BuildContext context) {
11 | return PopupMenuButton(
12 | offset: const Offset(0, 10),
13 | onSelected: (T result) => handler(context, result),
14 | itemBuilder: itemBuilder,
15 | );
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/linux/.gitignore:
--------------------------------------------------------------------------------
1 | flutter/ephemeral
2 |
--------------------------------------------------------------------------------
/linux/flutter/generated_plugin_registrant.cc:
--------------------------------------------------------------------------------
1 | //
2 | // Generated file. Do not edit.
3 | //
4 |
5 | // clang-format off
6 |
7 | #include "generated_plugin_registrant.h"
8 |
9 | #include