From 04924ead5c3cd88d1169daa5c9959a63819b6369 Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Thu, 7 Jan 2021 21:45:59 -0500 Subject: [PATCH] WIP --- cmd/cmd.go | 24 ++- go.mod | 5 +- go.sum | 3 + imagini.db | Bin 0 -> 61440 bytes internal/auth/ldap.go | 0 internal/auth/local.go | 0 internal/config/config.go | 35 ++-- internal/config/settings.go | 32 --- internal/db/db.go | 139 +++++++++--- internal/db/models.go | 3 +- internal/sessions/sessions.go | 6 +- main.go | 5 +- routes/middlewares.go | 21 +- routes/routes.go | 73 ++++--- web_native/lib/main.dart.bak | 113 ---------- web_native/lib/routes.dart | 6 +- .../lib/screens/example1/examplescreen1.dart | 40 ---- .../{example1 => login}/components/body.dart | 0 web_native/lib/screens/login/login-bloc.dart | 20 ++ web_native/lib/screens/login/loginscreen.dart | 40 ++++ .../lib/screens/photo/components/body.dart | 15 ++ .../photo-bloc.dart} | 4 +- web_native/lib/screens/photo/photoscreen.dart | 45 ++++ .../lib/screens/upload/upload-bloc.dart | 20 ++ .../lib/screens/upload/uploadscreen.dart | 197 ++++++++++++++++++ web_native/lib/theme/style.dart | 6 +- web_native/pubspec.lock | 104 +++++++++ web_native/pubspec.yaml | 8 +- 28 files changed, 676 insertions(+), 288 deletions(-) create mode 100644 imagini.db create mode 100644 internal/auth/ldap.go create mode 100644 internal/auth/local.go delete mode 100644 internal/config/settings.go delete mode 100644 web_native/lib/main.dart.bak delete mode 100644 web_native/lib/screens/example1/examplescreen1.dart rename web_native/lib/screens/{example1 => login}/components/body.dart (100%) create mode 100644 web_native/lib/screens/login/login-bloc.dart create mode 100644 web_native/lib/screens/login/loginscreen.dart create mode 100644 web_native/lib/screens/photo/components/body.dart rename web_native/lib/screens/{example1/example-bloc.dart => photo/photo-bloc.dart} (91%) create mode 100644 web_native/lib/screens/photo/photoscreen.dart create mode 100644 web_native/lib/screens/upload/upload-bloc.dart create mode 100644 web_native/lib/screens/upload/uploadscreen.dart diff --git a/cmd/cmd.go b/cmd/cmd.go index 6dd613c..9c568de 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -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 { diff --git a/go.mod b/go.mod index 6ace580..8af0f47 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index bbbbcdd..96f9dd4 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/imagini.db b/imagini.db new file mode 100644 index 0000000000000000000000000000000000000000..858c14376c196c116fc048f186395059818c9627 GIT binary patch literal 61440 zcmeI*-*4MQ00(e8PV6RYbCXaoeE_){6RXyir1{Zm0%={BjsD!06^ROo*lpa_TYi+p z9UWAf(p4HSNIW3%fAD~Wc;J;kU@u5KAb|uA10*!W13Yl&a}p}2=mjYZ2gwT()(WY}6#I4y{xa8c6)K_Ko*H_kFhKZEW*+lZe1Ub{q?Som}( zvL{64djfei`uWgTLvIg;Pkj=36ge39Ju*A6qCA%GDc|%;0Ouh90SG|gL<;P`Ii#o~ zBjQ2UHr9$}$t+k#-ZnOCt-a`ME<3Z5)mCOMEoL>n6`*V9+#~u8R?1riU9-xzxoK9% zbecrliR-z!rJ3BSwvb)zx~ds_bZqw8^39dp%-r&drf=NNxAJm#Sw^~cz5rzjR7bV=;061*cM347LuoSEO?xt&FyMTd$f1FN3ZX=$>tr%rSju zo9(lbdCz*X^u#S2B~#aIbJreguXLtAx_=|AsAtcL2d1aJ8O61olH20=ex$vq+>QtN zwK^xp6STW#{2tAM=lGP{o|}oY>1=eR4U4>#MR8@*M-{6x6BO?JiPt&*SaY?rSniaIX|d*!K$ZcI2n7ck=vO( zA*@^UxhI>u)<&Lnd~c^O8n$Kc6dGMzFILK%Z6|n{?7Q1EO7rWWY8La`hP~BuU^sl_ z=k7Z-v)b7)*kIDuC0L~9sJs?b)U+zrL(O`0f<3k29EZB=t}`C5wMOEt{y+!WcO^wt zRq=kzsV^sr^AT*XBYtiFYsWLr>SoEXimt6rzkasz+}|06^_o$n<2K*lGHP3NAm9EV z?>T$OI9-nf6m{;rSRZc^Qs6sgHD5Dr+bVC?JeVCJQa5Wy49~S3xFfuyYt&aoMLmCB z+_xO+j>sK{fp-3m(5KK%ycJO}YwJ~O+qNp@hWH($xYOt@hd>Xp0r)igzybjXKmY;| zfB*y_009U<00Izzz)2N|up7hg|C3t1xGxAm00Izz00bZa0SG_<0uX?JBf#eWrRdiJ z`HZ|qH&`G50SG_<0uX=z1Rwwb2tWV=&zit3DIzQ_39<25EH)XROpM3rzbS1paUnT< zA(fg)OlQ)`squLFLOgy{_$8}YT%+qnbJr+s7tM+FN@?-(%=Kg{onh&2>Rt#29C!h}^} zHSQPUT1p~x3)L;`0T!4?uk^&H}@mL{c`_^PxmkMzV~!`$~X5T!;@)V`0iWB z{QtAtDNr&9KmY;|fB*y_009U<00Izzz<(F$p8uDlae;h63gpG;x6wU%fCT~&fB*y_ z009U<00Izz00f?Ufq7XL78Zoz9+UL`^VFF}@C^Hlt5fr#zNM8|49e_f0KLjJIqlAX z_b7KW+$mXVQR-uA$;E(QYUwHe7YDS$ow7|XihWEi!RG&E@}v9wKlzpXLJzP&00Izz z00bZa0SG_<0uX=z1R!wS1lWYX%sxWwBY4RCKlx4|f0IASZ{%n413kb30SG_<0uX=z z1Rwwb2tWV=5P-n(5D3YVC`xiD#5N)N(_a(=!H~o@QZOKe=|+^oVLt!=kp2FDOdgS+ z$oJ&o@wh>h5&{r_00bZa0SG_<0uX=z1R(Hy3n)R6{q&dlHWYHUfnbntB}op3>Hh-I z6XCG){(m|8hCsd~MWRI?Mn9$pSReoa2tWV=5P$##AOHafKmYG{u?qnRKmY;|fB*y_009U<00Iy=#sYZ${}@*uiVgt? zKmY;|fB*y_009U<00I#36~OcVzB~wo00bZa0SG_<0uX=z1Rwwb2po5TCwcz=U!y1P A5C8xG literal 0 HcmV?d00001 diff --git a/internal/auth/ldap.go b/internal/auth/ldap.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/auth/local.go b/internal/auth/local.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/config/config.go b/internal/config/config.go index b6b969a..b734947 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 +} diff --git a/internal/config/settings.go b/internal/config/settings.go deleted file mode 100644 index 7043db7..0000000 --- a/internal/config/settings.go +++ /dev/null @@ -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 -} diff --git a/internal/db/db.go b/internal/db/db.go index 1049dac..eccd9ad 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -1,40 +1,125 @@ package db import ( - "gorm.io/driver/sqlite" - "gorm.io/gorm" + "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{ - PrepareStmt: true, - }) +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) } diff --git a/internal/db/models.go b/internal/db/models.go index ae20756..007fca5 100644 --- a/internal/db/models.go +++ b/internal/db/models.go @@ -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 diff --git a/internal/sessions/sessions.go b/internal/sessions/sessions.go index b3def17..276d64e 100644 --- a/internal/sessions/sessions.go +++ b/internal/sessions/sessions.go @@ -1,8 +1,8 @@ package sessions -import ( - "github.com/dgrijalva/jwt-go" -) +// import ( +// "github.com/dgrijalva/jwt-go" +// ) type Manager struct { diff --git a/main.go b/main.go index 90dbdc8..eea1bef 100644 --- a/main.go +++ b/main.go @@ -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, }, } diff --git a/routes/middlewares.go b/routes/middlewares.go index ef03e41..cf11ad1 100644 --- a/routes/middlewares.go +++ b/routes/middlewares.go @@ -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) diff --git a/routes/routes.go b/routes/routes.go index da3183c..618e557 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -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!") -} diff --git a/web_native/lib/main.dart.bak b/web_native/lib/main.dart.bak deleted file mode 100644 index d8a0526..0000000 --- a/web_native/lib/main.dart.bak +++ /dev/null @@ -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 { - 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: [ - 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. - ); - } -} diff --git a/web_native/lib/routes.dart b/web_native/lib/routes.dart index acefa2c..2bf44bb 100644 --- a/web_native/lib/routes.dart +++ b/web_native/lib/routes.dart @@ -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 routes = { - "/": (BuildContext context) => ExScreen1(), + "/": (BuildContext context) => PhotoScreen(), + "/Upload": (BuildContext context) => UploadScreen(), "/ExScreen2": (BuildContext context) => ExScreen2(), }; diff --git a/web_native/lib/screens/example1/examplescreen1.dart b/web_native/lib/screens/example1/examplescreen1.dart deleted file mode 100644 index f95be1e..0000000 --- a/web_native/lib/screens/example1/examplescreen1.dart +++ /dev/null @@ -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 { - 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(), - ), - ); - } -} diff --git a/web_native/lib/screens/example1/components/body.dart b/web_native/lib/screens/login/components/body.dart similarity index 100% rename from web_native/lib/screens/example1/components/body.dart rename to web_native/lib/screens/login/components/body.dart diff --git a/web_native/lib/screens/login/login-bloc.dart b/web_native/lib/screens/login/login-bloc.dart new file mode 100644 index 0000000..a20c51f --- /dev/null +++ b/web_native/lib/screens/login/login-bloc.dart @@ -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 get example => _exampleSubject.stream; + Sink get exampleSink => _exampleSubject.sink; + final StreamController _exampleSubject = StreamController(); + + Example2Bloc(); + + void dispose() { + _exampleSubject.close(); + } +} diff --git a/web_native/lib/screens/login/loginscreen.dart b/web_native/lib/screens/login/loginscreen.dart new file mode 100644 index 0000000..fafe7dd --- /dev/null +++ b/web_native/lib/screens/login/loginscreen.dart @@ -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 { + 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(), + ), + ); + } +} diff --git a/web_native/lib/screens/photo/components/body.dart b/web_native/lib/screens/photo/components/body.dart new file mode 100644 index 0000000..51d2aed --- /dev/null +++ b/web_native/lib/screens/photo/components/body.dart @@ -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!'), + ), + ); + } +} diff --git a/web_native/lib/screens/example1/example-bloc.dart b/web_native/lib/screens/photo/photo-bloc.dart similarity index 91% rename from web_native/lib/screens/example1/example-bloc.dart rename to web_native/lib/screens/photo/photo-bloc.dart index ec695c4..f9e41cb 100644 --- a/web_native/lib/screens/example1/example-bloc.dart +++ b/web_native/lib/screens/photo/photo-bloc.dart @@ -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 get example => _exampleSubject.stream; Sink get exampleSink => _exampleSubject.sink; final StreamController _exampleSubject = StreamController(); - ExampleBloc(); + PhotoBloc(); void dispose() { _exampleSubject.close(); diff --git a/web_native/lib/screens/photo/photoscreen.dart b/web_native/lib/screens/photo/photoscreen.dart new file mode 100644 index 0000000..234f992 --- /dev/null +++ b/web_native/lib/screens/photo/photoscreen.dart @@ -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 { + 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(), + ), + ); + } +} diff --git a/web_native/lib/screens/upload/upload-bloc.dart b/web_native/lib/screens/upload/upload-bloc.dart new file mode 100644 index 0000000..fcc97e2 --- /dev/null +++ b/web_native/lib/screens/upload/upload-bloc.dart @@ -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 get example => _exampleSubject.stream; + Sink get exampleSink => _exampleSubject.sink; + final StreamController _exampleSubject = StreamController(); + + UploadBloc(); + + void dispose() { + _exampleSubject.close(); + } +} diff --git a/web_native/lib/screens/upload/uploadscreen.dart b/web_native/lib/screens/upload/uploadscreen.dart new file mode 100644 index 0000000..dca1c1b --- /dev/null +++ b/web_native/lib/screens/upload/uploadscreen.dart @@ -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 { + 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: [ + 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: [ + 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: [ + 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: [ + 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 _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; + } +} diff --git a/web_native/lib/theme/style.dart b/web_native/lib/theme/style.dart index a4fe749..1923240 100644 --- a/web_native/lib/theme/style.dart +++ b/web_native/lib/theme/style.dart @@ -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, ); -} \ No newline at end of file +} diff --git a/web_native/pubspec.lock b/web_native/pubspec.lock index 62bef60..04d80f9 100644 --- a/web_native/pubspec.lock +++ b/web_native/pubspec.lock @@ -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" diff --git a/web_native/pubspec.yaml b/web_native/pubspec.yaml index 469cbbb..e9ce0d2 100644 --- a/web_native/pubspec.yaml +++ b/web_native/pubspec.yaml @@ -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: