Merge pull request #209 from eikek/qr-code

Qr code
This commit is contained in:
mergify[bot] 2020-08-03 22:46:10 +00:00 committed by GitHub
commit f1e776ae3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 287 additions and 128 deletions

View File

@ -616,9 +616,10 @@ def compileElm(
def createWebjarSource(wj: Seq[ModuleID], out: File): Seq[File] = { def createWebjarSource(wj: Seq[ModuleID], out: File): Seq[File] = {
val target = out / "Webjars.scala" val target = out / "Webjars.scala"
val badChars = "-.".toSet
val fields = wj val fields = wj
.map(m => .map(m =>
s"""val ${m.name.toLowerCase.filter(_ != '-')} = "/${m.name}/${m.revision}" """ s"""val ${m.name.toLowerCase.filter(c => !badChars.contains(c))} = "/${m.name}/${m.revision}" """
) )
.mkString("\n\n") .mkString("\n\n")
val content = s"""package docspell.restserver.webapp val content = s"""package docspell.restserver.webapp

View File

@ -21,15 +21,22 @@
"elm-explorations/markdown": "1.0.0", "elm-explorations/markdown": "1.0.0",
"justinmimbs/date": "3.1.2", "justinmimbs/date": "3.1.2",
"norpan/elm-html5-drag-drop": "3.1.4", "norpan/elm-html5-drag-drop": "3.1.4",
"pablohirafuji/elm-qrcode": "3.3.1",
"ryannhg/date-format": "2.3.0", "ryannhg/date-format": "2.3.0",
"truqu/elm-base64": "2.0.4", "truqu/elm-base64": "2.0.4",
"ursi/elm-throttle": "1.0.1" "ursi/elm-throttle": "1.0.1"
}, },
"indirect": { "indirect": {
"avh4/elm-color": "1.0.0",
"danfishgold/base64-bytes": "1.0.3",
"elm/bytes": "1.0.8", "elm/bytes": "1.0.8",
"elm/parser": "1.1.0", "elm/parser": "1.1.0",
"elm/regex": "1.0.0", "elm/regex": "1.0.0",
"elm/virtual-dom": "1.0.2" "elm/svg": "1.0.1",
"elm/virtual-dom": "1.0.2",
"elm-community/list-extra": "8.2.4",
"folkertdev/elm-flate": "2.0.4",
"justgook/elm-image": "4.0.0"
} }
}, },
"test-dependencies": { "test-dependencies": {

View File

@ -126,6 +126,7 @@ object TemplateRoutes {
Seq( Seq(
"/app/assets" + Webjars.jquery + "/jquery.min.js", "/app/assets" + Webjars.jquery + "/jquery.min.js",
"/app/assets" + Webjars.semanticui + "/semantic.min.js", "/app/assets" + Webjars.semanticui + "/semantic.min.js",
"/app/assets" + Webjars.clipboardjs + "/clipboard.min.js",
s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell-app.js" s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell-app.js"
), ),
s"/app/assets/docspell-webapp/${BuildInfo.version}/favicon", s"/app/assets/docspell-webapp/${BuildInfo.version}/favicon",

View File

@ -97,6 +97,10 @@ type Msg
| FolderDropdownMsg (Comp.Dropdown.Msg IdName) | FolderDropdownMsg (Comp.Dropdown.Msg IdName)
--- Update
update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
update flags msg model = update flags msg model =
case msg of case msg of
@ -223,6 +227,10 @@ update flags msg model =
( model_, Cmd.map FolderDropdownMsg c2 ) ( model_, Cmd.map FolderDropdownMsg c2 )
--- View
view : Flags -> UiSettings -> Model -> Html Msg view : Flags -> UiSettings -> Model -> Html Msg
view flags settings model = view flags settings model =
let let
@ -293,43 +301,6 @@ disappear then.
""" """
] ]
] ]
, urlInfoMessage flags model
]
urlInfoMessage : Flags -> Model -> Html Msg
urlInfoMessage flags model =
div
[ classList
[ ( "ui info icon message", True )
, ( "hidden", not model.enabled || model.source.id == "" )
]
]
[ i [ class "info icon" ] []
, div [ class "content" ]
[ div [ class "header" ]
[ text "Public Uploads"
]
, p []
[ text "This source defines URLs that can be used by anyone to send files to "
, text "you. There is a web page that you can share or the API url can be used "
, text "with other clients."
]
, dl [ class "ui list" ]
[ dt [] [ text "Public Upload Page" ]
, dd []
[ let
url =
flags.config.baseUrl ++ "/app/upload/" ++ model.source.id
in
a [ href url, target "_blank" ] [ code [] [ text url ] ]
]
, dt [] [ text "Public API Upload URL" ]
, dd []
[ code [] [ text (flags.config.baseUrl ++ "/api/v1/open/upload/item/" ++ model.source.id) ]
]
]
]
] ]

View File

@ -8,10 +8,10 @@ module Comp.SourceManage exposing
import Api import Api
import Api.Model.BasicResult exposing (BasicResult) import Api.Model.BasicResult exposing (BasicResult)
import Api.Model.Source import Api.Model.Source exposing (Source)
import Api.Model.SourceList exposing (SourceList) import Api.Model.SourceList exposing (SourceList)
import Comp.SourceForm import Comp.SourceForm
import Comp.SourceTable import Comp.SourceTable exposing (SelectMode(..))
import Comp.YesNoDimmer import Comp.YesNoDimmer
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
@ -19,53 +19,68 @@ import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Html.Events exposing (onClick, onSubmit) import Html.Events exposing (onClick, onSubmit)
import Http import Http
import Ports
import QRCode
import Util.Http import Util.Http
import Util.Maybe import Util.Maybe
type alias Model = type alias Model =
{ tableModel : Comp.SourceTable.Model { formModel : Comp.SourceForm.Model
, formModel : Comp.SourceForm.Model , viewMode : SelectMode
, viewMode : ViewMode
, formError : Maybe String , formError : Maybe String
, loading : Bool , loading : Bool
, deleteConfirm : Comp.YesNoDimmer.Model , deleteConfirm : Comp.YesNoDimmer.Model
, sources : List Source
} }
type ViewMode
= Table
| Form
init : Flags -> ( Model, Cmd Msg ) init : Flags -> ( Model, Cmd Msg )
init flags = init flags =
let let
( fm, fc ) = ( fm, fc ) =
Comp.SourceForm.init flags Comp.SourceForm.init flags
in in
( { tableModel = Comp.SourceTable.emptyModel ( { formModel = fm
, formModel = fm , viewMode = None
, viewMode = Table
, formError = Nothing , formError = Nothing
, loading = False , loading = False
, deleteConfirm = Comp.YesNoDimmer.emptyModel , deleteConfirm = Comp.YesNoDimmer.emptyModel
, sources = []
} }
, Cmd.map FormMsg fc , Cmd.batch
[ Cmd.map FormMsg fc
, Ports.initClipboard appClipboardData
, Ports.initClipboard apiClipboardData
]
) )
appClipboardData : ( String, String )
appClipboardData =
( "app-url", "#app-url-copy-to-clipboard-btn" )
apiClipboardData : ( String, String )
apiClipboardData =
( "api-url", "#api-url-copy-to-clipboard-btn" )
type Msg type Msg
= TableMsg Comp.SourceTable.Msg = TableMsg Comp.SourceTable.Msg
| FormMsg Comp.SourceForm.Msg | FormMsg Comp.SourceForm.Msg
| LoadSources | LoadSources
| SourceResp (Result Http.Error SourceList) | SourceResp (Result Http.Error SourceList)
| SetViewMode ViewMode
| InitNewSource | InitNewSource
| Submit | Submit
| SubmitResp (Result Http.Error BasicResult) | SubmitResp (Result Http.Error BasicResult)
| YesNoMsg Comp.YesNoDimmer.Msg | YesNoMsg Comp.YesNoDimmer.Msg
| RequestDelete | RequestDelete
| SetTableView
--- Update
update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
@ -73,29 +88,31 @@ update flags msg model =
case msg of case msg of
TableMsg m -> TableMsg m ->
let let
( tm, tc ) = ( tc, sel ) =
Comp.SourceTable.update flags m model.tableModel Comp.SourceTable.update flags m
( m2, c2 ) = ( m2, c2 ) =
( { model ( { model
| tableModel = tm | viewMode = sel
, viewMode = Maybe.map (\_ -> Form) tm.selected |> Maybe.withDefault Table
, formError = , formError =
if Util.Maybe.nonEmpty tm.selected then if Comp.SourceTable.isEdit sel then
Nothing model.formError
else else
model.formError Nothing
} }
, Cmd.map TableMsg tc , Cmd.map TableMsg tc
) )
( m3, c3 ) = ( m3, c3 ) =
case tm.selected of case sel of
Just source -> Edit source ->
update flags (FormMsg (Comp.SourceForm.SetSource source)) m2 update flags (FormMsg (Comp.SourceForm.SetSource source)) m2
Nothing -> Display _ ->
( m2, Cmd.none )
None ->
( m2, Cmd.none ) ( m2, Cmd.none )
in in
( m3, Cmd.batch [ c2, c3 ] ) ( m3, Cmd.batch [ c2, c3 ] )
@ -111,34 +128,27 @@ update flags msg model =
( { model | loading = True }, Api.getSources flags SourceResp ) ( { model | loading = True }, Api.getSources flags SourceResp )
SourceResp (Ok sources) -> SourceResp (Ok sources) ->
let ( { model
m2 = | viewMode = None
{ model | viewMode = Table, loading = False } , loading = False
in , sources = sources.items
update flags (TableMsg (Comp.SourceTable.SetSources sources.items)) m2 }
, Cmd.none
)
SourceResp (Err _) -> SourceResp (Err _) ->
( { model | loading = False }, Cmd.none ) ( { model | loading = False }, Cmd.none )
SetViewMode m -> SetTableView ->
let ( { model | viewMode = None }, Cmd.none )
m2 =
{ model | viewMode = m }
in
case m of
Table ->
update flags (TableMsg Comp.SourceTable.Deselect) m2
Form ->
( m2, Cmd.none )
InitNewSource -> InitNewSource ->
let let
nm =
{ model | viewMode = Form, formError = Nothing }
source = source =
Api.Model.Source.empty Api.Model.Source.empty
nm =
{ model | viewMode = Edit source, formError = Nothing }
in in
update flags (FormMsg (Comp.SourceForm.SetSource source)) nm update flags (FormMsg (Comp.SourceForm.SetSource source)) nm
@ -160,7 +170,7 @@ update flags msg model =
if res.success then if res.success then
let let
( m2, c2 ) = ( m2, c2 ) =
update flags (SetViewMode Table) model update flags SetTableView model
( m3, c3 ) = ( m3, c3 ) =
update flags LoadSources m2 update flags LoadSources m2
@ -194,13 +204,29 @@ update flags msg model =
( { model | deleteConfirm = cm }, cmd ) ( { model | deleteConfirm = cm }, cmd )
--- View
qrCodeView : String -> Html msg
qrCodeView message =
QRCode.encode message
|> Result.map QRCode.toSvg
|> Result.withDefault
(Html.text "Error generating QR-Code")
view : Flags -> UiSettings -> Model -> Html Msg view : Flags -> UiSettings -> Model -> Html Msg
view flags settings model = view flags settings model =
if model.viewMode == Table then case model.viewMode of
viewTable model None ->
viewTable model
else Edit _ ->
div [] (viewForm flags settings model) div [] (viewForm flags settings model)
Display source ->
viewLinks flags settings source
viewTable : Model -> Html Msg viewTable : Model -> Html Msg
@ -210,7 +236,7 @@ viewTable model =
[ i [ class "plus icon" ] [] [ i [ class "plus icon" ] []
, text "Create new" , text "Create new"
] ]
, Html.map TableMsg (Comp.SourceTable.view model.tableModel) , Html.map TableMsg (Comp.SourceTable.view model.sources)
, div , div
[ classList [ classList
[ ( "ui dimmer", True ) [ ( "ui dimmer", True )
@ -222,6 +248,112 @@ viewTable model =
] ]
viewLinks : Flags -> UiSettings -> Source -> Html Msg
viewLinks flags _ source =
let
appUrl =
flags.config.baseUrl ++ "/app/upload/" ++ source.id
apiUrl =
flags.config.baseUrl ++ "/api/v1/open/upload/item/" ++ source.id
in
div
[]
[ h3 [ class "ui dividing header" ]
[ text "Public Uploads: "
, text source.abbrev
, div [ class "sub header" ]
[ text source.id
]
]
, p []
[ text "This source defines URLs that can be used by anyone to send files to "
, text "you. There is a web page that you can share or the API url can be used "
, text "with other clients."
]
, p []
[ text "There have been "
, String.fromInt source.counter |> text
, text " items created through this source."
]
, h4 [ class "ui header" ]
[ text "Public Upload Page"
]
, div [ class "ui attached message" ]
[ div [ class "ui fluid left action input" ]
[ a
[ class "ui left icon button"
, title "Copy to clipboard"
, href "#"
, Tuple.second appClipboardData
|> String.dropLeft 1
|> id
, attribute "data-clipboard-target" "#app-url"
]
[ i [ class "copy icon" ] []
]
, a
[ class "ui icon button"
, href appUrl
, target "_blank"
, title "Open in new tab/window"
]
[ i [ class "link external icon" ] []
]
, input
[ type_ "text"
, id "app-url"
, value appUrl
, readonly True
]
[]
]
]
, div [ class "ui attached segment" ]
[ div [ class "qr-code" ]
[ qrCodeView appUrl
]
]
, h4 [ class "ui header" ]
[ text "Public API Upload URL"
]
, div [ class "ui attached message" ]
[ div [ class "ui fluid left action input" ]
[ a
[ class "ui left icon button"
, title "Copy to clipboard"
, href "#"
, Tuple.second apiClipboardData
|> String.dropLeft 1
|> id
, attribute "data-clipboard-target" "#api-url"
]
[ i [ class "copy icon" ] []
]
, input
[ type_ "text"
, value apiUrl
, readonly True
, id "api-url"
]
[]
]
]
, div [ class "ui attached segment" ]
[ div [ class "qr-code" ]
[ qrCodeView apiUrl
]
]
, div [ class "ui divider" ] []
, button
[ class "ui button"
, onClick SetTableView
]
[ text "Back"
]
]
viewForm : Flags -> UiSettings -> Model -> List (Html Msg) viewForm : Flags -> UiSettings -> Model -> List (Html Msg)
viewForm flags settings model = viewForm flags settings model =
let let
@ -256,7 +388,7 @@ viewForm flags settings model =
, button [ class "ui primary button", type_ "submit" ] , button [ class "ui primary button", type_ "submit" ]
[ text "Submit" [ text "Submit"
] ]
, a [ class "ui secondary button", onClick (SetViewMode Table), href "" ] , a [ class "ui secondary button", onClick SetTableView, href "" ]
[ text "Cancel" [ text "Cancel"
] ]
, if not newSource then , if not newSource then

View File

@ -1,7 +1,7 @@
module Comp.SourceTable exposing module Comp.SourceTable exposing
( Model ( Msg
, Msg(..) , SelectMode(..)
, emptyModel , isEdit
, update , update
, view , view
) )
@ -14,44 +14,47 @@ import Html.Attributes exposing (..)
import Html.Events exposing (onClick) import Html.Events exposing (onClick)
type alias Model = type SelectMode
{ sources : List Source = Edit Source
, selected : Maybe Source | Display Source
} | None
emptyModel : Model isEdit : SelectMode -> Bool
emptyModel = isEdit m =
{ sources = [] case m of
, selected = Nothing Edit _ ->
} True
Display _ ->
False
None ->
False
type Msg type Msg
= SetSources (List Source) = Select Source
| Select Source | Show Source
| Deselect
update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) update : Flags -> Msg -> ( Cmd Msg, SelectMode )
update _ msg model = update _ msg =
case msg of case msg of
SetSources list ->
( { model | sources = list, selected = Nothing }, Cmd.none )
Select source -> Select source ->
( { model | selected = Just source }, Cmd.none ) ( Cmd.none, Edit source )
Deselect -> Show source ->
( { model | selected = Nothing }, Cmd.none ) ( Cmd.none, Display source )
view : Model -> Html Msg view : List Source -> Html Msg
view model = view sources =
table [ class "ui selectable table" ] table [ class "ui table" ]
[ thead [] [ thead []
[ tr [] [ tr []
[ th [ class "collapsing" ] [ text "Abbrev" ] [ th [ class "collapsing" ] []
, th [ class "collapsing" ] [ text "Abbrev" ]
, th [ class "collapsing" ] [ text "Enabled" ] , th [ class "collapsing" ] [ text "Enabled" ]
, th [ class "collapsing" ] [ text "Counter" ] , th [ class "collapsing" ] [ text "Counter" ]
, th [ class "collapsing" ] [ text "Priority" ] , th [ class "collapsing" ] [ text "Priority" ]
@ -59,17 +62,37 @@ view model =
] ]
] ]
, tbody [] , tbody []
(List.map (renderSourceLine model) model.sources) (List.map renderSourceLine sources)
] ]
renderSourceLine : Model -> Source -> Html Msg renderSourceLine : Source -> Html Msg
renderSourceLine model source = renderSourceLine source =
tr tr
[ classList [ ( "active", model.selected == Just source ) ] []
, onClick (Select source)
]
[ td [ class "collapsing" ] [ td [ class "collapsing" ]
[ a
[ class "ui basic tiny primary button"
, href "#"
, onClick (Select source)
]
[ i [ class "edit icon" ] []
, text "Edit"
]
, a
[ classList
[ ( "ui basic tiny primary button", True )
, ( "disabled", not source.enabled )
]
, href "#"
, disabled (not source.enabled)
, onClick (Show source)
]
[ i [ class "eye icon" ] []
, text "Show"
]
]
, td [ class "collapsing" ]
[ text source.abbrev [ text source.abbrev
] ]
, td [ class "collapsing" ] , td [ class "collapsing" ]

View File

@ -1,5 +1,6 @@
port module Ports exposing port module Ports exposing
( getUiSettings ( getUiSettings
, initClipboard
, loadUiSettings , loadUiSettings
, onUiSettingsSaved , onUiSettingsSaved
, removeAccount , removeAccount
@ -78,3 +79,6 @@ getUiSettings flags =
Nothing -> Nothing ->
Cmd.none Cmd.none
port initClipboard : ( String, String ) -> Cmd msg

View File

@ -104,6 +104,14 @@
background: rgba(220, 255, 71, 0.6); background: rgba(220, 255, 71, 0.6);
} }
.default-layout .qr-code svg {
width: 200px;
height: 200px;
}
.default-layout .qr-code.bordered svg {
border: 1px solid #ccc;
}
.markdown-preview { .markdown-preview {
overflow: auto; overflow: auto;
max-height: 300px; max-height: 300px;

View File

@ -83,3 +83,13 @@ elmApp.ports.requestUiSettings.subscribe(function(args) {
} }
} }
}); });
var docspell_clipboards = {};
elmApp.ports.initClipboard.subscribe(function(args) {
var page = args[0];
if (!docspell_clipboards[page]) {
var sel = args[1];
docspell_clipboards[page] = new ClipboardJS(sel);
}
});

View File

@ -9,6 +9,7 @@ object Dependencies {
val BitpeaceVersion = "0.5.0" val BitpeaceVersion = "0.5.0"
val CalevVersion = "0.3.1" val CalevVersion = "0.3.1"
val CirceVersion = "0.13.0" val CirceVersion = "0.13.0"
val ClipboardJsVersion = "2.0.6"
val DoobieVersion = "0.9.0" val DoobieVersion = "0.9.0"
val EmilVersion = "0.6.2" val EmilVersion = "0.6.2"
val FastparseVersion = "2.1.3" val FastparseVersion = "2.1.3"
@ -245,10 +246,11 @@ object Dependencies {
val betterMonadicFor = "com.olegpy" %% "better-monadic-for" % BetterMonadicForVersion val betterMonadicFor = "com.olegpy" %% "better-monadic-for" % BetterMonadicForVersion
val webjars = Seq( val webjars = Seq(
"org.webjars" % "swagger-ui" % SwaggerUIVersion, "org.webjars" % "swagger-ui" % SwaggerUIVersion,
"org.webjars" % "Semantic-UI" % SemanticUIVersion, "org.webjars" % "Semantic-UI" % SemanticUIVersion,
"org.webjars" % "jquery" % JQueryVersion, "org.webjars" % "jquery" % JQueryVersion,
"org.webjars" % "viewerjs" % ViewerJSVersion "org.webjars" % "viewerjs" % ViewerJSVersion,
"org.webjars" % "clipboard.js" % ClipboardJsVersion
) )
val icu4j = Seq( val icu4j = Seq(

View File

@ -1,5 +1,5 @@
let let
nixpkgsUnstable = builtins.fetchTarball { nixpkgsUnstable = builtins.fetchTarball {
url = "https://github.com/NixOS/nixpkgs-channels/archive/nixos-unstable.tar.gz"; url = "https://github.com/NixOS/nixpkgs-channels/archive/nixos-unstable.tar.gz";
}; };
pkgsUnstable = import nixpkgsUnstable { }; pkgsUnstable = import nixpkgsUnstable { };