This commit is contained in:
Evan Reichard 2021-01-07 21:45:59 -05:00
parent 67e7bf9f5a
commit 04924ead5c
28 changed files with 676 additions and 288 deletions

View File

@ -1,12 +1,13 @@
package cmd
import (
"reichard.io/imagini/routes"
// "reichard.io/imagini/routes"
"reichard.io/imagini/internal/db"
"reichard.io/imagini/internal/config"
"github.com/urfave/cli/v2"
"net/http"
"log"
// "net/http"
// "log"
"fmt"
)
@ -25,12 +26,21 @@ var CmdDBTest = cli.Command{
}
func serveWeb(ctx *cli.Context) error {
routes.RegisterRoutes()
c := config.NewConfig()
db.ConnectDB(c)
//db.PopulateTestData()
newItems := db.ItemsFromAlbum(1, 2)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", newItems)
return nil
// routes.RegisterRoutes()
// if err := http.ListenAndServe(":8080", nil); err != nil {
// log.Fatal(err)
// }
// return nil
}
func testDatabase(ctx *cli.Context) error {

5
go.mod
View File

@ -3,11 +3,12 @@ module reichard.io/imagini
go 1.15
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/disintegration/imaging v1.6.2 // indirect
github.com/mattn/go-sqlite3 v1.14.6
github.com/tus/tusd v1.4.0
github.com/urfave/cli/v2 v2.3.0
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect
gorm.io/driver/sqlite v1.1.4 // indirect
gorm.io/gorm v1.20.9 // indirect
gorm.io/driver/sqlite v1.1.4
gorm.io/gorm v1.20.9
)

3
go.sum
View File

@ -17,6 +17,9 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSY
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v1.0.2 h1:KPldsxuKGsS2FPWsNeg9ZO18aCrGKujPoWXn2yo+KQM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=

BIN
imagini.db Normal file

Binary file not shown.

0
internal/auth/ldap.go Normal file
View File

0
internal/auth/local.go Normal file
View File

View File

@ -1,21 +1,32 @@
package config
import (
"reichard.io/imagini/internal/db"
"gorm.io/gorm"
"os"
)
type ServerConfig struct {
db *gorm.DB
settings *Settings
type Config struct {
DBType string
DBName string
DBPassword string
DataPath string
ConfigPath string
JWTSecret string
}
func NewConfig() {
loadedSettings := loadSettings()
loadedDB := db.OpenDB(&loadedSettings)
newConfig := &Config {
settings: &loadedSettings,
db: &loadedDB,
func NewConfig() *Config {
return &Config{
DBType: getEnv("DATABASE_TYPE", "SQLite"),
DBName: getEnv("DATABASE_NAME", "imagini"),
DBPassword: getEnv("DATABASE_PASSWORD", ""),
ConfigPath: getEnv("CONFIG_PATH", "/config"),
DataPath: getEnv("DATA_PATH", "/data"),
JWTSecret: getEnv("JWT_SECRET", "58b9340c0472cf045db226bc445966524e780cd38bc3dd707afce80c95d4de6f"),
}
}
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}

View File

@ -1,32 +0,0 @@
package config
import (
"os"
)
type Settings struct {
DBType string
DBName string
DBPassword string
DataPath string
ConfigPath string
JWTSecret string
}
func loadSettings() *Settings {
return &Settings{
DBType: getEnv("DATABASE_TYPE", "SQLite"),
DBName: getEnv("DATABASE_NAME", "imagini"),
DBPassword: getEnv("DATABASE_PASSWORD", ""),
ConfigPath: getEnv("CONFIG_PATH", "/config"),
DataPath: getEnv("DATA_PATH", "/data"),
JWTSecret: getEnv("JWT_SECRET", "58b9340c0472cf045db226bc445966524e780cd38bc3dd707afce80c95d4de6f"),
}
}
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}

View File

@ -1,40 +1,125 @@
package db
import (
"gorm.io/driver/sqlite"
"log"
"path"
"fmt"
"time"
"gorm.io/gorm"
"gorm.io/driver/sqlite"
"reichard.io/imagini/internal/config"
)
func OpenDB(data) *gorm.DB {
database, _ := gorm.Open(sqlite.Open("./db/imagini.db"), &gorm.Config{
var db *gorm.DB
func ConnectDB(c *config.Config) {
fmt.Printf("%+v\n", c)
if c.DBType == "SQLite" {
dbLocation := path.Join(c.ConfigPath, "imagini.db")
db, _ = gorm.Open(sqlite.Open(dbLocation), &gorm.Config{
PrepareStmt: true,
})
} else {
log.Fatal("ERROR: Unsupported Database")
}
// Initialize Database
database.AutoMigrate(&ServerSetting{})
database.AutoMigrate(&User{})
database.AutoMigrate(&MediaItem{})
database.AutoMigrate(&Tag{})
database.AutoMigrate(&Album{})
return database
db.AutoMigrate(&ServerSetting{})
db.AutoMigrate(&User{})
db.AutoMigrate(&MediaItem{})
db.AutoMigrate(&Tag{})
db.AutoMigrate(&Album{})
}
func ItemsFromAlbum(userID int, albumID int) []MediaItem {
database, _ := gorm.Open(sqlite.Open("./db/imagini.db"), &gorm.Config{})
database.Raw(`
SELECT
MediaItems.*
FROM
MediaAlbums
INNER JOIN MediaItems ON MediaAlbums.mediaID = MediaItems.mediaID
WHERE MediaAlbums.albumID = ? AND MediaItems.userID = ?`, albumID, userID)
return nil
func ItemsFromAlbum(user User, album Album) []MediaItem {
var mediaItems []MediaItem
// db.Table("media_albums").
// Select("media_item.*").
// Joins("INNER JOIN media_items ON media_albums.ID = media_items.Albums").
// Where("media_albums.album_id = ? AND media_items.User = ?", albumID, userID).
db.
//Where("album = ? AND user = ?", albumID, userID).
Find(&mediaItems)
return mediaItems
// db.Raw(`
// SELECT
// MediaItems.*
// FROM
// MediaAlbums
// INNER JOIN MediaItems ON MediaAlbums.mediaID = MediaItems.mediaID
// WHERE MediaAlbums.albumID = ? AND MediaItems.userID = ?`, albumID, userID)
}
func ItemsFromTags(userID int, tagID int) []MediaItem {
return nil
}
// func ItemsFromTags(userID int, tagID int) []MediaItem {
// return nil
// }
//
// func IndexMediaItems(newItems []MediaItem) {
// }
func IndexMediaItems(newItems []MediaItem) {
func PopulateTestData() {
user1 := User{Name: "Evan", Email: "evan@reichard.io", AuthType: "LDAP", Salt: "1234", HashedPWSalt: "1234"}
user2 := User{Name: "Ryan", Email: "ryan@example.com", AuthType: "Local", Salt: "2345", HashedPWSalt: "2345"}
user3 := User{Name: "Bill", Email: "bill@example.com", AuthType: "LDAP", Salt: "3456", HashedPWSalt: "3456"}
mi1 := MediaItem{
User: user1,
EXIFDate: time.Now(),
Latitude: "1234",
Longitude: "1234",
RelPath: "./1234.jpg",
Tags: []Tag{
{Name: "Tag1"},
{Name: "Tag2"},
},
Albums: []Album{
{Name: "Album1"},
{Name: "Album2"},
},
}
mi2 := MediaItem{
User: user2,
EXIFDate: time.Now(),
Latitude: "1234",
Longitude: "1234",
RelPath: "./1234.jpg",
Tags: []Tag{
{Name: "Tag3"},
{Name: "Tag4"},
},
Albums: []Album{
{Name: "Album3"},
{Name: "Album4"},
},
}
mi3 := MediaItem{
User: user3,
EXIFDate: time.Now(),
Latitude: "1234",
Longitude: "1234",
RelPath: "./1234.jpg",
Tags: []Tag{
{Name: "Tag4"},
{Name: "Tag5"},
},
Albums: []Album{
{Name: "Album1"},
{Name: "Album7"},
},
}
// db.Create(&user1)
// db.Create(&user2)
// db.Create(&user3)
db.Create(&mi1)
db.Create(&mi2)
db.Create(&mi3)
}

View File

@ -19,12 +19,11 @@ type User struct {
AuthType string
Salt string
HashedPWSalt string
MediaItems []MediaItem
}
type MediaItem struct {
gorm.Model
User User
User User `gorm:"ForeignKey:ID"`
EXIFDate time.Time
Latitude string
Longitude string

View File

@ -1,8 +1,8 @@
package sessions
import (
"github.com/dgrijalva/jwt-go"
)
// import (
// "github.com/dgrijalva/jwt-go"
// )
type Manager struct {

View File

@ -3,9 +3,11 @@ package main
import (
"os"
"log"
"github.com/urfave/cli/v2"
"reichard.io/imagini/cmd"
"reichard.io/imagini/internal/sessions"
"github.com/urfave/cli/v2"
)
var globalSessions *sessions.Manager
@ -16,7 +18,6 @@ func main() {
Usage: "A self hosted photo library.",
Commands: []*cli.Command{
&cmd.CmdServe,
&cmd.CmdDBTest,
},
}

View File

@ -6,31 +6,32 @@ import (
"os"
)
type Middleware func(http.HandlerFunc) http.HandlerFunc
type Middleware func(http.Handler) http.Handler
func MultipleMiddleware(h http.HandlerFunc, m ...Middleware) http.HandlerFunc {
func MultipleMiddleware(h http.Handler, m ...Middleware) http.Handler {
if len(m) < 1 {
return h
}
wrapped := h
for i := len(m) - 1; i >= 0; i-- {
wrapped = m[i](wrapped)
}
return wrapped
}
func authMiddleware(h http.HandlerFunc) http.HandlerFunc {
func authMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.SetOutput(os.Stdout)
log.Println(r.Method, r.URL)
h.ServeHTTP(w, r)
_, ok := ValidateUserToken(r)
if ok {
next.ServeHTTP(w, r)
} else {
w.WriteHeader(http.StatusUnauthorized)
}
})
}
func logMiddleware(h http.HandlerFunc) http.HandlerFunc {
func logMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.SetOutput(os.Stdout)
log.Println(r.Method, r.URL)

View File

@ -3,27 +3,55 @@ package routes
import (
"net/http"
"fmt"
"reichard.io/imagini/internal/db"
// "reichard.io/imagini/internal/db"
"github.com/tus/tusd/pkg/filestore"
tusd "github.com/tus/tusd/pkg/handler"
)
func RegisterRoutes() {
http.HandleFunc("/Users", usersHandler)
http.HandleFunc("/Tags", tagsHandler)
commonMiddleware := []Middleware{
logMiddleware,
authMiddleware,
}
http.Handle("/Users", MultipleMiddleware(usersHandler, commonMiddleware...))
http.Handle("/Uploads/", MultipleMiddleware(uploadsHandler, commonMiddleware...))
// Uploads Handler
http.Handle("/uploads/", uploadsHandler())
// http.HandleFunc("/uploads/", uploadsHandler())
http.Handle("/Uploads/", func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, ok := ValidateUserToken(r)
if ok {
next.ServeHTTP(w, r)
} else {
w.WriteHeader(http.StatusUnauthorized)
}
})
}(http.StripPrefix("/Uploads/", tusHandler)))
}
func tagsHandler(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
filters, present := query["filters"]
// func tagsHandler(w http.ResponseWriter, r *http.Request) {
// query := r.URL.Query()
// filters, present := query["filters"]
// }
func helloHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/hello" {
http.Error(w, "404 not found.", http.StatusNotFound)
return
}
if r.Method != "GET" {
http.Error(w, "Method is not supported.", http.StatusNotFound)
return
}
fmt.Fprintf(w, "Hello!")
}
func uploadsHandler() http.Handler {
store := filestore.FileStore{
Path: "./uploads",
Path: "./Uploads",
}
composer := tusd.NewStoreComposer()
store.UseIn(composer)
@ -45,12 +73,16 @@ func uploadsHandler() http.Handler {
}
}()
return http.StripPrefix("/uploads/", handler)
// return func(w http.ResponseWriter, r *http.Request) {
// http.StripPrefix("/Uploads/", handler).ServeHTTP(w, r)
// };
return http.StripPrefix("/Uploads/", handler)
}
func processMedia() {
var mi db.MediaItem
// func processMedia() {
// var mi db.MediaItem
//
// TODO:
// - Derive Magic -> mediaType
// - Create Thumbnail
@ -75,23 +107,10 @@ func processMedia() {
//
// img = imaging.Fit(img, 240, 160, imaging.Lanczos)
// err = imaging.Save(img, "thumbnail.jpg")
}
// }
func helloHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/hello" {
http.Error(w, "404 not found.", http.StatusNotFound)
return
}
if r.Method != "GET" {
http.Error(w, "Method is not supported.", http.StatusNotFound)
return
}
fmt.Fprintf(w, "Hello!")
}

View File

@ -1,113 +0,0 @@
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

View File

@ -1,8 +1,10 @@
import 'package:flutter/widgets.dart';
import 'package:imagini/screens/example1/examplescreen1.dart';
import 'package:imagini/screens/photo/photoscreen.dart';
import 'package:imagini/screens/upload/uploadscreen.dart';
import 'package:imagini/screens/example2/examplescreen2.dart';
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
"/": (BuildContext context) => ExScreen1(),
"/": (BuildContext context) => PhotoScreen(),
"/Upload": (BuildContext context) => UploadScreen(),
"/ExScreen2": (BuildContext context) => ExScreen2(),
};

View File

@ -1,40 +0,0 @@
import 'package:flutter/material.dart';
import 'package:imagini/screens/example1/components/body.dart';
import 'package:imagini/screens/example1/example-bloc.dart';
import 'package:imagini/bloc/bloc-prov.dart';
class ExScreen1 extends StatefulWidget {
@override
_ExScreen1State createState() => _ExScreen1State();
}
class _ExScreen1State extends State<ExScreen1> {
ExampleBloc exampleBloc;
@override
void initState() {
super.initState();
exampleBloc = ExampleBloc();
}
@override
void dispose() {
exampleBloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider(
bloc: exampleBloc,
child: Scaffold(
appBar: AppBar(
title: Text("First Screen"),
),
body: Body(),
),
);
}
}

View File

@ -0,0 +1,20 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:imagini/models/contact.dart';
import 'package:flutter/material.dart';
import 'package:imagini/bloc/bloc.dart';
class Example2Bloc extends Bloc {
StreamSubscription _audioPlayerStateSubscription;
Stream<String> get example => _exampleSubject.stream;
Sink<String> get exampleSink => _exampleSubject.sink;
final StreamController<String> _exampleSubject = StreamController<String>();
Example2Bloc();
void dispose() {
_exampleSubject.close();
}
}

View File

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:imagini/screens/example2/components/body.dart';
import 'package:imagini/screens/example2/example2-bloc.dart';
import 'package:imagini/bloc/bloc-prov.dart';
class ExScreen2 extends StatefulWidget {
@override
_ExScreen2State createState() => _ExScreen2State();
}
class _ExScreen2State extends State<ExScreen2> {
Example2Bloc example2Bloc;
@override
void initState() {
super.initState();
example2Bloc = Example2Bloc();
}
@override
void dispose() {
example2Bloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider(
bloc: Example2Bloc(),
child: Scaffold(
appBar: AppBar(
title: Text("Second Screen"),
),
body: Body(),
),
);
}
}

View File

@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class Body extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
onPressed: () {
Navigator.pushNamed(context, '/Upload');
},
child: Text('Page Two!'),
),
);
}
}

View File

@ -5,14 +5,14 @@ import 'package:imagini/models/contact.dart';
import 'package:flutter/material.dart';
import 'package:imagini/bloc/bloc.dart';
class ExampleBloc extends Bloc {
class PhotoBloc extends Bloc {
StreamSubscription _audioPlayerStateSubscription;
Stream<String> get example => _exampleSubject.stream;
Sink<String> get exampleSink => _exampleSubject.sink;
final StreamController<String> _exampleSubject = StreamController<String>();
ExampleBloc();
PhotoBloc();
void dispose() {
_exampleSubject.close();

View File

@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import 'package:imagini/screens/photo/components/body.dart';
import 'package:imagini/screens/photo/photo-bloc.dart';
import 'package:imagini/bloc/bloc-prov.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
class PhotoScreen extends StatefulWidget {
@override
_PhotoScreenState createState() => _PhotoScreenState();
}
class _PhotoScreenState extends State<PhotoScreen> {
PhotoBloc exampleBloc;
@override
void initState() {
super.initState();
exampleBloc = PhotoBloc();
}
@override
void dispose() {
exampleBloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider(
bloc: exampleBloc,
child: PlatformScaffold(
appBar: PlatformAppBar(
title: Text('Photos'),
cupertino: (_, __) => CupertinoNavigationBarData(
// Issue with cupertino where a bar with no transparency
// will push the list down. Adding some alpha value fixes it (in a hacky way)
backgroundColor: Colors.lightGreen.withAlpha(254),
),
),
body: Body(),
),
);
}
}

View File

@ -0,0 +1,20 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:imagini/models/contact.dart';
import 'package:flutter/material.dart';
import 'package:imagini/bloc/bloc.dart';
class UploadBloc extends Bloc {
StreamSubscription _audioPlayerStateSubscription;
Stream<String> get example => _exampleSubject.stream;
Sink<String> get exampleSink => _exampleSubject.sink;
final StreamController<String> _exampleSubject = StreamController<String>();
UploadBloc();
void dispose() {
_exampleSubject.close();
}
}

View File

@ -0,0 +1,197 @@
import 'package:cross_file/cross_file.dart' show XFile;
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:imagini/bloc/bloc-prov.dart';
import 'package:imagini/screens/upload/upload-bloc.dart';
import 'package:tus_client/tus_client.dart';
import 'package:url_launcher/url_launcher.dart';
class UploadScreen extends StatefulWidget {
@override
_UploadScreenState createState() => _UploadScreenState();
}
class _UploadScreenState extends State<UploadScreen> {
UploadBloc exampleBloc;
double _progress = 0;
XFile _file;
TusClient _client;
Uri _fileUrl;
@override
void initState() {
super.initState();
exampleBloc = UploadBloc();
}
@override
void dispose() {
exampleBloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider(
bloc: exampleBloc,
child: PlatformScaffold(
appBar: PlatformAppBar(
title: Text('Uploads'),
cupertino: (_, __) => CupertinoNavigationBarData(
// Issue with cupertino where a bar with no transparency
// will push the list down. Adding some alpha value fixes it (in a hacky way)
backgroundColor: Colors.lightGreen.withAlpha(254),
),
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
SizedBox(height: 12),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
child: Text(
"This demo uses TUS client to upload a file",
style: TextStyle(fontSize: 18),
),
),
Padding(
padding: const EdgeInsets.all(6),
child: Card(
color: Colors.teal,
child: InkWell(
onTap: () async {
_file =
await _getXFile(await FilePicker.platform.pickFiles());
setState(() {
_progress = 0;
_fileUrl = null;
});
},
child: Container(
padding: EdgeInsets.all(20),
child: Column(
children: <Widget>[
Icon(Icons.cloud_upload, color: Colors.white, size: 60),
Text(
"Upload a file",
style: TextStyle(fontSize: 25, color: Colors.white),
),
],
),
),
),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: <Widget>[
Expanded(
child: RaisedButton(
onPressed: _file == null
? null
: () async {
// Create a client
print("Create a client");
_client = TusClient(
Uri.parse("https://master.tus.io/files/"),
_file,
store: TusMemoryStore(),
);
print("Starting upload");
await _client.upload(
onComplete: () async {
print("Completed!");
setState(() => _fileUrl = _client.uploadUrl);
},
onProgress: (progress) {
print("Progress: $progress");
setState(() => _progress = progress);
},
);
},
child: Text("Upload"),
),
),
SizedBox(width: 8),
Expanded(
child: RaisedButton(
onPressed: _progress == 0
? null
: () async {
_client.pause();
},
child: Text("Pause"),
),
),
],
),
),
Stack(
children: <Widget>[
Container(
margin: const EdgeInsets.all(8),
padding: const EdgeInsets.all(1),
color: Colors.grey,
width: double.infinity,
child: Text(" "),
),
FractionallySizedBox(
widthFactor: _progress / 100,
child: Container(
margin: const EdgeInsets.all(8),
padding: const EdgeInsets.all(1),
color: Colors.green,
child: Text(" "),
),
),
Container(
margin: const EdgeInsets.all(8),
padding: const EdgeInsets.all(1),
width: double.infinity,
child: Text("Progress: ${_progress.toStringAsFixed(1)}%"),
),
],
),
GestureDetector(
onTap: _progress != 100
? null
: () async {
await launch(_fileUrl.toString());
},
child: Container(
color: _progress == 100 ? Colors.green : Colors.grey,
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(8.0),
child:
Text(_progress == 100 ? "Link to view:\n $_fileUrl" : "-"),
),
),
],
),
),
),
);
}
Future<XFile> _getXFile(FilePickerResult result) async {
if (result != null) {
final chosenFile = result.files.first;
if (chosenFile.path != null) {
// Android, iOS, Desktop
return XFile(chosenFile.path);
} else {
// Web
return XFile.fromData(
chosenFile.bytes,
name: chosenFile.name,
);
}
}
return null;
}
}

View File

@ -7,7 +7,7 @@ ThemeData appTheme() {
hintColor: Colors.white,
dividerColor: Colors.white,
buttonColor: Colors.white,
scaffoldBackgroundColor: Colors.black,
canvasColor: Colors.black,
scaffoldBackgroundColor: Colors.blue,
canvasColor: Colors.blue,
);
}

View File

@ -92,6 +92,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.14.2"
cross_file:
dependency: transitive
description:
name: cross_file
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0"
crypto:
dependency: transitive
description:
@ -120,6 +127,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.0-nullsafety.4"
file_picker:
dependency: "direct main"
description:
name: file_picker
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.5"
flutter:
dependency: "direct main"
description: flutter
@ -130,11 +144,30 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_platform_widgets:
dependency: "direct main"
description:
name: flutter_platform_widgets
url: "https://pub.dartlang.org"
source: hosted
version: "0.72.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.11"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
@ -147,6 +180,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
http:
dependency: transitive
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.2"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.4"
integration_test:
dependency: "direct dev"
description: flutter
@ -236,6 +283,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0-nullsafety.4"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
pool:
dependency: transitive
description:
@ -332,6 +386,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.12-nullsafety.9"
tus_client:
dependency: "direct main"
description:
name: tus_client
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.1"
typed_data:
dependency: transitive
description:
@ -339,6 +400,48 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.5"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "5.7.10"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.1+4"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.1+9"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.9"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.5+1"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.1+3"
vector_math:
dependency: transitive
description:
@ -383,3 +486,4 @@ packages:
version: "2.2.1"
sdks:
dart: ">=2.12.0-0.0 <3.0.0"
flutter: ">=1.22.0 <2.0.0"

View File

@ -23,10 +23,10 @@ environment:
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
flutter_platform_widgets: ^0.72.0
tus_client: ^0.1.1
file_picker: ^2.1.5
url_launcher: ^5.7.10
cupertino_icons: ^1.0.1
dev_dependencies: