This commit is contained in:
2022-09-26 00:12:57 +02:00
commit 7a95780b5b
80 changed files with 2749 additions and 0 deletions

View 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);
}

View 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));
}
}

View 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)';
}

View 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
View 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
View 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
View 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'),
);
}
}