Integrate elm App with Tauri. Can navigate into source directory.
25
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
src-elm/elm-stuff
|
||||||
7
README.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Tauri + Vanilla
|
||||||
|
|
||||||
|
This template should help get you started developing with Tauri in vanilla HTML, CSS and Javascript.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||||
42
src-elm/elm.json
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"type": "application",
|
||||||
|
"source-directories": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"elm-version": "0.19.1",
|
||||||
|
"dependencies": {
|
||||||
|
"direct": {
|
||||||
|
"ChristophP/elm-mark": "2.0.4",
|
||||||
|
"Gizra/elm-keyboard-event": "1.0.1",
|
||||||
|
"NoRedInk/elm-json-decode-pipeline": "1.0.1",
|
||||||
|
"SwiftsNamesake/proper-keyboard": "4.0.0",
|
||||||
|
"basti1302/elm-human-readable-filesize": "1.2.0",
|
||||||
|
"carwow/elm-slider": "11.1.6",
|
||||||
|
"dasch/levenshtein": "1.0.3",
|
||||||
|
"elm/browser": "1.0.2",
|
||||||
|
"elm/core": "1.0.5",
|
||||||
|
"elm/html": "1.0.1",
|
||||||
|
"elm/json": "1.1.4",
|
||||||
|
"elm/regex": "1.0.0",
|
||||||
|
"elm/time": "1.0.0",
|
||||||
|
"elm-community/list-extra": "8.7.0",
|
||||||
|
"elm-community/string-extra": "4.0.1",
|
||||||
|
"elm-explorations/test": "2.2.0",
|
||||||
|
"rtfeldman/elm-iso8601-date-strings": "1.1.4",
|
||||||
|
"z5h/jaro-winkler": "1.0.2"
|
||||||
|
},
|
||||||
|
"indirect": {
|
||||||
|
"debois/elm-dom": "1.3.0",
|
||||||
|
"elm/bytes": "1.0.8",
|
||||||
|
"elm/parser": "1.1.0",
|
||||||
|
"elm/random": "1.0.0",
|
||||||
|
"elm/url": "1.0.0",
|
||||||
|
"elm/virtual-dom": "1.0.5",
|
||||||
|
"myrho/elm-round": "1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test-dependencies": {
|
||||||
|
"direct": {},
|
||||||
|
"indirect": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
11645
src-elm/index.html
Normal file
3068
src-elm/package-lock.json
generated
Normal file
8
src-elm/package.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"elm-json": "^0.2.13",
|
||||||
|
"elm-live": "^4.0.2",
|
||||||
|
"elm-review": "^2.13.5",
|
||||||
|
"elm-test": "^0.19.1-revision17"
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src-elm/review/elm.json
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"type": "application",
|
||||||
|
"source-directories": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"elm-version": "0.19.1",
|
||||||
|
"dependencies": {
|
||||||
|
"direct": {
|
||||||
|
"SiriusStarr/elm-review-no-unsorted": "1.1.2",
|
||||||
|
"elm/core": "1.0.5",
|
||||||
|
"elm/json": "1.1.3",
|
||||||
|
"elm/project-metadata-utils": "1.0.2",
|
||||||
|
"jfmengels/elm-review": "2.8.1",
|
||||||
|
"jfmengels/elm-review-code-style": "1.0.0",
|
||||||
|
"jfmengels/elm-review-common": "1.2.1",
|
||||||
|
"jfmengels/elm-review-debug": "1.0.6",
|
||||||
|
"jfmengels/elm-review-documentation": "2.0.1",
|
||||||
|
"jfmengels/elm-review-simplify": "2.0.16",
|
||||||
|
"jfmengels/elm-review-unused": "1.1.22",
|
||||||
|
"stil4m/elm-syntax": "7.2.9"
|
||||||
|
},
|
||||||
|
"indirect": {
|
||||||
|
"avh4/elm-fifo": "1.0.4",
|
||||||
|
"elm/html": "1.0.0",
|
||||||
|
"elm/parser": "1.1.0",
|
||||||
|
"elm/random": "1.0.0",
|
||||||
|
"elm/regex": "1.0.0",
|
||||||
|
"elm/time": "1.0.0",
|
||||||
|
"elm/virtual-dom": "1.0.3",
|
||||||
|
"elm-community/dict-extra": "2.4.0",
|
||||||
|
"elm-community/graph": "6.0.0",
|
||||||
|
"elm-community/intdict": "3.0.0",
|
||||||
|
"elm-community/list-extra": "8.6.0",
|
||||||
|
"elm-community/maybe-extra": "5.3.0",
|
||||||
|
"elm-community/result-extra": "2.4.0",
|
||||||
|
"elm-community/string-extra": "4.0.1",
|
||||||
|
"elm-explorations/test": "1.2.2",
|
||||||
|
"miniBill/elm-unicode": "1.0.2",
|
||||||
|
"rtfeldman/elm-hex": "1.0.0",
|
||||||
|
"stil4m/structured-writer": "1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test-dependencies": {
|
||||||
|
"direct": {
|
||||||
|
"elm-explorations/test": "1.2.2"
|
||||||
|
},
|
||||||
|
"indirect": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src-elm/review/src/ReviewConfig.elm
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
module ReviewConfig exposing (config)
|
||||||
|
|
||||||
|
{-| Do not rename the ReviewConfig module or the config function, because
|
||||||
|
`elm-review` will look for these.
|
||||||
|
|
||||||
|
To add packages that contain rules, add them to this review project using
|
||||||
|
|
||||||
|
`elm install author/packagename`
|
||||||
|
|
||||||
|
when inside the directory containing this file.
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Docs.ReviewAtDocs
|
||||||
|
import NoDebug.Log
|
||||||
|
import NoDebug.TodoOrToString
|
||||||
|
import NoExposingEverything
|
||||||
|
import NoImportingEverything
|
||||||
|
import NoMissingTypeAnnotation
|
||||||
|
import NoMissingTypeAnnotationInLetIn
|
||||||
|
import NoMissingTypeExpose
|
||||||
|
import NoPrematureLetComputation
|
||||||
|
import NoSimpleLetBody
|
||||||
|
import NoUnsortedCases
|
||||||
|
import NoUnsortedLetDeclarations
|
||||||
|
import NoUnsortedRecords
|
||||||
|
import NoUnsortedTopLevelDeclarations
|
||||||
|
import NoUnused.CustomTypeConstructorArgs
|
||||||
|
import NoUnused.CustomTypeConstructors
|
||||||
|
import NoUnused.Dependencies
|
||||||
|
import NoUnused.Exports
|
||||||
|
import NoUnused.Modules
|
||||||
|
import NoUnused.Parameters
|
||||||
|
import NoUnused.Patterns
|
||||||
|
import NoUnused.Variables
|
||||||
|
import Review.Rule as Rule exposing (Rule)
|
||||||
|
import Simplify
|
||||||
|
|
||||||
|
|
||||||
|
config : List Rule
|
||||||
|
config =
|
||||||
|
[ Docs.ReviewAtDocs.rule
|
||||||
|
, NoDebug.Log.rule
|
||||||
|
, NoDebug.TodoOrToString.rule
|
||||||
|
|> Rule.ignoreErrorsForDirectories [ "tests/" ]
|
||||||
|
, NoExposingEverything.rule
|
||||||
|
, NoImportingEverything.rule []
|
||||||
|
, NoMissingTypeAnnotation.rule
|
||||||
|
, NoMissingTypeAnnotationInLetIn.rule
|
||||||
|
, NoMissingTypeExpose.rule
|
||||||
|
, NoSimpleLetBody.rule
|
||||||
|
, NoPrematureLetComputation.rule
|
||||||
|
, NoUnused.CustomTypeConstructors.rule []
|
||||||
|
, NoUnused.CustomTypeConstructorArgs.rule
|
||||||
|
, NoUnused.Dependencies.rule
|
||||||
|
, NoUnused.Exports.rule
|
||||||
|
, NoUnused.Modules.rule
|
||||||
|
, NoUnused.Parameters.rule
|
||||||
|
, NoUnused.Patterns.rule
|
||||||
|
, NoUnused.Variables.rule
|
||||||
|
, NoUnsortedCases.rule NoUnsortedCases.defaults
|
||||||
|
, NoUnsortedLetDeclarations.rule
|
||||||
|
(NoUnsortedLetDeclarations.sortLetDeclarations
|
||||||
|
|> NoUnsortedLetDeclarations.alphabetically
|
||||||
|
)
|
||||||
|
, NoUnsortedRecords.rule
|
||||||
|
(NoUnsortedRecords.defaults
|
||||||
|
|> NoUnsortedRecords.reportAmbiguousRecordsWithoutFix
|
||||||
|
)
|
||||||
|
, NoUnsortedTopLevelDeclarations.rule
|
||||||
|
(NoUnsortedTopLevelDeclarations.sortTopLevelDeclarations
|
||||||
|
|> NoUnsortedTopLevelDeclarations.portsFirst
|
||||||
|
|> NoUnsortedTopLevelDeclarations.exposedOrderWithPrivateLast
|
||||||
|
|> NoUnsortedTopLevelDeclarations.alphabetically
|
||||||
|
)
|
||||||
|
, Simplify.rule Simplify.defaults
|
||||||
|
]
|
||||||
254
src-elm/src/File.elm
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
module File exposing (File, FileStatus(..), defaultDir, extendSelectionToNext, extendSelectionToPrevious, fileDecoder, selectNext, selectPrevious, selectSimilar, toggleSelectionStatus, withName, withParentPath, withStatus)
|
||||||
|
|
||||||
|
import Iso8601
|
||||||
|
import Json.Decode exposing (Decoder)
|
||||||
|
import Json.Decode.Pipeline exposing (hardcoded, required)
|
||||||
|
import List.Extra
|
||||||
|
import StringComparison exposing (isSimilarityLevelGreaterThan)
|
||||||
|
import Time exposing (Posix, millisToPosix)
|
||||||
|
|
||||||
|
|
||||||
|
type alias File =
|
||||||
|
{ isDir : Bool
|
||||||
|
, mode : Int
|
||||||
|
, modTime : Posix
|
||||||
|
, name : String
|
||||||
|
, parentPath : String
|
||||||
|
, satisfiesFilter : Bool
|
||||||
|
, size : Int
|
||||||
|
, status : FileStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type FileStatus
|
||||||
|
= Unselected
|
||||||
|
| Edited
|
||||||
|
| Selected
|
||||||
|
| SelectedForDeletion
|
||||||
|
|
||||||
|
|
||||||
|
defaultDir : File
|
||||||
|
defaultDir =
|
||||||
|
{ isDir = True
|
||||||
|
, mode = 777
|
||||||
|
, modTime = millisToPosix 0
|
||||||
|
, name = ""
|
||||||
|
, parentPath = ""
|
||||||
|
, satisfiesFilter = False
|
||||||
|
, size = 0
|
||||||
|
, status = Unselected
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extendSelectionToNext : List File -> List File
|
||||||
|
extendSelectionToNext files =
|
||||||
|
let
|
||||||
|
( isAtLeastOneFileSelected, updatedFiles ) =
|
||||||
|
extendSelection List.Extra.mapAccuml files
|
||||||
|
in
|
||||||
|
if isAtLeastOneFileSelected then
|
||||||
|
updatedFiles
|
||||||
|
|
||||||
|
else
|
||||||
|
updatedFiles
|
||||||
|
|> selectLast
|
||||||
|
|
||||||
|
|
||||||
|
extendSelectionToPrevious : List File -> List File
|
||||||
|
extendSelectionToPrevious files =
|
||||||
|
let
|
||||||
|
( isAtLeastOneFileSelected, updatedFiles ) =
|
||||||
|
extendSelection List.Extra.mapAccumr files
|
||||||
|
in
|
||||||
|
if isAtLeastOneFileSelected then
|
||||||
|
updatedFiles
|
||||||
|
|
||||||
|
else
|
||||||
|
updatedFiles
|
||||||
|
|> selectLast
|
||||||
|
|
||||||
|
|
||||||
|
fileDecoder : Decoder File
|
||||||
|
fileDecoder =
|
||||||
|
Json.Decode.succeed File
|
||||||
|
|> required "IsDir" Json.Decode.bool
|
||||||
|
|> required "Mode" Json.Decode.int
|
||||||
|
|> required "ModTime" Iso8601.decoder
|
||||||
|
|> required "Name" Json.Decode.string
|
||||||
|
|> required "DirPath" Json.Decode.string
|
||||||
|
|> hardcoded False
|
||||||
|
|> required "Size" Json.Decode.int
|
||||||
|
|> hardcoded Unselected
|
||||||
|
|
||||||
|
|
||||||
|
selectNext : List File -> List File
|
||||||
|
selectNext files =
|
||||||
|
let
|
||||||
|
( isAtLeastOneFileSelected, updatedFiles ) =
|
||||||
|
selectNextOrPrevious List.Extra.mapAccuml files
|
||||||
|
in
|
||||||
|
if isAtLeastOneFileSelected then
|
||||||
|
updatedFiles
|
||||||
|
|
||||||
|
else
|
||||||
|
updatedFiles
|
||||||
|
|> selectLast
|
||||||
|
|
||||||
|
|
||||||
|
selectPrevious : List File -> List File
|
||||||
|
selectPrevious files =
|
||||||
|
let
|
||||||
|
( isAtLeastOneFileSelected, updatedFiles ) =
|
||||||
|
selectNextOrPrevious List.Extra.mapAccumr files
|
||||||
|
in
|
||||||
|
if isAtLeastOneFileSelected then
|
||||||
|
updatedFiles
|
||||||
|
|
||||||
|
else
|
||||||
|
updatedFiles
|
||||||
|
|> selectFirst
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{- selects files whose name have a high level of similarity -}
|
||||||
|
|
||||||
|
|
||||||
|
selectSimilar : File -> Int -> List File -> List File
|
||||||
|
selectSimilar referenceFile minSimilarity files =
|
||||||
|
List.map (selectIfSimilar referenceFile minSimilarity) files
|
||||||
|
|
||||||
|
|
||||||
|
toggleSelectionStatus : File -> File
|
||||||
|
toggleSelectionStatus file =
|
||||||
|
case file.status of
|
||||||
|
Unselected ->
|
||||||
|
{ file | status = Selected }
|
||||||
|
|
||||||
|
Edited ->
|
||||||
|
{ file | status = Selected }
|
||||||
|
|
||||||
|
Selected ->
|
||||||
|
{ file | status = Unselected }
|
||||||
|
|
||||||
|
SelectedForDeletion ->
|
||||||
|
-- TODO Remove from the list of files selected for deletion?
|
||||||
|
{ file | status = Selected }
|
||||||
|
|
||||||
|
|
||||||
|
withName : String -> File -> File
|
||||||
|
withName name file =
|
||||||
|
{ file | name = name }
|
||||||
|
|
||||||
|
|
||||||
|
withParentPath : String -> File -> File
|
||||||
|
withParentPath path file =
|
||||||
|
{ file | parentPath = path }
|
||||||
|
|
||||||
|
|
||||||
|
withStatus : FileStatus -> File -> File
|
||||||
|
withStatus fileStatus file =
|
||||||
|
{ file | status = fileStatus }
|
||||||
|
|
||||||
|
|
||||||
|
extendSelection :
|
||||||
|
((SelectionAccumulator -> File -> ( SelectionAccumulator, File )) -> SelectionAccumulator -> List File -> ( SelectionAccumulator, List File ))
|
||||||
|
-> List File
|
||||||
|
-> ( Bool, List File )
|
||||||
|
extendSelection visit files =
|
||||||
|
let
|
||||||
|
( finalAccumulator, updatedFiles ) =
|
||||||
|
visit
|
||||||
|
(\acc file ->
|
||||||
|
let
|
||||||
|
newAcc : SelectionAccumulator
|
||||||
|
newAcc =
|
||||||
|
{ acc | isPreviousSelected = file.status == Selected }
|
||||||
|
in
|
||||||
|
if acc.isPreviousSelected then
|
||||||
|
( { newAcc | selectedCount = acc.selectedCount + 1 }
|
||||||
|
, { file | status = Selected }
|
||||||
|
)
|
||||||
|
|
||||||
|
else
|
||||||
|
( newAcc, file )
|
||||||
|
)
|
||||||
|
initialAccumulator
|
||||||
|
files
|
||||||
|
|
||||||
|
initialAccumulator : SelectionAccumulator
|
||||||
|
initialAccumulator =
|
||||||
|
{ isPreviousSelected = False
|
||||||
|
, selectedCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
isAtLeastOneFileSelected : Bool
|
||||||
|
isAtLeastOneFileSelected =
|
||||||
|
finalAccumulator.selectedCount > 0
|
||||||
|
in
|
||||||
|
( isAtLeastOneFileSelected, updatedFiles )
|
||||||
|
|
||||||
|
|
||||||
|
selectFirst : List File -> List File
|
||||||
|
selectFirst files =
|
||||||
|
List.Extra.updateAt 0 (\f -> { f | status = Selected }) files
|
||||||
|
|
||||||
|
|
||||||
|
selectIfSimilar : File -> Int -> File -> File
|
||||||
|
selectIfSimilar referenceFile minSimilarity file =
|
||||||
|
if
|
||||||
|
(file == referenceFile)
|
||||||
|
|| isSimilarityLevelGreaterThan referenceFile.name file.name minSimilarity
|
||||||
|
then
|
||||||
|
{ file | status = Selected }
|
||||||
|
|
||||||
|
else
|
||||||
|
{ file | status = Unselected }
|
||||||
|
|
||||||
|
|
||||||
|
selectLast : List File -> List File
|
||||||
|
selectLast files =
|
||||||
|
List.Extra.updateAt (List.length files - 1) (\f -> { f | status = Selected }) files
|
||||||
|
|
||||||
|
|
||||||
|
selectNextOrPrevious :
|
||||||
|
((SelectionAccumulator -> File -> ( SelectionAccumulator, File )) -> SelectionAccumulator -> List File -> ( SelectionAccumulator, List File ))
|
||||||
|
-> List File
|
||||||
|
-> ( Bool, List File )
|
||||||
|
selectNextOrPrevious visit files =
|
||||||
|
let
|
||||||
|
( finalAccumulator, updatedFiles ) =
|
||||||
|
visit
|
||||||
|
(\acc file ->
|
||||||
|
let
|
||||||
|
newAcc : SelectionAccumulator
|
||||||
|
newAcc =
|
||||||
|
{ acc | isPreviousSelected = file.status == Selected }
|
||||||
|
in
|
||||||
|
if acc.isPreviousSelected then
|
||||||
|
( { newAcc | selectedCount = acc.selectedCount + 1 }
|
||||||
|
, { file | status = Selected }
|
||||||
|
)
|
||||||
|
|
||||||
|
else
|
||||||
|
( newAcc, { file | status = Unselected } )
|
||||||
|
)
|
||||||
|
initialAccumulator
|
||||||
|
files
|
||||||
|
|
||||||
|
initialAccumulator : SelectionAccumulator
|
||||||
|
initialAccumulator =
|
||||||
|
{ isPreviousSelected = False
|
||||||
|
, selectedCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
isAtLeastOneFileSelected : Bool
|
||||||
|
isAtLeastOneFileSelected =
|
||||||
|
finalAccumulator.selectedCount > 0
|
||||||
|
in
|
||||||
|
( isAtLeastOneFileSelected, updatedFiles )
|
||||||
|
|
||||||
|
|
||||||
|
type alias SelectionAccumulator =
|
||||||
|
{ isPreviousSelected : Bool
|
||||||
|
, selectedCount : Int
|
||||||
|
}
|
||||||
2619
src-elm/src/Main.elm
Normal file
48
src-elm/src/Pattern.elm
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
module Pattern exposing (Pattern, Token(..), fromString, toRegexp)
|
||||||
|
|
||||||
|
import Regex exposing (Regex)
|
||||||
|
|
||||||
|
|
||||||
|
type alias Pattern =
|
||||||
|
List Token
|
||||||
|
|
||||||
|
|
||||||
|
type Token
|
||||||
|
= RawString String
|
||||||
|
| Joker
|
||||||
|
|
||||||
|
|
||||||
|
fromString : String -> List Token
|
||||||
|
fromString string =
|
||||||
|
string
|
||||||
|
|> String.split "*"
|
||||||
|
|> List.map escapeSpecialChars
|
||||||
|
|> List.map RawString
|
||||||
|
|> List.intersperse Joker
|
||||||
|
|> List.filter (\t -> t /= RawString "")
|
||||||
|
|
||||||
|
|
||||||
|
toRegexp : Pattern -> Maybe Regex
|
||||||
|
toRegexp pattern =
|
||||||
|
pattern
|
||||||
|
|> List.map
|
||||||
|
(\token ->
|
||||||
|
case token of
|
||||||
|
RawString string ->
|
||||||
|
"(" ++ string ++ ")"
|
||||||
|
|
||||||
|
Joker ->
|
||||||
|
"(.*?)"
|
||||||
|
)
|
||||||
|
|> String.concat
|
||||||
|
|> Regex.fromString
|
||||||
|
|
||||||
|
|
||||||
|
escapeSpecialChars : String -> String
|
||||||
|
escapeSpecialChars string =
|
||||||
|
string
|
||||||
|
|> String.replace "." "\\."
|
||||||
|
|> String.replace "[" "\\["
|
||||||
|
|> String.replace "]" "\\]"
|
||||||
|
|> String.replace "(" "\\("
|
||||||
|
|> String.replace ")" "\\)"
|
||||||
53
src-elm/src/StringComparison.elm
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
module StringComparison exposing (..)
|
||||||
|
|
||||||
|
import List.Extra
|
||||||
|
|
||||||
|
|
||||||
|
isSimilarityLevelGreaterThan : String -> String -> Int -> Bool
|
||||||
|
isSimilarityLevelGreaterThan string1 string2 threshold =
|
||||||
|
let
|
||||||
|
len1 =
|
||||||
|
String.length string1
|
||||||
|
|
||||||
|
len2 =
|
||||||
|
String.length string2
|
||||||
|
|
||||||
|
chars1 =
|
||||||
|
string1
|
||||||
|
|> String.padRight (len2 - len1) ' '
|
||||||
|
|> String.toList
|
||||||
|
|
||||||
|
chars2 =
|
||||||
|
string2
|
||||||
|
|> String.padRight (len1 - len2) ' '
|
||||||
|
|> String.toList
|
||||||
|
|
||||||
|
charList =
|
||||||
|
List.Extra.zip chars1 chars2
|
||||||
|
|
||||||
|
similarCharCount =
|
||||||
|
List.foldl compareChars 0 charList
|
||||||
|
in
|
||||||
|
similarCharCount >= threshold
|
||||||
|
|
||||||
|
|
||||||
|
compareChars : ( Char, Char ) -> Int -> Int
|
||||||
|
compareChars ( char1, char2 ) currentLevel =
|
||||||
|
if char1 == char2 then
|
||||||
|
currentLevel + 1
|
||||||
|
|
||||||
|
else
|
||||||
|
currentLevel
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--compareChars : List Char -> List Char -> Int -> Int
|
||||||
|
--compareChars chars1 chars2 currentLevel =
|
||||||
|
-- if List.isEmpty chars1 || List.isEmpty chars2 then
|
||||||
|
-- currentLevel
|
||||||
|
--
|
||||||
|
-- else if List.head chars1 == List.head chars2 then
|
||||||
|
-- compareChars (List.drop 1 chars1) (List.drop 1 chars2) (currentLevel + 1)
|
||||||
|
--
|
||||||
|
-- else
|
||||||
|
-- compareChars (List.drop 1 chars1) (List.drop 1 chars2) currentLevel
|
||||||
250
src-elm/tests/FileTest.elm
Normal file
|
|
@ -0,0 +1,250 @@
|
||||||
|
module FileTest exposing (suite)
|
||||||
|
|
||||||
|
import Expect
|
||||||
|
import File exposing (File, FileStatus(..), extendSelectionToNext, extendSelectionToPrevious, selectNext, selectPrevious, selectSimilar, withStatus)
|
||||||
|
import Fixtures exposing (filteredDir1, filteredDir2, filteredDir3, filteredDir4, filteredDir5, filteredDir6, filteredDir7, filteredDir8)
|
||||||
|
import Iso8601
|
||||||
|
import Json.Decode exposing (Decoder, decodeString)
|
||||||
|
import Test exposing (Test, describe, test)
|
||||||
|
import Time exposing (millisToPosix)
|
||||||
|
|
||||||
|
|
||||||
|
suite : Test
|
||||||
|
suite =
|
||||||
|
describe "File module"
|
||||||
|
[ test "selectNext selects the next file" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List File
|
||||||
|
actual =
|
||||||
|
selectNext
|
||||||
|
[ filteredDir1
|
||||||
|
, filteredDir2 |> withStatus Selected
|
||||||
|
, filteredDir3
|
||||||
|
, filteredDir4
|
||||||
|
, filteredDir5
|
||||||
|
]
|
||||||
|
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[ filteredDir1
|
||||||
|
, filteredDir2
|
||||||
|
, filteredDir3 |> withStatus Selected
|
||||||
|
, filteredDir4
|
||||||
|
, filteredDir5
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
, test "selectNext does nothing when the currently selected file is the last" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List File
|
||||||
|
actual =
|
||||||
|
selectNext expected
|
||||||
|
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[ filteredDir1
|
||||||
|
, filteredDir2
|
||||||
|
, filteredDir3
|
||||||
|
, filteredDir4
|
||||||
|
, filteredDir5 |> withStatus Selected
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
, test "selectNext selects the last file in a list when none is selected" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List File
|
||||||
|
actual =
|
||||||
|
selectNext
|
||||||
|
[ filteredDir1
|
||||||
|
, filteredDir2
|
||||||
|
, filteredDir3
|
||||||
|
, filteredDir4
|
||||||
|
, filteredDir5
|
||||||
|
]
|
||||||
|
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[ filteredDir1
|
||||||
|
, filteredDir2
|
||||||
|
, filteredDir3
|
||||||
|
, filteredDir4
|
||||||
|
, filteredDir5 |> withStatus Selected
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
, test "selectPrevious selects the previous file in a list when there is one" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List File
|
||||||
|
actual =
|
||||||
|
selectPrevious
|
||||||
|
[ filteredDir1
|
||||||
|
, filteredDir2
|
||||||
|
, filteredDir3
|
||||||
|
, filteredDir4 |> withStatus Selected
|
||||||
|
, filteredDir5
|
||||||
|
]
|
||||||
|
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[ filteredDir1
|
||||||
|
, filteredDir2
|
||||||
|
, filteredDir3 |> withStatus Selected
|
||||||
|
, filteredDir4
|
||||||
|
, filteredDir5
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
, test "selectPrevious does nothing when the currently selected file is the first" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List File
|
||||||
|
actual =
|
||||||
|
selectPrevious expected
|
||||||
|
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[ filteredDir1 |> withStatus Selected
|
||||||
|
, filteredDir2
|
||||||
|
, filteredDir3
|
||||||
|
, filteredDir4
|
||||||
|
, filteredDir5
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
, test "selectPrevious selects the first file in a list when none is selected" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List File
|
||||||
|
actual =
|
||||||
|
selectPrevious
|
||||||
|
[ filteredDir1
|
||||||
|
, filteredDir2
|
||||||
|
, filteredDir3
|
||||||
|
, filteredDir4
|
||||||
|
, filteredDir5
|
||||||
|
]
|
||||||
|
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[ filteredDir1 |> withStatus Selected
|
||||||
|
, filteredDir2
|
||||||
|
, filteredDir3
|
||||||
|
, filteredDir4
|
||||||
|
, filteredDir5
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
, test "extendSelectionToNext selects the next file after the first selected" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List File
|
||||||
|
actual =
|
||||||
|
extendSelectionToNext
|
||||||
|
[ filteredDir1
|
||||||
|
, filteredDir2 |> withStatus Selected
|
||||||
|
, filteredDir3
|
||||||
|
, filteredDir4
|
||||||
|
, filteredDir5
|
||||||
|
]
|
||||||
|
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[ filteredDir1
|
||||||
|
, filteredDir2 |> withStatus Selected
|
||||||
|
, filteredDir3 |> withStatus Selected
|
||||||
|
, filteredDir4
|
||||||
|
, filteredDir5
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
, test "extendSelectionToPrevious selects the file before the first selected" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List File
|
||||||
|
actual =
|
||||||
|
extendSelectionToPrevious
|
||||||
|
[ filteredDir1
|
||||||
|
, filteredDir2
|
||||||
|
, filteredDir3 |> withStatus Selected
|
||||||
|
, filteredDir4
|
||||||
|
, filteredDir5
|
||||||
|
]
|
||||||
|
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[ filteredDir1
|
||||||
|
, filteredDir2 |> withStatus Selected
|
||||||
|
, filteredDir3 |> withStatus Selected
|
||||||
|
, filteredDir4
|
||||||
|
, filteredDir5
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
, test "selectSimilar selects the files with a name looking like the given one" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List File
|
||||||
|
actual =
|
||||||
|
selectSimilar
|
||||||
|
filteredDir6
|
||||||
|
10
|
||||||
|
[ filteredDir1
|
||||||
|
, filteredDir6 |> withStatus Selected
|
||||||
|
, filteredDir2
|
||||||
|
, filteredDir7
|
||||||
|
, filteredDir3
|
||||||
|
, filteredDir8
|
||||||
|
, filteredDir4 |> withStatus Selected
|
||||||
|
, filteredDir5
|
||||||
|
]
|
||||||
|
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[ filteredDir1
|
||||||
|
, filteredDir6 |> withStatus Selected
|
||||||
|
, filteredDir2
|
||||||
|
, filteredDir7 |> withStatus Selected
|
||||||
|
, filteredDir3
|
||||||
|
, filteredDir8 |> withStatus Selected
|
||||||
|
, filteredDir4
|
||||||
|
, filteredDir5
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
, test "filerDecoder is able to decode a file descriptor" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : File
|
||||||
|
actual =
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"DirPath": "/Users/pascal",
|
||||||
|
"IsDir": true,
|
||||||
|
"ModTime": "2025-12-16T19:43:08.307Z",
|
||||||
|
"Mode": 16832,
|
||||||
|
"Name": "Music",
|
||||||
|
"Size": 256
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|> decodeString File.fileDecoder
|
||||||
|
|> Result.toMaybe
|
||||||
|
|> Maybe.withDefault Fixtures.dir1
|
||||||
|
|
||||||
|
expected : File
|
||||||
|
expected =
|
||||||
|
{ parentPath = "/Users/pascal"
|
||||||
|
, isDir = True
|
||||||
|
, modTime = millisToPosix 1765914188307
|
||||||
|
, mode = 16832
|
||||||
|
, name = "Music"
|
||||||
|
, satisfiesFilter = False
|
||||||
|
, status = Unselected
|
||||||
|
, size = 256
|
||||||
|
}
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
]
|
||||||
112
src-elm/tests/Fixtures.elm
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
module Fixtures exposing (allDirs, dir1, dir2, dir3, dir4, dir5, filteredDir1, filteredDir2, filteredDir3, filteredDir4, filteredDir5, filteredDir6, filteredDir7, filteredDir8, model, windowsDir)
|
||||||
|
|
||||||
|
import File exposing (File, FileStatus(..), withName)
|
||||||
|
import Main exposing (Model, defaultModel)
|
||||||
|
import Time exposing (millisToPosix)
|
||||||
|
|
||||||
|
|
||||||
|
allDirs : List File
|
||||||
|
allDirs =
|
||||||
|
[ dir1
|
||||||
|
, dir2
|
||||||
|
, dir3
|
||||||
|
, dir4
|
||||||
|
, dir5
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
dir1 : File
|
||||||
|
dir1 =
|
||||||
|
{ isDir = True
|
||||||
|
, mode = 777
|
||||||
|
, modTime = millisToPosix 0
|
||||||
|
, name = "dirname"
|
||||||
|
, parentPath = "/some/path/"
|
||||||
|
, satisfiesFilter = False
|
||||||
|
, size = 0
|
||||||
|
, status = Unselected
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
dir2 : File
|
||||||
|
dir2 =
|
||||||
|
{ dir1 | name = "dir2" }
|
||||||
|
|
||||||
|
|
||||||
|
dir3 : File
|
||||||
|
dir3 =
|
||||||
|
{ dir1 | name = "different" }
|
||||||
|
|
||||||
|
|
||||||
|
dir4 : File
|
||||||
|
dir4 =
|
||||||
|
{ dir1
|
||||||
|
| name = "dirname4"
|
||||||
|
, parentPath = "/some/path/extended"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
dir5 : File
|
||||||
|
dir5 =
|
||||||
|
{ dir1
|
||||||
|
| name = "dir5"
|
||||||
|
, parentPath = "/some/path/extended"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
filteredDir1 : File
|
||||||
|
filteredDir1 =
|
||||||
|
dir1 |> withSatisfiedFilter
|
||||||
|
|
||||||
|
|
||||||
|
filteredDir2 : File
|
||||||
|
filteredDir2 =
|
||||||
|
dir2 |> withSatisfiedFilter
|
||||||
|
|
||||||
|
|
||||||
|
filteredDir3 : File
|
||||||
|
filteredDir3 =
|
||||||
|
dir3 |> withSatisfiedFilter
|
||||||
|
|
||||||
|
|
||||||
|
filteredDir4 : File
|
||||||
|
filteredDir4 =
|
||||||
|
dir4 |> withSatisfiedFilter
|
||||||
|
|
||||||
|
|
||||||
|
filteredDir5 : File
|
||||||
|
filteredDir5 =
|
||||||
|
dir5 |> withSatisfiedFilter
|
||||||
|
|
||||||
|
|
||||||
|
filteredDir6 : File
|
||||||
|
filteredDir6 =
|
||||||
|
filteredDir1 |> withName "a name with random chars 1 [123]"
|
||||||
|
|
||||||
|
|
||||||
|
filteredDir7 : File
|
||||||
|
filteredDir7 =
|
||||||
|
filteredDir1 |> withName "a name with random chars 2 [456]"
|
||||||
|
|
||||||
|
|
||||||
|
filteredDir8 : File
|
||||||
|
filteredDir8 =
|
||||||
|
filteredDir1 |> withName "a name with random chars 3 [678] - Foo"
|
||||||
|
|
||||||
|
|
||||||
|
model : Model
|
||||||
|
model =
|
||||||
|
{ defaultModel | destinationSubdirectories = [] }
|
||||||
|
|
||||||
|
|
||||||
|
windowsDir : File
|
||||||
|
windowsDir =
|
||||||
|
{ dir1
|
||||||
|
| name = "windows dir"
|
||||||
|
, parentPath = "C:\\some\\path\\extended"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
withSatisfiedFilter : File -> File
|
||||||
|
withSatisfiedFilter file =
|
||||||
|
{ file | satisfiesFilter = True }
|
||||||
291
src-elm/tests/MaintTest.elm
Normal file
|
|
@ -0,0 +1,291 @@
|
||||||
|
module MaintTest exposing (suite)
|
||||||
|
|
||||||
|
import Expect
|
||||||
|
import File exposing (File, FileStatus(..), defaultDir, withName, withParentPath, withStatus)
|
||||||
|
import Fixtures exposing (allDirs, dir1, dir2, dir3, dir4, dir5, filteredDir1, filteredDir2, filteredDir3, filteredDir4, filteredDir5, model, windowsDir)
|
||||||
|
import Main exposing (Model, defaultModel, filterDestinationDirectories, pathElements, select, truncateConcatenatedNames, windowsPathSep)
|
||||||
|
import Test exposing (Test, describe, test)
|
||||||
|
|
||||||
|
|
||||||
|
suite : Test
|
||||||
|
suite =
|
||||||
|
describe "Main module"
|
||||||
|
[ describe "filterDestinationDirectories"
|
||||||
|
[ test "identifies filenames containing a given string" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[ { dir1 | satisfiesFilter = True }
|
||||||
|
, dir2
|
||||||
|
, dir3
|
||||||
|
, { dir4 | satisfiesFilter = True }
|
||||||
|
, dir5
|
||||||
|
]
|
||||||
|
|
||||||
|
filteredModel : Model
|
||||||
|
filteredModel =
|
||||||
|
{ model
|
||||||
|
| destinationDirectoryFilter = "dirn"
|
||||||
|
, destinationSubdirectories = allDirs
|
||||||
|
}
|
||||||
|
|> filterDestinationDirectories
|
||||||
|
in
|
||||||
|
Expect.equal expected filteredModel.destinationSubdirectories
|
||||||
|
, test "identifies parent path containing a given string" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[ dir1
|
||||||
|
, dir2
|
||||||
|
, dir3
|
||||||
|
, { dir4 | satisfiesFilter = True }
|
||||||
|
, { dir5 | satisfiesFilter = True }
|
||||||
|
]
|
||||||
|
|
||||||
|
filteredModel : Model
|
||||||
|
filteredModel =
|
||||||
|
{ model
|
||||||
|
| destinationDirectoryFilter = "ext"
|
||||||
|
, destinationSubdirectories = allDirs
|
||||||
|
}
|
||||||
|
|> filterDestinationDirectories
|
||||||
|
in
|
||||||
|
Expect.equal expected filteredModel.destinationSubdirectories
|
||||||
|
, describe "pathElements"
|
||||||
|
[ test "pathElements returns the list of nested path and their names" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
elements : List File
|
||||||
|
elements =
|
||||||
|
pathElements defaultModel [] <| dir5.parentPath ++ defaultModel.pathSeparator ++ dir5.name
|
||||||
|
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[ defaultDir
|
||||||
|
|> withName "some"
|
||||||
|
|> withParentPath "/"
|
||||||
|
, defaultDir
|
||||||
|
|> withName "path"
|
||||||
|
|> withParentPath "/some"
|
||||||
|
, defaultDir
|
||||||
|
|> withName "extended"
|
||||||
|
|> withParentPath "/some/path"
|
||||||
|
, defaultDir
|
||||||
|
|> withName "dir5"
|
||||||
|
|> withParentPath "/some/path/extended"
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Expect.equal expected elements
|
||||||
|
, test "pathElements returns the list of nested path and their names under Windows" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
elements : List File
|
||||||
|
elements =
|
||||||
|
pathElements windowsModel [] <|
|
||||||
|
windowsDir.parentPath
|
||||||
|
++ windowsModel.pathSeparator
|
||||||
|
++ windowsDir.name
|
||||||
|
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[ defaultDir
|
||||||
|
|> withName "some"
|
||||||
|
|> withParentPath "C:"
|
||||||
|
, defaultDir
|
||||||
|
|> withName "path"
|
||||||
|
|> withParentPath "C:\\some"
|
||||||
|
, defaultDir
|
||||||
|
|> withName "extended"
|
||||||
|
|> withParentPath "C:\\some\\path"
|
||||||
|
, defaultDir
|
||||||
|
|> withName "windows dir"
|
||||||
|
|> withParentPath "C:\\some\\path\\extended"
|
||||||
|
]
|
||||||
|
|
||||||
|
windowsModel : Model
|
||||||
|
windowsModel =
|
||||||
|
{ defaultModel | pathSeparator = windowsPathSep }
|
||||||
|
in
|
||||||
|
Expect.equal expected elements
|
||||||
|
, test "pathElements ignores ." <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
elements : List File
|
||||||
|
elements =
|
||||||
|
pathElements defaultModel [] "."
|
||||||
|
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[]
|
||||||
|
in
|
||||||
|
Expect.equal expected elements
|
||||||
|
]
|
||||||
|
, test "truncate returns a list of files whose cumulated name length does not exceed given size" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List File
|
||||||
|
actual =
|
||||||
|
truncateConcatenatedNames 22 allDirs
|
||||||
|
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[ dir3
|
||||||
|
, dir4
|
||||||
|
, dir5
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
]
|
||||||
|
, describe "select"
|
||||||
|
[ test "select selects only the clicked file when neither CTRL or SHIFT are pressed" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List File
|
||||||
|
actual =
|
||||||
|
select model allDirs dir3
|
||||||
|
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[ dir1
|
||||||
|
, dir2
|
||||||
|
, dir3 |> withStatus Selected
|
||||||
|
, dir4
|
||||||
|
, dir5
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
, test "select unselects the clicked file when neither CTRL or SHIFT are pressed" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List File
|
||||||
|
actual =
|
||||||
|
select model
|
||||||
|
[ dir1
|
||||||
|
, dir2
|
||||||
|
, clickedFile
|
||||||
|
, dir4
|
||||||
|
, dir5
|
||||||
|
]
|
||||||
|
dir3
|
||||||
|
|
||||||
|
clickedFile : File
|
||||||
|
clickedFile =
|
||||||
|
dir3 |> withStatus Selected
|
||||||
|
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
allDirs
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
, test "select adds the clicked file to the current selection if it is unselected and CTRL is pressed" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List File
|
||||||
|
actual =
|
||||||
|
select
|
||||||
|
{ model | isControlPressed = True }
|
||||||
|
[ dir1
|
||||||
|
, dir2
|
||||||
|
, clickedFile
|
||||||
|
, dir4
|
||||||
|
, dir5
|
||||||
|
]
|
||||||
|
dir4
|
||||||
|
|
||||||
|
clickedFile : File
|
||||||
|
clickedFile =
|
||||||
|
dir3 |> withStatus Selected
|
||||||
|
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[ dir1
|
||||||
|
, dir2
|
||||||
|
, dir3 |> withStatus Selected
|
||||||
|
, dir4 |> withStatus Selected
|
||||||
|
, dir5
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
, test "select removes the clicked file from the current selection if it is selected and CTRL is pressed" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List File
|
||||||
|
actual =
|
||||||
|
select
|
||||||
|
{ model | isControlPressed = True }
|
||||||
|
[ dir1
|
||||||
|
, dir2
|
||||||
|
, dir3 |> withStatus Selected
|
||||||
|
, clickedFile
|
||||||
|
, dir5
|
||||||
|
]
|
||||||
|
clickedFile
|
||||||
|
|
||||||
|
clickedFile : File
|
||||||
|
clickedFile =
|
||||||
|
dir4 |> withStatus Selected
|
||||||
|
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[ dir1
|
||||||
|
, dir2
|
||||||
|
, dir3 |> withStatus Selected
|
||||||
|
, dir4
|
||||||
|
, dir5
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
, test "select selects the file range from the first selected to the clicked file when it is after and SHIFT is pressed" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List File
|
||||||
|
actual =
|
||||||
|
select
|
||||||
|
{ model | isShiftPressed = True }
|
||||||
|
[ filteredDir1
|
||||||
|
, filteredDir2 |> withStatus Selected
|
||||||
|
, filteredDir3
|
||||||
|
, filteredDir4
|
||||||
|
, filteredDir5
|
||||||
|
]
|
||||||
|
filteredDir4
|
||||||
|
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[ filteredDir1
|
||||||
|
, filteredDir2 |> withStatus Selected
|
||||||
|
, filteredDir3 |> withStatus Selected
|
||||||
|
, filteredDir4 |> withStatus Selected
|
||||||
|
, filteredDir5
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
, test "select selects the file range from the clicked file to the last selected when it is before and SHIFT is pressed" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List File
|
||||||
|
actual =
|
||||||
|
select
|
||||||
|
{ model | isShiftPressed = True }
|
||||||
|
[ filteredDir1
|
||||||
|
, filteredDir2
|
||||||
|
, filteredDir3
|
||||||
|
, filteredDir4 |> withStatus Selected
|
||||||
|
, filteredDir5
|
||||||
|
]
|
||||||
|
filteredDir2
|
||||||
|
|
||||||
|
expected : List File
|
||||||
|
expected =
|
||||||
|
[ filteredDir1
|
||||||
|
, filteredDir2 |> withStatus Selected
|
||||||
|
, filteredDir3 |> withStatus Selected
|
||||||
|
, filteredDir4 |> withStatus Selected
|
||||||
|
, filteredDir5
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
]
|
||||||
|
]
|
||||||
74
src-elm/tests/SearchReplaceTest.elm
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
module SearchReplaceTest exposing (suite)
|
||||||
|
|
||||||
|
import Expect
|
||||||
|
import Pattern exposing (Token(..), fromString)
|
||||||
|
import Test exposing (Test, describe, test)
|
||||||
|
|
||||||
|
|
||||||
|
suite : Test
|
||||||
|
suite =
|
||||||
|
describe "SearchReplace"
|
||||||
|
[ describe "fromString"
|
||||||
|
[ test "parses a string with a joker" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List Token
|
||||||
|
actual =
|
||||||
|
fromString "a*b"
|
||||||
|
|
||||||
|
expected : List Token
|
||||||
|
expected =
|
||||||
|
[ RawString "a", Joker, RawString "b" ]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
, test "parses a string without any joker" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List Token
|
||||||
|
actual =
|
||||||
|
fromString "ab"
|
||||||
|
|
||||||
|
expected : List Token
|
||||||
|
expected =
|
||||||
|
[ RawString "ab" ]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
, test "parses a string with several jokers" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List Token
|
||||||
|
actual =
|
||||||
|
fromString "ab*cd*"
|
||||||
|
|
||||||
|
expected : List Token
|
||||||
|
expected =
|
||||||
|
[ RawString "ab", Joker, RawString "cd", Joker ]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
, test "parses a string with a leading joker" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List Token
|
||||||
|
actual =
|
||||||
|
fromString "*abcd"
|
||||||
|
|
||||||
|
expected : List Token
|
||||||
|
expected =
|
||||||
|
[ Joker, RawString "abcd" ]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
, test "escapes special chars" <|
|
||||||
|
\_ ->
|
||||||
|
let
|
||||||
|
actual : List Token
|
||||||
|
actual =
|
||||||
|
fromString ".[]()"
|
||||||
|
|
||||||
|
expected : List Token
|
||||||
|
expected =
|
||||||
|
--[ RawString "\\.\\[\\]\\(\\)" ]
|
||||||
|
[ RawString "\\.\\[\\]\\(\\)" ]
|
||||||
|
in
|
||||||
|
Expect.equal expected actual
|
||||||
|
]
|
||||||
|
]
|
||||||
23
src-elm/tests/StringComparisonTest.elm
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
module StringComparisonTest exposing (..)
|
||||||
|
|
||||||
|
import Expect exposing (Expectation)
|
||||||
|
import StringComparison exposing (isSimilarityLevelGreaterThan)
|
||||||
|
import Test exposing (Test, describe, test)
|
||||||
|
|
||||||
|
|
||||||
|
suite : Test
|
||||||
|
suite =
|
||||||
|
describe "isSimilarityLevelGreaterThan"
|
||||||
|
[ test "returns true when comparing string with more than level common chars" <|
|
||||||
|
\_ ->
|
||||||
|
isSimilarityLevelGreaterThan "abcdeijk" "abcdefgh" 5
|
||||||
|
|> Expect.equal True
|
||||||
|
, test "detects similarity even if the first string is shorter than the second" <|
|
||||||
|
\_ ->
|
||||||
|
isSimilarityLevelGreaterThan "zbcde" "abcdefgh" 4
|
||||||
|
|> Expect.equal True
|
||||||
|
, test "detects similarity even if the first string is longer than the second" <|
|
||||||
|
\_ ->
|
||||||
|
isSimilarityLevelGreaterThan "zBCDExkHamsld" "aBCDEfgH" 5
|
||||||
|
|> Expect.equal True
|
||||||
|
]
|
||||||
7
src-tauri/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
|
||||||
|
# Generated by Tauri
|
||||||
|
# will have schema files for capabilities auto-completion
|
||||||
|
/gen/schemas
|
||||||
5169
src-tauri/Cargo.lock
generated
Normal file
26
src-tauri/Cargo.toml
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
[package]
|
||||||
|
name = "file-organizer"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A Tauri App"
|
||||||
|
authors = ["you"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
# The `_lib` suffix may seem redundant but it is necessary
|
||||||
|
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||||
|
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||||
|
name = "file_organizer_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2", features = [] }
|
||||||
|
tauri-plugin-opener = "2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
tauri-plugin-fs = "2.4.5"
|
||||||
|
|
||||||
3
src-tauri/build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
22
src-tauri/capabilities/default.json
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Capability for the main window",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"opener:default",
|
||||||
|
"fs:default",
|
||||||
|
"fs:read-all",
|
||||||
|
"fs:write-all",
|
||||||
|
"fs:allow-rename",
|
||||||
|
"fs:allow-mkdir",
|
||||||
|
"fs:allow-exists",
|
||||||
|
"fs:allow-watch",
|
||||||
|
"fs:read-dirs",
|
||||||
|
{
|
||||||
|
"identifier": "fs:scope",
|
||||||
|
"allow": [{ "path": "**/*" }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 974 B |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
17
src-tauri/src/lib.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_home_directory() {
|
||||||
|
println!("I was invoked from JavaScript!");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
|
||||||
|
tauri::Builder::default()
|
||||||
|
.invoke_handler(tauri::generate_handler![get_home_directory])
|
||||||
|
.plugin(tauri_plugin_fs::init())
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
6
src-tauri/src/main.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
file_organizer_lib::run()
|
||||||
|
}
|
||||||
36
src-tauri/tauri.conf.json
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "file-organizer",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "com.fileorganizer.app",
|
||||||
|
"build": {
|
||||||
|
"frontendDist": "../src"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"withGlobalTauri": true,
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "File Organizer",
|
||||||
|
"width": 800,
|
||||||
|
"height": 600
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"pattern": {
|
||||||
|
"use": "brownfield"
|
||||||
|
},
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
93
src/assets/fonts/OFL.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
BIN
src/assets/fonts/nunito-v16-latin-regular.woff2
Normal file
16758
src/elm.js
Normal file
39
src/example/index.html
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Tauri App</title>
|
||||||
|
<script type="module" src="/main.js" defer></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<main class="container">
|
||||||
|
<h1>Welcome to Tauri</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<a href="https://tauri.app" target="_blank">
|
||||||
|
<img src="/assets/tauri.svg" class="logo tauri" alt="Tauri logo" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://developer.mozilla.org/en-US/docs/Web/JavaScript"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/assets/javascript.svg"
|
||||||
|
class="logo vanilla"
|
||||||
|
alt="JavaScript logo"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p>Click on the Tauri logo to learn more about the framework</p>
|
||||||
|
|
||||||
|
<form class="row" id="greet-form">
|
||||||
|
<input id="greet-input" placeholder="Enter a name..." />
|
||||||
|
<button type="submit">Greet</button>
|
||||||
|
</form>
|
||||||
|
<p id="greet-msg"></p>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
18
src/example/main.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
const { invoke } = window.__TAURI__.core;
|
||||||
|
|
||||||
|
let greetInputEl;
|
||||||
|
let greetMsgEl;
|
||||||
|
|
||||||
|
async function greet() {
|
||||||
|
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||||
|
greetMsgEl.textContent = await invoke("greet", { name: greetInputEl.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
|
greetInputEl = document.querySelector("#greet-input");
|
||||||
|
greetMsgEl = document.querySelector("#greet-msg");
|
||||||
|
document.querySelector("#greet-form").addEventListener("submit", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
greet();
|
||||||
|
});
|
||||||
|
});
|
||||||
17
src/index.html
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>File Organizer</title>
|
||||||
|
<link rel="stylesheet" href="main.css" />
|
||||||
|
<link rel="stylesheet" href="slider.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="elm"></div>
|
||||||
|
|
||||||
|
<script src="elm.js"></script>
|
||||||
|
<script src="wrapper.js" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
342
src/main.css
Normal file
|
|
@ -0,0 +1,342 @@
|
||||||
|
:root {
|
||||||
|
--main-color: rgb(98, 189, 176);
|
||||||
|
--faded-main-color: rgba(98, 189, 176, 0.59);
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background-color: lightgrey;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
color: black;
|
||||||
|
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||||
|
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||||
|
sans-serif;
|
||||||
|
font-size: smaller;
|
||||||
|
cursor: default; /* enforces the default cursor of the platform */
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Nunito";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local(""),
|
||||||
|
url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: small;
|
||||||
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box {
|
||||||
|
height: 25px;
|
||||||
|
min-height: 25px;
|
||||||
|
line-height:20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 10px 1px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 3px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 60px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
margin: 0 0 0 20px;
|
||||||
|
padding: 0 8px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.panel-header .btn {
|
||||||
|
min-width: 30px;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box .btn {
|
||||||
|
width: 60px;
|
||||||
|
height: 25px;
|
||||||
|
line-height: 25px;
|
||||||
|
margin: 0 0 0 20px;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: darkgrey;
|
||||||
|
}
|
||||||
|
|
||||||
|
#delete-button {
|
||||||
|
background-color: red;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
#delete-button:hover {
|
||||||
|
background-color: #801515;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
height: 15px;
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box .input {
|
||||||
|
}
|
||||||
|
|
||||||
|
.file.dir {
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box .input {
|
||||||
|
height: 25px;
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
line-height: 15px;
|
||||||
|
padding: 0 10px;
|
||||||
|
background-color: rgba(240, 240, 240, 1);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box .input:hover {
|
||||||
|
border: none;
|
||||||
|
background-color: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box .input:focus {
|
||||||
|
border: none;
|
||||||
|
background-color: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: flex-end;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file {
|
||||||
|
background: ghostwhite;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file:nth-child(odd) { background: #DDD; }
|
||||||
|
|
||||||
|
.file.selected {
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: rgb(121, 219, 190);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file.marked-for-deletion {
|
||||||
|
font-weight: bold;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename {
|
||||||
|
align-self: flex-start;
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 10px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filemodificationdate {
|
||||||
|
width: 80px;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 20px 1fr 50px;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug {
|
||||||
|
grid-template-rows: 20px 1fr 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
grid-column-start: 1;
|
||||||
|
grid-column-end: 3;
|
||||||
|
grid-row-start: 1;
|
||||||
|
grid-row-end: 2;
|
||||||
|
background: lightgrey;
|
||||||
|
color: black;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container-left {
|
||||||
|
grid-column-start: 1;
|
||||||
|
grid-column-end: 2;
|
||||||
|
grid-row-start: 2;
|
||||||
|
grid-row-end: 4;
|
||||||
|
background: lightgrey;
|
||||||
|
padding: 5px;
|
||||||
|
min-height: 0; /* Prevents the grid to extend outside of the visible area */
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 2fr 4fr;
|
||||||
|
grid-row-gap: 5px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container-right {
|
||||||
|
grid-column-start: 2;
|
||||||
|
grid-column-end: 3;
|
||||||
|
grid-row-start: 2;
|
||||||
|
grid-row-end: 4;
|
||||||
|
background: lightgrey;
|
||||||
|
padding: 5px;
|
||||||
|
min-height: 0; /* Prevents the grid to extend outside of the visible area */
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 2fr 4fr;
|
||||||
|
grid-row-gap: 5px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
grid-column-start: 1;
|
||||||
|
grid-column-end: 3;
|
||||||
|
grid-row-start: 3;
|
||||||
|
grid-row-end: 4;
|
||||||
|
background: lightgrey;
|
||||||
|
color: black;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 5px 5px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
border: 1px solid darkgrey;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-content {
|
||||||
|
padding: 5px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
height: 20px;
|
||||||
|
min-height: 20px;
|
||||||
|
padding: 5px;
|
||||||
|
background-color: var(--faded-main-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header.unfocused > h2 {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header.focused {
|
||||||
|
background-color: var(--main-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* force scrollbar to be always visible on Mac OS */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: rgba(0, 0, 0, .5);
|
||||||
|
box-shadow: 0 0 1px rgba(255, 255, 255, .5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
border: 1px solid red;
|
||||||
|
background-color: #FFAAAA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.link {
|
||||||
|
margin: 0 2px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form > input {
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form > button {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form > input {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#similarity-level {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug {
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spin {
|
||||||
|
border: 16px solid var(--main-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top: 16px solid var(--main-color);
|
||||||
|
border-right: 16px solid var(--faded-main-color);
|
||||||
|
border-bottom: 16px solid var(--faded-main-color);
|
||||||
|
border-left: 16px solid var(--faded-main-color);
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
-webkit-animation: spin 1.5s linear infinite;
|
||||||
|
animation: spin 1.5s linear infinite;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 50%;
|
||||||
|
}
|
||||||
|
@-webkit-keyframes spin {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
196
src/slider.css
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
.input-range-container {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* In the case of the double slider, each individual slider has it's width set to 100% of the parent element. Therefore, in order to set a fixed width, it is recommended to set it on the parent element and not override the width of the range slider. This is to ensure the flexibility of the component. */
|
||||||
|
.input-range-container,
|
||||||
|
.input-range {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range,
|
||||||
|
.input-range:hover,
|
||||||
|
.input-range:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
overflow: visible;
|
||||||
|
pointer-events: none;
|
||||||
|
height: 14px;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range::-moz-focus-outer {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
height: 10px;
|
||||||
|
width: 10px;
|
||||||
|
border: none;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 100%;
|
||||||
|
box-shadow: 0 0 0 2px rgba(33, 34, 36, 0.07);
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: all;
|
||||||
|
z-index: 2;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range::-moz-range-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range::-moz-range-thumb {
|
||||||
|
height: 10px;
|
||||||
|
width: 10px;
|
||||||
|
border: none;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 100%;
|
||||||
|
box-shadow: 0 0 0 2px rgba(33, 34, 36, 0.07);
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: all;
|
||||||
|
z-index: 2;
|
||||||
|
position: relative;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range::-ms-track {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range::-ms-fill-lower {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range::-ms-thumb {
|
||||||
|
height: 10px;
|
||||||
|
width: 10px;
|
||||||
|
border: none;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 100%;
|
||||||
|
box-shadow: 0 0 0 2px rgba(33, 34, 36, 0.07);
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: all;
|
||||||
|
z-index: 2;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range:disabled, .input-range:disabled:hover {
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range:disabled::-webkit-slider-thumb, .input-range:disabled:hover::-webkit-slider-thumb {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range:disabled::-moz-range-thumb, .input-range:disabled:hover::-moz-range-thumb {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range:disabled::-ms-thumb, .input-range:disabled:hover::-ms-thumb {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range:disabled ~ .input-range__track, .input-range:disabled:hover ~ .input-range__track {
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range:disabled ~ .input-range__progress, .input-range:disabled:hover ~ .input-range__progress {
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: #dcdee1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-thumb {
|
||||||
|
height: 10px;
|
||||||
|
width: 10px;
|
||||||
|
border: none;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 100%;
|
||||||
|
box-shadow: 0 0 0 2px rgba(33, 34, 36, 0.07);
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: all;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-thumb--first {
|
||||||
|
margin-left: -16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-thumb--second {
|
||||||
|
margin-left: -32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range--first {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range--second {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range__track,
|
||||||
|
.input-range__progress {
|
||||||
|
border-radius: 8px;
|
||||||
|
position: absolute;
|
||||||
|
height: 4px;
|
||||||
|
margin-top: -2px;
|
||||||
|
top: 50%;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range__track:hover,
|
||||||
|
.input-range__progress:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range__track {
|
||||||
|
background-color: #dcdee1;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range__track:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range__progress {
|
||||||
|
background-color: #00a4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range-labels-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range-label {
|
||||||
|
font-size: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range-label--current-value {
|
||||||
|
text-align: center;
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range-label:first-child {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-range-label:last-child {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
62
src/wrapper.js
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
const app = Elm.Main.init({ node: document.getElementById("elm") });
|
||||||
|
const path = window.__TAURI__.path;
|
||||||
|
const fs = window.__TAURI__.fs;
|
||||||
|
const { invoke } = window.__TAURI__.core;
|
||||||
|
|
||||||
|
// Get current directory path
|
||||||
|
app.ports.getCurrentDirectoryPath.subscribe(function () {
|
||||||
|
path
|
||||||
|
.homeDir()
|
||||||
|
.then((home) => {
|
||||||
|
app.ports.receiveCurrentDirectoryPath.send(home);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
app.ports.receiveError.send(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getFileMetadata(directory, file) {
|
||||||
|
if (file.name.startsWith(".")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = directory + "/" + file.name;
|
||||||
|
return await fs
|
||||||
|
.stat(filePath)
|
||||||
|
.then((metadata) => {
|
||||||
|
const value = {
|
||||||
|
IsDir: file.isDirectory,
|
||||||
|
Mode: metadata.mode,
|
||||||
|
ModTime: metadata.mtime.toISOString(),
|
||||||
|
Name: file.name,
|
||||||
|
DirPath: directory,
|
||||||
|
Size: metadata.size,
|
||||||
|
};
|
||||||
|
return value;
|
||||||
|
})
|
||||||
|
.catch((msg) => {
|
||||||
|
console.error(msg);
|
||||||
|
app.ports.receiveError.send(msg);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get source directory content
|
||||||
|
app.ports.getSourceDirectoryContent.subscribe(function (directoryName) {
|
||||||
|
fs.readDir(directoryName, { recursive: false })
|
||||||
|
.then((files) => {
|
||||||
|
Promise.all(
|
||||||
|
files.map((file) => {
|
||||||
|
return getFileMetadata(directoryName, file);
|
||||||
|
}),
|
||||||
|
).then((metadata) => {
|
||||||
|
const filteredMetadata = metadata.filter((m) => m != null);
|
||||||
|
app.ports.receiveSourceDirectoryContent.send(filteredMetadata);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((msg) => {
|
||||||
|
console.error(msg);
|
||||||
|
app.ports.receiveError.send(msg);
|
||||||
|
});
|
||||||
|
});
|
||||||