Integrate elm App with Tauri. Can navigate into source directory.

This commit is contained in:
Pascal Le Merrer 2026-01-19 14:13:52 +01:00
commit 94003989ef
50 changed files with 41456 additions and 0 deletions

25
.gitignore vendored Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

3068
src-elm/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

8
src-elm/package.json Normal file
View 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
View 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": {}
}
}

View 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
View 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

File diff suppressed because it is too large Load diff

48
src-elm/src/Pattern.elm Normal file
View 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 ")" "\\)"

View 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
View 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
View 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
View 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
]
]

View 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
]
]

View 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
View 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

File diff suppressed because it is too large Load diff

26
src-tauri/Cargo.toml Normal file
View 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
View file

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

17
src-tauri/src/lib.rs Normal file
View 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
View 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
View 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
View 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.

Binary file not shown.

16758
src/elm.js Normal file

File diff suppressed because it is too large Load diff

39
src/example/index.html Normal file
View 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
View 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
View 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
View 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
View 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
View 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);
});
});