Merge branch 'master' of github.com:eikek/docspell into pr-fix-ocrmypdf

This commit is contained in:
Malte 2020-10-31 19:39:23 +01:00
commit d5f0d47e80
19 changed files with 590 additions and 28 deletions

View File

@ -1,5 +1,62 @@
# Changelog
## v0.14.0
*Soon*
This release contains many bug fixes, thank you all so much for
helping out! There is also a new feature and some more scripts in
tools.
- Edit/delete multiple items at once (#253, #412)
- Show/hide side menus via ui settings (#351)
- Adds two more scripts to the `tools/` section (thanks to
@totti4ever):
- one script to import data from paperless (#358, #359), and
- a script to check clean a directory from files that are already in
docspell (#403)
- Extend docker image to use newest ocrmypdf version (#393, thanks
@totti4ever)
- Fix bug that would stop processing when pdf conversion fails (#392,
#387)
- Fix bug to have a separate, configurable source identifier for the
integration upload endpoint (#389)
- Fixes ui bug to not highlight the last viewed item when searching
again. (#373)
- Fixes bug when saving multiple changes to the ui settings (#368)
- Fixes uniqueness check for equipments (#370)
- Fixes a bug when doing document classification where user input was
not correctly escaped for regexes (#356)
- Fixes debian packages to have both (joex + restserver) the same user
to make H2 work (#336)
- Fixes a bug when searching with multiple tags using MariaDB (#404)
### REST Api Changes
- Routes for managing multiple items:
- `/sec/items/deleteAll`
- `/sec/items/tags`
- `/sec/items/tagsremove`
- `/sec/items/name`
- `/sec/items/folder`
- `/sec/items/direction`
- `/sec/items/date`
- `/sec/items/duedate`
- `/sec/items/corrOrg`
- `/sec/items/corrPerson`
- `/sec/items/concPerson`
- `/sec/items/concEquipment`
- `/sec/items/confirm`
- `/sec/items/unconfirm`
- `/sec/items/reprocess`
- Adds another parameter to `ItemSearch` structure to enable searching
in a subset of items giving their ids.
### Configuration Changes
- new setting `….integration-endpoint.source-name` to define the
source name for files uploaded through this endpoint
## v0.13.0
*Oct 19, 2020*

View File

@ -46,6 +46,12 @@ trait OItem[F[_]] {
collective: Ident
): F[UpdateResult]
def removeTagsMultipleItems(
items: NonEmptyList[Ident],
tags: List[String],
collective: Ident
): F[UpdateResult]
/** Toggles tags of the given item. Tags must exist, but can be IDs or names. */
def toggleTags(item: Ident, tags: List[String], collective: Ident): F[UpdateResult]
@ -225,6 +231,29 @@ object OItem {
}
}
def removeTagsMultipleItems(
items: NonEmptyList[Ident],
tags: List[String],
collective: Ident
): F[UpdateResult] =
tags.distinct match {
case Nil => UpdateResult.success.pure[F]
case ws =>
store.transact {
(for {
itemIds <- OptionT
.liftF(RItem.filterItems(items, collective))
.filter(_.nonEmpty)
given <- OptionT.liftF(RTag.findAllByNameOrId(ws, collective))
_ <- OptionT.liftF(
itemIds.traverse(item =>
RTagItem.removeAllTags(item, given.map(_.tagId).toList)
)
)
} yield UpdateResult.success).getOrElse(UpdateResult.notFound)
}
}
def toggleTags(
item: Ident,
tags: List[String],

View File

@ -1945,6 +1945,29 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/BasicResult"
/sec/items/tagsremove:
post:
tags:
- Item (Multi Edit)
summary: Remove tags from multiple items
description: |
Remove the given tags from all given items.
security:
- authTokenHeader: []
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/ItemsAndRefs"
responses:
200:
description: Ok
content:
application/json:
schema:
$ref: "#/components/schemas/BasicResult"
put:
tags:
- Item (Multi Edit)

View File

@ -73,6 +73,18 @@ object ItemMultiRoutes {
resp <- Ok(Conversions.basicResult(res, "Tags added."))
} yield resp
case req @ POST -> Root / "tagsremove" =>
for {
json <- req.as[ItemsAndRefs]
items <- readIds[F](json.items)
res <- backend.item.removeTagsMultipleItems(
items,
json.refs,
user.account.collective
)
resp <- Ok(Conversions.basicResult(res, "Tags removed"))
} yield resp
case req @ PUT -> Root / "name" =>
for {
json <- req.as[ItemsAndName]

View File

@ -10,6 +10,7 @@ module Api exposing
, changeFolderName
, changePassword
, checkCalEvent
, confirmMultiple
, createImapSettings
, createMailSettings
, createNewFolder
@ -74,6 +75,7 @@ module Api exposing
, refreshSession
, register
, removeMember
, removeTagsMultiple
, sendMail
, setAttachmentName
, setCollectiveSettings
@ -107,6 +109,7 @@ module Api exposing
, startReIndex
, submitNotifyDueItems
, toggleTags
, unconfirmMultiple
, updateNotifyDueItems
, updateScanMailbox
, upload
@ -1284,6 +1287,34 @@ getJobQueueStateTask flags =
--- Item (Mulit Edit)
confirmMultiple :
Flags
-> Set String
-> (Result Http.Error BasicResult -> msg)
-> Cmd msg
confirmMultiple flags ids receive =
Http2.authPut
{ url = flags.config.baseUrl ++ "/api/v1/sec/items/confirm"
, account = getAccount flags
, body = Http.jsonBody (Api.Model.IdList.encode (IdList (Set.toList ids)))
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
}
unconfirmMultiple :
Flags
-> Set String
-> (Result Http.Error BasicResult -> msg)
-> Cmd msg
unconfirmMultiple flags ids receive =
Http2.authPut
{ url = flags.config.baseUrl ++ "/api/v1/sec/items/unconfirm"
, account = getAccount flags
, body = Http.jsonBody (Api.Model.IdList.encode (IdList (Set.toList ids)))
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
}
setTagsMultiple :
Flags
-> ItemsAndRefs
@ -1312,6 +1343,20 @@ addTagsMultiple flags data receive =
}
removeTagsMultiple :
Flags
-> ItemsAndRefs
-> (Result Http.Error BasicResult -> msg)
-> Cmd msg
removeTagsMultiple flags data receive =
Http2.authPost
{ url = flags.config.baseUrl ++ "/api/v1/sec/items/tagsremove"
, account = getAccount flags
, body = Http.jsonBody (Api.Model.ItemsAndRefs.encode data)
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
}
setNameMultiple :
Flags
-> ItemsAndName

View File

@ -53,6 +53,12 @@ type SaveNameState
| SaveFailed
type TagEditMode
= AddTags
| RemoveTags
| ReplaceTags
type alias Model =
{ tagModel : Comp.Dropdown.Model Tag
, nameModel : String
@ -70,6 +76,7 @@ type alias Model =
, concPersonModel : Comp.Dropdown.Model IdName
, concEquipModel : Comp.Dropdown.Model IdName
, modalEdit : Maybe Comp.DetailEdit.Model
, tagEditMode : TagEditMode
}
@ -81,6 +88,8 @@ type Msg
| UpdateThrottle
| RemoveDueDate
| RemoveDate
| ConfirmMsg Bool
| ToggleTagEditMode
| FolderDropdownMsg (Comp.Dropdown.Msg IdName)
| TagDropdownMsg (Comp.Dropdown.Msg Tag)
| DirDropdownMsg (Comp.Dropdown.Msg Direction)
@ -145,6 +154,7 @@ init =
, dueDate = Nothing
, dueDatePicker = Comp.DatePicker.emptyModel
, modalEdit = Nothing
, tagEditMode = AddTags
}
@ -201,6 +211,9 @@ resultNone model =
update : Flags -> Msg -> Model -> UpdateResult
update flags msg model =
case msg of
ConfirmMsg flag ->
resultNoCmd (ConfirmChange flag) model
TagDropdownMsg m ->
let
( m2, _ ) =
@ -209,19 +222,48 @@ update flags msg model =
newModel =
{ model | tagModel = m2 }
mkChange list =
case model.tagEditMode of
AddTags ->
AddTagChange list
RemoveTags ->
RemoveTagChange list
ReplaceTags ->
ReplaceTagChange list
change =
if isDropdownChangeMsg m then
Comp.Dropdown.getSelected newModel.tagModel
|> Util.List.distinct
|> List.map (\t -> IdName t.id t.name)
|> ReferenceList
|> TagChange
|> mkChange
else
NoFormChange
in
resultNoCmd change newModel
ToggleTagEditMode ->
let
( m2, _ ) =
Comp.Dropdown.update (Comp.Dropdown.SetSelection []) model.tagModel
newModel =
{ model | tagModel = m2 }
in
case model.tagEditMode of
AddTags ->
resultNone { newModel | tagEditMode = RemoveTags }
RemoveTags ->
resultNone { newModel | tagEditMode = ReplaceTags }
ReplaceTags ->
resultNone { newModel | tagEditMode = AddTags }
GetTagsResp (Ok tags) ->
let
tagList =
@ -550,16 +592,66 @@ renderEditForm cfg settings model =
else
span [ class "invisible hidden" ] []
tagModeIcon =
case model.tagEditMode of
AddTags ->
i [ class "grey plus link icon" ] []
RemoveTags ->
i [ class "grey eraser link icon" ] []
ReplaceTags ->
i [ class "grey redo alternate link icon" ] []
tagModeMsg =
case model.tagEditMode of
AddTags ->
"Tags chosen here are *added* to all selected items."
RemoveTags ->
"Tags chosen here are *removed* from all selected items."
ReplaceTags ->
"Tags chosen here *replace* those on selected items."
in
div [ class cfg.menuClass ]
[ div [ class "ui form warning" ]
[ optional [ Data.Fields.Tag ] <|
[ div [ class "field" ]
[ div
[ class "ui fluid buttons"
]
[ button
[ class "ui primary button"
, onClick (ConfirmMsg True)
]
[ text "Confirm"
]
, div [ class "or" ] []
, button
[ class "ui secondary button"
, onClick (ConfirmMsg False)
]
[ text "Unconfirm"
]
]
]
, optional [ Data.Fields.Tag ] <|
div [ class "field" ]
[ label []
[ Icons.tagsIcon "grey"
, text "Tags"
, a
[ class "right-float"
, href "#"
, title "Change tag edit mode"
, onClick ToggleTagEditMode
]
[ tagModeIcon
]
]
, Html.map TagDropdownMsg (Comp.Dropdown.view settings model.tagModel)
, Markdown.toHtml [ class "small-info" ] tagModeMsg
]
, div [ class " field" ]
[ label [] [ text "Name" ]

View File

@ -20,7 +20,9 @@ import Set exposing (Set)
type FormChange
= NoFormChange
| TagChange ReferenceList
| AddTagChange ReferenceList
| ReplaceTagChange ReferenceList
| RemoveTagChange ReferenceList
| FolderChange (Maybe IdName)
| DirectionChange Direction
| OrgChange (Maybe IdName)
@ -30,6 +32,7 @@ type FormChange
| ItemDateChange (Maybe Int)
| DueDateChange (Maybe Int)
| NameChange String
| ConfirmChange Bool
multiUpdate :
@ -44,13 +47,27 @@ multiUpdate flags ids change receive =
Set.toList ids
in
case change of
TagChange tags ->
ReplaceTagChange tags ->
let
data =
ItemsAndRefs items (List.map .id tags.items)
in
Api.setTagsMultiple flags data receive
AddTagChange tags ->
let
data =
ItemsAndRefs items (List.map .id tags.items)
in
Api.addTagsMultiple flags data receive
RemoveTagChange tags ->
let
data =
ItemsAndRefs items (List.map .id tags.items)
in
Api.removeTagsMultiple flags data receive
NameChange name ->
let
data =
@ -114,5 +131,12 @@ multiUpdate flags ids change receive =
in
Api.setConcEquipmentMultiple flags data receive
ConfirmChange flag ->
if flag then
Api.confirmMultiple flags ids receive
else
Api.unconfirmMultiple flags ids receive
NoFormChange ->
Cmd.none

View File

@ -59,7 +59,7 @@ view flags settings model =
, onClick ToggleSearchMenu
, title "Hide menu"
]
[ i [ class "ui angle left icon" ] []
[ i [ class "chevron left icon" ] []
]
, div [ class "right floated menu" ]
[ a
@ -303,13 +303,9 @@ viewSearchBar flags model =
[ class "search-menu-toggle ui blue icon button"
, onClick ToggleSearchMenu
, href "#"
, if model.searchTypeForm == ContentOnlySearch then
title "Search menu disabled"
else
title "Open search menu"
, title "Open search menu"
]
[ i [ class "angle right icon" ] []
[ i [ class "filter icon" ] []
]
, div [ class "item" ]
[ div [ class "ui left icon right action input" ]

View File

@ -14,14 +14,14 @@ object Dependencies {
val EmilVersion = "0.6.3"
val FastparseVersion = "2.1.3"
val FlexmarkVersion = "0.62.2"
val FlywayVersion = "7.1.0"
val FlywayVersion = "7.1.1"
val Fs2Version = "2.4.4"
val H2Version = "1.4.200"
val Http4sVersion = "0.21.8"
val Icu4jVersion = "68.1"
val JsoupVersion = "1.13.1"
val KindProjectorVersion = "0.10.3"
val Log4sVersion = "1.8.2"
val Log4sVersion = "1.9.0"
val LogbackVersion = "1.2.3"
val MariaDbVersion = "2.7.0"
val MiniTestVersion = "2.8.2"
@ -34,7 +34,7 @@ object Dependencies {
val StanfordNlpVersion = "4.0.0"
val TikaVersion = "1.24.1"
val YamuscaVersion = "0.7.0"
val SwaggerUIVersion = "3.36.0"
val SwaggerUIVersion = "3.36.1"
val SemanticUIVersion = "2.4.1"
val TwelveMonkeysVersion = "3.6"
val JQueryVersion = "3.5.1"

View File

@ -0,0 +1,179 @@
#!/usr/bin/env bash
echo "##################### START #####################"
echo " Docspell Consumedir Cleaner - v0.1 beta"
echo " by totti4ever" && echo
echo " $(date)"
echo
echo "#################################################"
echo && echo
jq --version > /dev/null
if [ $? -ne 0 ]; then
echo "please install 'jq'"
exit -4
fi
ds_url=${1%/}
ds_user_param=$2
ds_user=${ds_user_param#*/}
ds_collective=${ds_user_param%%/*}
ds_password=$3
ds_consumedir_path=${4%/}
ds_archive_path=$ds_consumedir_path/_archive/$ds_collective
if [ $# -ne 4 ]; then
echo "FATAL Exactly four parameters needed"
exit -3
elif [ "$1" == "" ] || [ "$2" == "" ] || [ "$3" == "" ] || [ "$4" == "" ]; then
echo "FATAL Parameter missing"
echo " ds_url: $ds_url"
echo " ds_user: $ds_user"
echo " ds_password: $ds_password"
echo " ds_consumedir_path: $ds_consumedir_path"
exit -2
elif [ "$ds_collective" == "_archive" ]; then
echo "FATAL collective name '_archive' is not supported by this script"
exit -1
fi
############# FUNCTIONS
function curl_call() {
curl_cmd="$1 -H 'X-Docspell-Auth: $ds_token'"
curl_result=$(eval $curl_cmd)
curl_code=$?
if [ "$curl_result" == '"Authentication failed."' ] || [ "$curl_result" == 'Response timed out' ]; then
printf "\nNew login required ($curl_result)... "
login
printf "%${#len_resultset}s" " "; printf " .."
curl_call $1
elif [ "$curl_result" == "Bad Gateway" ] || [ "$curl_result" == '404 page not found' ]; then
echo "FATAL Connection to server failed"
exit -1
fi
}
function login() {
curl_call "curl -s -X POST -d '{\"account\": \"$ds_collective/$ds_user\", \"password\": \"$ds_password\"}' ${ds_url}/api/v1/open/auth/login"
curl_status=$(echo $curl_result | jq -r ".success")
if [ "$curl_status" == "true" ]; then
ds_token=$(echo $curl_result | jq -r ".token")
echo "Login successfull ( Token: $ds_token )"
else
echo "FATAL Login not succesfull"
exit 1
fi
}
############# END
echo "Settings:"
if [ "$DS_CC_REMOVE" == "true" ]; then
echo " ### !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ###"
echo " - DELETE files? YES"
echo " when already existing in Docspell. This cannot be undone!"
echo " ### !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ###"
else
echo " - DELETE files? no"
echo " moving already uploaded files to archive"
fi
echo
if [ "$DS_CC_UPLOAD_MISSING" == true ]; then
echo " - UPLOAD files? YES"
echo " files not existing in Docspell will be uploaded and will be re-checked in the next run."
else
echo " - UPLOAD files? no"
echo " files not existing in Docspell will NOT be uploaded and stay where they are."
fi
echo && echo
echo "Press 'ctrl+c' to cancel"
for ((i=9;i>=0;i--)); do
printf "\r waiting $i seconds "
sleep 1s
done
echo && echo
# login, get token
login
echo "Scanning folder for collective '$ds_collective' ($ds_consumedir_path/$ds_collective)"
echo && echo
while read -r line
do
tmp_filepath=$line
if [ "$tmp_filepath" == "" ]; then
echo "no files found" && echo
exit 0 #no results
elif [ ! -f "$tmp_filepath" ]; then
echo "FATAL no access to file: $tmp_filepath"
exit 3
fi
echo "Checking '$tmp_filepath'"
printf "%${#len_resultset}s" " "; printf " "
# check for checksum
tmp_checksum=$(sha256sum "$tmp_filepath" | awk '{print $1}')
curl_call "curl -s -X GET '$ds_url/api/v1/sec/checkfile/$tmp_checksum'"
curl_status=$(echo $curl_result | jq -r ".exists")
if [ $curl_code -ne 0 ]; then
# error
echo "ERROR $curl_result // $curl_status"
# file exists in Docspell
elif [ "$curl_status" == "true" ]; then
item_name=$(echo $curl_result | jq -r ".items[0].name")
item_id=$(echo $curl_result | jq -r ".items[0].id")
echo "File already exists: '$item_name (ID: $item_id)'"
printf "%${#len_resultset}s" " "; printf " "
if [ "$DS_CC_REMOVE" == "true" ]; then
echo "... removing file"
rm "$tmp_filepath"
else
created=$(echo $curl_result | jq -r ".items[0].created")
cur_dir="$ds_archive_path/$(date -d @$(echo "($created+500)/1000" | bc) +%Y-%m
)"
echo "... moving to archive by month added ('$cur_dir')"
mkdir -p "$cur_dir"
mv "$tmp_filepath" "$cur_dir/"
fi
# file does not exist in Docspell
else
echo "Files does not exist, yet"
if [ "$DS_CC_UPLOAD_MISSING" == true ]; then
printf "%${#len_resultset}s" " "; printf " "
printf "...uploading file.."
curl_call "curl -s -X POST '$ds_url/api/v1/sec/upload/item' -H 'Content-Type: multipart/form-data' -F 'file=@$tmp_filepath'"
curl_status=$(echo $curl_result | jq -r ".success")
if [ "$curl_status" == "true" ]; then
echo ". done"
else
echo -e "\nERROR $curl_result"
fi
fi
fi
echo
done \
<<< $(find $ds_consumedir_path/$ds_collective -type f)
echo ################# DONE #################
date

View File

@ -87,6 +87,13 @@ The directory contains a file `docspell.conf` that you can
]
, text "."
]
, li []
[ text "Chat on "
, a [ href "https://gitter.im/eikek/docspell" ]
[ text "Gitter"
]
, text " for questions and feedback."
]
]
]
]

View File

@ -173,13 +173,12 @@ footHero model =
[ text " "
]
, span []
[ text "© 2020 "
]
, a
[ href "https://github.com/eikek"
, target "_blank"
]
[ text "@eikek"
[ a
[ href "https://gitter.im/eikek/docspell"
, target "_blank"
]
[ text "Chat on Gitter"
]
]
]
]

View File

@ -104,8 +104,8 @@ view model =
[ class "form"
, onSubmit SubmitSearch
]
[ div [ class "dropdown field is-active is-fullwidth" ]
[ div [ class "control has-icons-right is-fullwidth" ]
[ div [ class "dropdown field is-active is-fullwidth has-addons" ]
[ div [ class "control is-fullwidth" ]
[ input
[ class "input"
, type_ "text"
@ -114,7 +114,13 @@ view model =
, value model.searchInput
]
[]
, span [ class "icon is-right is-small" ]
]
, div [ class "control" ]
[ button
[ class "button is-primary"
, href "#"
, onClick SubmitSearch
]
[ img [ src "/icons/search-20.svg" ] []
]
]

View File

@ -38,6 +38,7 @@ description = "A list of features and limitations."
with due dates
- [Read your mailboxes](@/docs/webapp/scanmailbox.md) via IMAP to
import mails into docspell
- [Edit multiple items](@/docs/webapp/multiedit.md) at once
- REST server and document processing are separate applications which
can be scaled-out independently
- Everything stored in a SQL database: PostgreSQL, MariaDB or H2

View File

@ -0,0 +1,52 @@
+++
title = "Directory Cleaner"
description = "Clean directories from files in docspell"
weight = 32
+++
# Introduction
This script is made for cleaning up the consumption directory used for
the consumedir service (as it is provided as docker container)which
are copied or moved there.
<https://github.com/eikek/docspell/tree/master/tools/consumedir-cleaner>
## How it works
- Checks for every file (in the collective's folder of the given user
name) if it already exists in the collective (using Docspell's API).
- If so, by default those files are moved to an archive folder just
besides the collective's consumption folders named _archive. The
archive's files are organized into monthly subfolders by the date
they've been added to Docspell
- If set, those files can also be deleted instead of being moved to
the archive. There is no undo function provided for this, so be
careful.
- If a file is found which does not exist in the collective, by
default nothing happens, so that file would be found in every run
and just ignored
- If set, those files can also be uploaded to Docspell. Depending on
the setting for files already existing these files would either be
deleted or moved to the archive in the next run.
## Usage (parameters / settings)
Copy the script to your machine and run it with the following
parameters:
1. URL of Docspell, including http(s)
2. Username for Docspell, possibly including Collective (if other name
as user)
3. Password for Docspell
4. Path to the directory which files shall be checked against
existence in Docspell
Additionally, environment variables can be used to alter the behavior:
- `DS_CC_REMOVE`
- `true` delete files which already exist in the collective
- `false` (default) - move them to the archive (see above)
- `DS_CC_UPLOAD_MISSING`
- `true` - uploads files which do not exist in the collective
- `false` (default) - ignore them and do nothing

View File

@ -0,0 +1,43 @@
+++
title = "Paperless Import"
description = "Import your data from paperless."
weight = 35
+++
# Introduction
Coming from
[paperless](https://github.com/the-paperless-project/paperless/), the
script in `tools/import-paperless` can be used to get started by
importing your data from paperless into docspell.
<https://github.com/eikek/docspell/tree/master/tools/import-paperless>
The script imports the files and also tags and correspondents.
# Usage
Copy the script to the machine where paperless is running. Run it with
the following arguments:
1. URL of Docspell, including http(s)
2. Username for Docspell, possibly including Collective (if other name as user)
3. Password for Docspell
4. Path to Paperless' database file (`db.sqlite3`). When using Paperless with docker, it is in the mapped directory `/usr/src/paperless/data`
5. Path to Paperless' document base directory. When using Paperless with docker, it is the mapped directory `/usr/src/paperless/media/documents/origin/`
Some settings can be changed inside the script, right at the top:
* `LIMIT="LIMIT 0"` (default: inactive): For testing purposes, limits
the number of tags and correspondents read from Paperless (this will
most likely lead to warnings when processing the documents)
* `LIMIT_DOC="LIMIT 5"` (default: inactive): For testing purposes,
limits the number of documents and document-to-tag relations read
from Paperless
* `SKIP_EXISTING_DOCS=true` (default: true): Won't touch already
existing documents. If set to `false` documents, which exist
already, won't be uploaded again, but the tags, correspondent, date
and title from Paperless will be applied.
**Warning** In case you already had set these information in Docspell,
they will be overwritten!

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 139 KiB

View File

@ -12,10 +12,7 @@
</a>
<span class="ml-1 mr-1"></span>
<span>
© 2020
<a href="https://github.com/eikek" target="_blank">
@eikek
</a>
<a href="https://gitter.im/eikek/docspell">Chat on Gitter</a>
</span>
</div>
</div>