init
This commit is contained in:
26
lib/data_provider/data_provider.dart
Normal file
26
lib/data_provider/data_provider.dart
Normal file
@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Item {
|
||||
bool isFolder;
|
||||
Uri uri;
|
||||
String name;
|
||||
|
||||
Item(this.isFolder, this.uri, this.name);
|
||||
}
|
||||
|
||||
class Folder {
|
||||
List<Item> items;
|
||||
Uri self;
|
||||
Uri parent;
|
||||
|
||||
Folder(this.items, this.self, this.parent);
|
||||
}
|
||||
|
||||
|
||||
abstract class DataProvider {
|
||||
final List<String> validSuffix = [".jpg", ".jpeg", ".png"];
|
||||
|
||||
void connect();
|
||||
Future<Folder> listOfFiles({Uri? uri});
|
||||
ImageProvider getImageProvider(Uri uri);
|
||||
}
|
41
lib/data_provider/local_data_provider.dart
Normal file
41
lib/data_provider/local_data_provider.dart
Normal file
@ -0,0 +1,41 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:gallery/data_provider/data_provider.dart';
|
||||
|
||||
class LocalDataProvider extends DataProvider {
|
||||
final String initialPath;
|
||||
|
||||
LocalDataProvider(this.initialPath);
|
||||
|
||||
@override
|
||||
void connect() {}
|
||||
|
||||
@override
|
||||
Future<Folder> listOfFiles({Uri? uri}) async {
|
||||
final dir = uri != null ? Directory.fromUri(uri) : Directory(initialPath);
|
||||
|
||||
final list = dir.listSync();
|
||||
final List<Item> res = [];
|
||||
|
||||
for (var val in list) {
|
||||
if (validSuffix.any((suff) => val.path.endsWith(suff))) {
|
||||
res.add(Item(false, val.uri, val.uri.pathSegments.last));
|
||||
} else if (await FileSystemEntity.isDirectory(val.path)) {
|
||||
res.add(
|
||||
Item(true, val.uri, val.uri.pathSegments.reversed.skip(1).first));
|
||||
}
|
||||
}
|
||||
|
||||
res.sort(
|
||||
(a, b) => b.isFolder ? 1 : -1,
|
||||
);
|
||||
|
||||
return Folder(res, dir.uri, dir.parent.uri);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageProvider getImageProvider(Uri uri) {
|
||||
return FileImage(File.fromUri(uri));
|
||||
}
|
||||
}
|
148
lib/data_provider/ssh_data_provider.dart
Normal file
148
lib/data_provider/ssh_data_provider.dart
Normal file
@ -0,0 +1,148 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:gallery/data_provider/data_provider.dart';
|
||||
|
||||
import 'dart:ui' as ui show Codec, ImmutableBuffer;
|
||||
|
||||
class SSHDataProvider extends DataProvider {
|
||||
final String host;
|
||||
final int port;
|
||||
final String username;
|
||||
final String password;
|
||||
final String initialPath;
|
||||
|
||||
SftpClient? sftpClient;
|
||||
SSHClient? sshClient;
|
||||
|
||||
SSHDataProvider(
|
||||
{required this.host,
|
||||
required this.port,
|
||||
required this.initialPath,
|
||||
required this.password,
|
||||
required this.username});
|
||||
|
||||
@override
|
||||
Future<void> connect() async {
|
||||
if (sshClient != null && !sshClient!.isClosed) return;
|
||||
|
||||
sshClient = SSHClient(
|
||||
await SSHSocket.connect(host, port),
|
||||
username: username,
|
||||
onPasswordRequest: () => password,
|
||||
);
|
||||
|
||||
sftpClient = await sshClient?.sftp();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Folder> listOfFiles({Uri? uri}) async {
|
||||
await connect();
|
||||
if (sftpClient == null) throw const FormatException("");
|
||||
|
||||
final dir = uri != null ? Directory.fromUri(uri) : Directory(initialPath);
|
||||
|
||||
final items = await sftpClient!.listdir(dir.path);
|
||||
List<Item> res = [];
|
||||
for (final val in items) {
|
||||
if (validSuffix.any((suff) => val.filename.endsWith(suff))) {
|
||||
res.add(Item(false, Uri.file(dir.path + val.filename), val.filename));
|
||||
} else if (val.attr.isDirectory) {
|
||||
res.add(
|
||||
Item(true, Uri.directory(dir.path + val.filename), val.filename));
|
||||
}
|
||||
}
|
||||
|
||||
return Folder(res, dir.uri, dir.parent.uri);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageProvider getImageProvider(Uri uri) {
|
||||
return _SSHImageProvider(uri, sftpClient!);
|
||||
}
|
||||
}
|
||||
|
||||
class _SSHImageProvider extends ImageProvider<_SSHImageProvider> {
|
||||
/// Creates an object that decodes a [File] as an image.
|
||||
///
|
||||
/// The arguments must not be null.
|
||||
const _SSHImageProvider(this.uri, this.sftpClient, {this.scale = 1.0})
|
||||
: assert(scale != null);
|
||||
|
||||
/// The file to decode into an image.
|
||||
final Uri uri;
|
||||
final SftpClient sftpClient;
|
||||
|
||||
/// The scale to place in the [ImageInfo] object of the image.
|
||||
final double scale;
|
||||
|
||||
@override
|
||||
Future<_SSHImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture<_SSHImageProvider>(this);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(_SSHImageProvider key, DecoderCallback decode) {
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, null, decode),
|
||||
scale: key.scale,
|
||||
debugLabel: key.uri.path,
|
||||
informationCollector: () => <DiagnosticsNode>[
|
||||
ErrorDescription('Path: ${uri.path}'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadBuffer(
|
||||
_SSHImageProvider key, DecoderBufferCallback decode) {
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, decode, null),
|
||||
scale: key.scale,
|
||||
debugLabel: key.uri.path,
|
||||
informationCollector: () => <DiagnosticsNode>[
|
||||
ErrorDescription('Path: ${uri.path}'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<ui.Codec> _loadAsync(_SSHImageProvider key,
|
||||
DecoderBufferCallback? decode, DecoderCallback? decodeDeprecated) async {
|
||||
assert(key == this);
|
||||
|
||||
final file = await sftpClient.open(uri.toFilePath());
|
||||
// todo do not load whole image in ram, create tempfile instead.
|
||||
final Uint8List bytes = await file.readBytes();
|
||||
|
||||
if (bytes.lengthInBytes == 0) {
|
||||
// The file may become available later.
|
||||
PaintingBinding.instance.imageCache.evict(key);
|
||||
throw StateError('$file is empty and cannot be loaded as an image.');
|
||||
}
|
||||
|
||||
if (decode != null) {
|
||||
return decode(await ui.ImmutableBuffer.fromUint8List(bytes));
|
||||
}
|
||||
return decodeDeprecated!(bytes);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
return other is _SSHImageProvider &&
|
||||
other.uri.path == uri.path &&
|
||||
other.scale == scale;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(uri.path, scale);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'${objectRuntimeType(this, 'FileImage')}("${uri.path}", scale: $scale)';
|
||||
}
|
63
lib/full_screen_image_view.dart
Normal file
63
lib/full_screen_image_view.dart
Normal file
@ -0,0 +1,63 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'data_provider/data_provider.dart';
|
||||
|
||||
class FullScreenImageView extends StatefulWidget {
|
||||
const FullScreenImageView(
|
||||
{Key? key,
|
||||
required this.idx,
|
||||
required this.items,
|
||||
required this.provider})
|
||||
: super(key: key);
|
||||
final int idx;
|
||||
final List<Item> items;
|
||||
final DataProvider provider;
|
||||
|
||||
@override
|
||||
State<FullScreenImageView> createState() => _FullScreenImageViewState();
|
||||
}
|
||||
|
||||
class _FullScreenImageViewState extends State<FullScreenImageView> {
|
||||
late final PageController _controller =
|
||||
PageController(initialPage: widget.idx - 1);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(widget.items[widget.idx - 1].name)),
|
||||
body: Center(
|
||||
child: CallbackShortcuts(
|
||||
bindings: {
|
||||
const SingleActivator(LogicalKeyboardKey.arrowRight): () =>
|
||||
_controller.nextPage(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
curve: Curves.ease),
|
||||
const SingleActivator(LogicalKeyboardKey.arrowLeft): () =>
|
||||
_controller.previousPage(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
curve: Curves.ease),
|
||||
},
|
||||
child: Focus(
|
||||
autofocus: true,
|
||||
child: PageView.builder(
|
||||
itemCount: widget.items.length,
|
||||
controller: _controller,
|
||||
pageSnapping: true,
|
||||
itemBuilder: (context, pagePosition) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(10),
|
||||
child: Image(
|
||||
image: widget.provider
|
||||
.getImageProvider(widget.items[pagePosition].uri),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
135
lib/home_page.dart
Normal file
135
lib/home_page.dart
Normal file
@ -0,0 +1,135 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gallery/data_provider/data_provider.dart';
|
||||
import 'package:gallery/data_provider/local_data_provider.dart';
|
||||
import 'package:gallery/data_provider/ssh_data_provider.dart';
|
||||
import 'package:gallery/image_tile.dart';
|
||||
|
||||
import 'full_screen_image_view.dart';
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({super.key, required this.title});
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
Future<Folder>? folder;
|
||||
|
||||
// DataProvider dprovider = SSHDataProvider(
|
||||
// initialPath: "/media/",
|
||||
// host: "",
|
||||
// password: "",
|
||||
// port: 22,
|
||||
// username: "");
|
||||
|
||||
// DataProvider dprovider = LocalDataProvider("${(await getApplicationDocumentsDirectory()).path}/../Downloads/");
|
||||
DataProvider dprovider = LocalDataProvider("/home");
|
||||
|
||||
final ScrollController _controller = ScrollController();
|
||||
|
||||
_MyHomePageState() {
|
||||
folder = dprovider.listOfFiles();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.title),
|
||||
),
|
||||
body: FutureBuilder(
|
||||
future: folder,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final data = snapshot.data!;
|
||||
|
||||
return GridView.builder(
|
||||
controller: _controller,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisSpacing: 0,
|
||||
mainAxisSpacing: 0,
|
||||
crossAxisCount: width ~/ 300,
|
||||
),
|
||||
itemCount: data.items.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return ImageTile(
|
||||
child: const Icon(Icons.arrow_back, size: 142),
|
||||
onClick: () {
|
||||
setState(() {
|
||||
folder = dprovider.listOfFiles(uri: data.parent);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final elem = data.items[index - 1];
|
||||
return ImageTile(
|
||||
dtaProvider: dprovider,
|
||||
imageUri: elem.isFolder ? null : elem.uri,
|
||||
onClick: () {
|
||||
if (elem.isFolder) {
|
||||
setState(() {
|
||||
folder = dprovider.listOfFiles(uri: elem.uri);
|
||||
});
|
||||
_controller.jumpTo(.0);
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
final d = data.items
|
||||
.where((element) => !element.isFolder)
|
||||
.toList(growable: false);
|
||||
// check how many folders are before index and subtract it
|
||||
final newidx = index -
|
||||
data.items
|
||||
.getRange(0, index)
|
||||
.where((element) => element.isFolder)
|
||||
.length;
|
||||
|
||||
return FullScreenImageView(
|
||||
provider: dprovider,
|
||||
items: d,
|
||||
idx: newidx,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: elem.isFolder
|
||||
? Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.folder_open, size: 142),
|
||||
Text(elem.name)
|
||||
],
|
||||
)
|
||||
: null);
|
||||
},
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
return const Text("Error loading files");
|
||||
}
|
||||
|
||||
return const CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
_controller.animateTo(.0,
|
||||
duration: const Duration(milliseconds: 400),
|
||||
curve: Curves.easeInOutQuad);
|
||||
},
|
||||
tooltip: 'Increment',
|
||||
child: const Icon(Icons.arrow_upward),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
41
lib/image_tile.dart
Normal file
41
lib/image_tile.dart
Normal file
@ -0,0 +1,41 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gallery/data_provider/data_provider.dart';
|
||||
|
||||
class ImageTile extends StatelessWidget {
|
||||
const ImageTile({Key? key, this.onClick, this.child, this.imageUri, this.dtaProvider}) : super(key: key);
|
||||
final Function? onClick;
|
||||
final Widget? child;
|
||||
final Uri? imageUri;
|
||||
final DataProvider? dtaProvider;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
onClick?.call();
|
||||
},
|
||||
child: Container(padding: const EdgeInsets.all(5), child: Container(
|
||||
decoration: imageUri == null
|
||||
? const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(10.0)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
spreadRadius: 2.0,
|
||||
blurRadius: 5.0),
|
||||
])
|
||||
: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: dtaProvider!.getImageProvider(imageUri!),
|
||||
),
|
||||
),
|
||||
child: child
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
34
lib/main.dart
Normal file
34
lib/main.dart
Normal file
@ -0,0 +1,34 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'home_page.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class AppScrollBehavior extends MaterialScrollBehavior {
|
||||
@override
|
||||
Set<PointerDeviceKind> get dragDevices => {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.mouse,
|
||||
};
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
scrollBehavior: AppScrollBehavior(),
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
),
|
||||
home: const MyHomePage(title: 'Gallery'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user