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);
|
||||
});
|
||||
});
|
||||