diff --git a/thinks/forms.py b/thinks/forms.py index 1f015ca..15f1f6f 100644 --- a/thinks/forms.py +++ b/thinks/forms.py @@ -3,6 +3,7 @@ from django.conf import settings from .models import Think + class CreateThinkForm(forms.ModelForm): class Meta: model = Think @@ -15,11 +16,13 @@ class CreateThinkForm(forms.ModelForm): instance.root.mkdir(exist_ok=True,parents=True) return instance + class RemixThinkForm(forms.ModelForm): class Meta: model = Think fields = [] + class RenameThinkForm(forms.ModelForm): class Meta: model = Think @@ -49,6 +52,7 @@ class RenameThinkForm(forms.ModelForm): instance = super().save(commit) return instance + class SaveFileForm(forms.ModelForm): path = forms.CharField() content = forms.CharField(required=False, widget=forms.Textarea) @@ -72,6 +76,7 @@ class SaveFileForm(forms.ModelForm): return super().save(commit) + class RenameFileForm(forms.ModelForm): path = forms.CharField() newpath = forms.CharField() @@ -100,6 +105,7 @@ class RenameFileForm(forms.ModelForm): return super().save(commit) + class DeleteFileForm(forms.ModelForm): path = forms.CharField() @@ -121,6 +127,7 @@ class DeleteFileForm(forms.ModelForm): return super().save(commit) + class RunCommandForm(forms.ModelForm): command = forms.CharField() @@ -128,9 +135,51 @@ class RunCommandForm(forms.ModelForm): model = Think fields = [] + class GitCommitForm(forms.ModelForm): message = forms.CharField() class Meta: model = Think fields = [] + + +class MultipleFileInput(forms.ClearableFileInput): + allow_multiple_selected = True + + +class MultipleFileField(forms.FileField): + def __init__(self, *args, **kwargs): + kwargs.setdefault("widget", MultipleFileInput()) + super().__init__(*args, **kwargs) + + def clean(self, data, initial=None): + single_file_clean = super().clean + if isinstance(data, (list, tuple)): + result = [single_file_clean(d, initial) for d in data] + else: + result = [single_file_clean(data, initial)] + return result + + +class UploadFileForm(forms.ModelForm): + files = MultipleFileField() + + class Meta: + model = Think + fields = [] + + + def save(self, commit=False): + think = self.instance + + files = self.cleaned_data['files'] + + for uf in files: + path = think.file_path(uf.name) + print(uf, path) + with open(path, 'wb') as df: + for chunk in uf.chunks(): + df.write(chunk) + + return super().save(commit) diff --git a/thinks/jujutsu.py b/thinks/jujutsu.py index 418a760..bc2eb98 100644 --- a/thinks/jujutsu.py +++ b/thinks/jujutsu.py @@ -26,14 +26,19 @@ class JJController: ) return res - def init_jj(self): + def init_jj(self, force=False): print("Init jj") - if not (self.root / '.jj').exists(): + res = self.run(['jj','git','remote','list']) + if force or not (self.root / '.jj').exists(): self.run(['jj','git','init']) self.ignore_paths(['.make.*']) git_url = settings.GIT_REPO_URL_TEMPLATE.format(name=self.think.slug) self.run(['jj','git','remote','add','origin', git_url]) + def clean_paths(self, paths): + paths = [self.root / p for p in paths] + return [str(p.relative_to(self.root)) for p in paths if p.is_relative_to(self.root)] + @ensure_jj def ignore_paths(self, paths): paths = self.clean_paths(paths) diff --git a/thinks/static/thinks/think-editor.css b/thinks/static/thinks/think-editor.css index 05313da..7dd587a 100644 --- a/thinks/static/thinks/think-editor.css +++ b/thinks/static/thinks/think-editor.css @@ -109,8 +109,12 @@ input:not([type="hidden"]) ~ button { input { border: thin solid currentColor; + height: 100%; + background: var(--default-background); +} +input[type="file"] { + font-size: 0.66rem; } - .field { display: flex; @@ -239,6 +243,12 @@ input { z-index: 1; } + & #file-form { + & > :is(img,video) { + width: 100%; + } + } + & code-editor { background: var(--default-background); display: block; diff --git a/thinks/static/thinks/think-editor.js b/thinks/static/thinks/think-editor.js index ed83263..1de6cd4 100644 --- a/thinks/static/thinks/think-editor.js +++ b/thinks/static/thinks/think-editor.js @@ -5546,7 +5546,9 @@ var $author$project$App$Model = function (show_preview) { return function (file_content) { return function (is_dir) { return function (elm_packages) { - return {command_to_run: command_to_run, commit_message: commit_message, content_changed: content_changed, csrf_token: csrf_token, editor_size: editor_size, elm_packages: elm_packages, file_content: file_content, file_path: file_path, files: files, is_dir: is_dir, jj_status: jj_status, log: log, preview_url: preview_url, selected_package: selected_package, show_log: show_log, show_preview: show_preview, slug: slug}; + return function (mime_type) { + return {command_to_run: command_to_run, commit_message: commit_message, content_changed: content_changed, csrf_token: csrf_token, editor_size: editor_size, elm_packages: elm_packages, file_content: file_content, file_path: file_path, files: files, is_dir: is_dir, jj_status: jj_status, log: log, mime_type: mime_type, preview_url: preview_url, selected_package: selected_package, show_log: show_log, show_preview: show_preview, slug: slug}; + }; }; }; }; @@ -5564,6 +5566,10 @@ var $author$project$App$Model = function (show_preview) { }; }; }; +var $danyx23$elm_mimetype$MimeType$PlainText = {$: 'PlainText'}; +var $danyx23$elm_mimetype$MimeType$Text = function (a) { + return {$: 'Text', a: a}; +}; var $elm_community$json_extra$Json$Decode$Extra$andMap = $elm$json$Json$Decode$map2($elm$core$Basics$apR); var $elm$json$Json$Decode$bool = _Json_decodeBool; var $elm$core$Basics$composeR = F3( @@ -5597,8 +5603,201 @@ var $author$project$App$decode_file_info = A4( A2($elm$json$Json$Decode$field, 'name', $elm$json$Json$Decode$string), A2($elm$json$Json$Decode$field, 'is_dir', $elm$json$Json$Decode$bool), A2($elm$json$Json$Decode$field, 'path', $elm$json$Json$Decode$string)); +var $danyx23$elm_mimetype$MimeType$App = function (a) { + return {$: 'App', a: a}; +}; +var $danyx23$elm_mimetype$MimeType$Audio = function (a) { + return {$: 'Audio', a: a}; +}; +var $danyx23$elm_mimetype$MimeType$Avi = {$: 'Avi'}; +var $danyx23$elm_mimetype$MimeType$Css = {$: 'Css'}; +var $danyx23$elm_mimetype$MimeType$Excel = {$: 'Excel'}; +var $danyx23$elm_mimetype$MimeType$ExcelXml = {$: 'ExcelXml'}; +var $danyx23$elm_mimetype$MimeType$Gif = {$: 'Gif'}; +var $danyx23$elm_mimetype$MimeType$Html = {$: 'Html'}; +var $danyx23$elm_mimetype$MimeType$Image = function (a) { + return {$: 'Image', a: a}; +}; +var $danyx23$elm_mimetype$MimeType$Jpeg = {$: 'Jpeg'}; +var $danyx23$elm_mimetype$MimeType$Json = {$: 'Json'}; +var $danyx23$elm_mimetype$MimeType$Mp3 = {$: 'Mp3'}; +var $danyx23$elm_mimetype$MimeType$Mp4 = {$: 'Mp4'}; +var $danyx23$elm_mimetype$MimeType$Mpeg = {$: 'Mpeg'}; +var $danyx23$elm_mimetype$MimeType$Ogg = {$: 'Ogg'}; +var $danyx23$elm_mimetype$MimeType$OtherAudio = function (a) { + return {$: 'OtherAudio', a: a}; +}; +var $danyx23$elm_mimetype$MimeType$OtherImage = function (a) { + return {$: 'OtherImage', a: a}; +}; +var $danyx23$elm_mimetype$MimeType$OtherMimeType = function (a) { + return {$: 'OtherMimeType', a: a}; +}; +var $danyx23$elm_mimetype$MimeType$OtherText = function (a) { + return {$: 'OtherText', a: a}; +}; +var $danyx23$elm_mimetype$MimeType$OtherVideo = function (a) { + return {$: 'OtherVideo', a: a}; +}; +var $danyx23$elm_mimetype$MimeType$Pdf = {$: 'Pdf'}; +var $danyx23$elm_mimetype$MimeType$Png = {$: 'Png'}; +var $danyx23$elm_mimetype$MimeType$PowerPoint = {$: 'PowerPoint'}; +var $danyx23$elm_mimetype$MimeType$PowerPointXml = {$: 'PowerPointXml'}; +var $danyx23$elm_mimetype$MimeType$Quicktime = {$: 'Quicktime'}; +var $danyx23$elm_mimetype$MimeType$Video = function (a) { + return {$: 'Video', a: a}; +}; +var $danyx23$elm_mimetype$MimeType$Wav = {$: 'Wav'}; +var $danyx23$elm_mimetype$MimeType$Webm = {$: 'Webm'}; +var $danyx23$elm_mimetype$MimeType$Word = {$: 'Word'}; +var $danyx23$elm_mimetype$MimeType$WordXml = {$: 'WordXml'}; +var $danyx23$elm_mimetype$MimeType$Xml = {$: 'Xml'}; +var $elm$core$String$toLower = _String_toLower; +var $danyx23$elm_mimetype$MimeType$parseMimeType = function (mimeString) { + var _v0 = $elm$core$String$toLower(mimeString); + switch (_v0) { + case '': + return $elm$core$Maybe$Nothing; + case 'image/jpeg': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Image($danyx23$elm_mimetype$MimeType$Jpeg)); + case 'image/png': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Image($danyx23$elm_mimetype$MimeType$Png)); + case 'image/gif': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Image($danyx23$elm_mimetype$MimeType$Gif)); + case 'audio/mp3': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Audio($danyx23$elm_mimetype$MimeType$Mp3)); + case 'audio/mpeg': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Audio($danyx23$elm_mimetype$MimeType$Mp3)); + case 'audio/wav': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Audio($danyx23$elm_mimetype$MimeType$Wav)); + case 'audio/ogg': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Audio($danyx23$elm_mimetype$MimeType$Ogg)); + case 'video/mp4': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Video($danyx23$elm_mimetype$MimeType$Mp4)); + case 'video/mpeg': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Video($danyx23$elm_mimetype$MimeType$Mpeg)); + case 'video/quicktime': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Video($danyx23$elm_mimetype$MimeType$Quicktime)); + case 'video/avi': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Video($danyx23$elm_mimetype$MimeType$Avi)); + case 'video/webm': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Video($danyx23$elm_mimetype$MimeType$Webm)); + case 'text/plain': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Text($danyx23$elm_mimetype$MimeType$PlainText)); + case 'text/html': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Text($danyx23$elm_mimetype$MimeType$Html)); + case 'text/css': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Text($danyx23$elm_mimetype$MimeType$Css)); + case 'text/xml': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Text($danyx23$elm_mimetype$MimeType$Xml)); + case 'application/json': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Text($danyx23$elm_mimetype$MimeType$Json)); + case 'application/msword': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$App($danyx23$elm_mimetype$MimeType$Word)); + case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$App($danyx23$elm_mimetype$MimeType$WordXml)); + case 'application/vnd.ms-excel': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$App($danyx23$elm_mimetype$MimeType$Excel)); + case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$App($danyx23$elm_mimetype$MimeType$ExcelXml)); + case 'application/vnd.ms-powerpoint': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$App($danyx23$elm_mimetype$MimeType$PowerPoint)); + case 'application/vnd.openxmlformats-officedocument.presentationml.presentation': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$App($danyx23$elm_mimetype$MimeType$PowerPointXml)); + case 'application/pdf': + return $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$App($danyx23$elm_mimetype$MimeType$Pdf)); + default: + var lowerCaseMimeString = _v0; + return A2($elm$core$String$startsWith, 'image/', lowerCaseMimeString) ? $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Image( + $danyx23$elm_mimetype$MimeType$OtherImage( + A2( + $elm$core$String$dropLeft, + $elm$core$String$length('image/'), + lowerCaseMimeString)))) : (A2($elm$core$String$startsWith, 'audio/', lowerCaseMimeString) ? $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Audio( + $danyx23$elm_mimetype$MimeType$OtherAudio( + A2( + $elm$core$String$dropLeft, + $elm$core$String$length('audio/'), + lowerCaseMimeString)))) : (A2($elm$core$String$startsWith, 'video/', lowerCaseMimeString) ? $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Video( + $danyx23$elm_mimetype$MimeType$OtherVideo( + A2( + $elm$core$String$dropLeft, + $elm$core$String$length('video/'), + lowerCaseMimeString)))) : (A2($elm$core$String$startsWith, 'text/', lowerCaseMimeString) ? $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$Text( + $danyx23$elm_mimetype$MimeType$OtherText( + A2( + $elm$core$String$dropLeft, + $elm$core$String$length('text/'), + lowerCaseMimeString)))) : $elm$core$Maybe$Just( + $danyx23$elm_mimetype$MimeType$OtherMimeType(lowerCaseMimeString))))); + } +}; +var $elm$core$Maybe$withDefault = F2( + function (_default, maybe) { + if (maybe.$ === 'Just') { + var value = maybe.a; + return value; + } else { + return _default; + } + }); +var $author$project$App$decode_mime_type = A2( + $elm$json$Json$Decode$map, + A2( + $elm$core$Basics$composeR, + $danyx23$elm_mimetype$MimeType$parseMimeType, + $elm$core$Maybe$withDefault( + $danyx23$elm_mimetype$MimeType$Text($danyx23$elm_mimetype$MimeType$PlainText))), + $elm$json$Json$Decode$string); var $author$project$App$NotLoaded = {$: 'NotLoaded'}; -var $author$project$App$init_model = {command_to_run: '', commit_message: '', content_changed: false, csrf_token: '', editor_size: 0.5, elm_packages: _List_Nil, file_content: 'The editor has not loaded successfully.', file_path: 'Oops!', files: _List_Nil, is_dir: false, jj_status: '', log: $author$project$App$NotLoaded, preview_url: '', selected_package: '', show_log: false, show_preview: true, slug: 'not-a-real-thing'}; +var $author$project$App$init_model = { + command_to_run: '', + commit_message: '', + content_changed: false, + csrf_token: '', + editor_size: 0.5, + elm_packages: _List_Nil, + file_content: 'The editor has not loaded successfully.', + file_path: 'Oops!', + files: _List_Nil, + is_dir: false, + jj_status: '', + log: $author$project$App$NotLoaded, + mime_type: $danyx23$elm_mimetype$MimeType$Text($danyx23$elm_mimetype$MimeType$PlainText), + preview_url: '', + selected_package: '', + show_log: false, + show_preview: true, + slug: 'not-a-real-thing' +}; var $elm$json$Json$Decode$list = _Json_decodeList; var $elm$json$Json$Decode$oneOf = _Json_oneOf; var $elm$core$Result$withDefault = F2( @@ -5618,38 +5817,52 @@ var $author$project$App$load_flags = A2( $elm$json$Json$Decode$oneOf( _List_fromArray( [ - A2( - $elm$json$Json$Decode$field, - 'elm_packages', - $elm$json$Json$Decode$list($author$project$App$decode_elm_package)), - $elm$json$Json$Decode$succeed(_List_Nil) + A2($elm$json$Json$Decode$field, 'mime_type', $author$project$App$decode_mime_type), + $elm$json$Json$Decode$succeed( + $danyx23$elm_mimetype$MimeType$Text($danyx23$elm_mimetype$MimeType$PlainText)) ])), A2( $elm_community$json_extra$Json$Decode$Extra$andMap, - A2($elm$json$Json$Decode$field, 'is_dir', $elm$json$Json$Decode$bool), + $elm$json$Json$Decode$oneOf( + _List_fromArray( + [ + A2( + $elm$json$Json$Decode$field, + 'elm_packages', + $elm$json$Json$Decode$list($author$project$App$decode_elm_package)), + $elm$json$Json$Decode$succeed(_List_Nil) + ])), A2( $elm_community$json_extra$Json$Decode$Extra$andMap, - A2($elm$json$Json$Decode$field, 'file_content', $elm$json$Json$Decode$string), + A2($elm$json$Json$Decode$field, 'is_dir', $elm$json$Json$Decode$bool), A2( $elm_community$json_extra$Json$Decode$Extra$andMap, - A2($elm$json$Json$Decode$field, 'file_path', $elm$json$Json$Decode$string), + $elm$json$Json$Decode$oneOf( + _List_fromArray( + [ + A2($elm$json$Json$Decode$field, 'file_content', $elm$json$Json$Decode$string), + $elm$json$Json$Decode$succeed('') + ])), A2( $elm_community$json_extra$Json$Decode$Extra$andMap, - A2( - $elm$json$Json$Decode$field, - 'files', - $elm$json$Json$Decode$list($author$project$App$decode_file_info)), + A2($elm$json$Json$Decode$field, 'file_path', $elm$json$Json$Decode$string), A2( $elm_community$json_extra$Json$Decode$Extra$andMap, - A2($elm$json$Json$Decode$field, 'slug', $elm$json$Json$Decode$string), + A2( + $elm$json$Json$Decode$field, + 'files', + $elm$json$Json$Decode$list($author$project$App$decode_file_info)), A2( $elm_community$json_extra$Json$Decode$Extra$andMap, - A2($elm$json$Json$Decode$field, 'preview_url', $elm$json$Json$Decode$string), + A2($elm$json$Json$Decode$field, 'slug', $elm$json$Json$Decode$string), A2( $elm_community$json_extra$Json$Decode$Extra$andMap, - A2($elm$json$Json$Decode$field, 'csrf_token', $elm$json$Json$Decode$string), - $elm$json$Json$Decode$succeed( - A9($author$project$App$Model, $author$project$App$init_model.show_preview, $author$project$App$init_model.show_log, $author$project$App$init_model.editor_size, $author$project$App$init_model.content_changed, $author$project$App$init_model.log, $author$project$App$init_model.command_to_run, $author$project$App$init_model.selected_package, $author$project$App$init_model.jj_status, $author$project$App$init_model.commit_message))))))))))), + A2($elm$json$Json$Decode$field, 'preview_url', $elm$json$Json$Decode$string), + A2( + $elm_community$json_extra$Json$Decode$Extra$andMap, + A2($elm$json$Json$Decode$field, 'csrf_token', $elm$json$Json$Decode$string), + $elm$json$Json$Decode$succeed( + A9($author$project$App$Model, $author$project$App$init_model.show_preview, $author$project$App$init_model.show_log, $author$project$App$init_model.editor_size, $author$project$App$init_model.content_changed, $author$project$App$init_model.log, $author$project$App$init_model.command_to_run, $author$project$App$init_model.selected_package, $author$project$App$init_model.jj_status, $author$project$App$init_model.commit_message)))))))))))), $elm$core$Result$withDefault($author$project$App$init_model)); var $author$project$App$SetLog = function (a) { return {$: 'SetLog', a: a}; @@ -6447,8 +6660,8 @@ var $author$project$App$FileSaved = F2( function (a, b) { return {$: 'FileSaved', a: a, b: b}; }); -var $author$project$App$FileUploaded = function (a) { - return {$: 'FileUploaded', a: a}; +var $author$project$App$FilesUploaded = function (a) { + return {$: 'FilesUploaded', a: a}; }; var $author$project$App$HttpError = function (a) { return {$: 'HttpError', a: a}; @@ -6480,10 +6693,6 @@ var $author$project$App$SaveContent = function (a) { var $author$project$App$SetFileContent = function (a) { return {$: 'SetFileContent', a: a}; }; -var $author$project$App$UploadFile = F2( - function (a, b) { - return {$: 'UploadFile', a: a, b: b}; - }); var $elm$core$Platform$Cmd$batch = _Platform_batch; var $author$project$App$decode_command_response = $elm$json$Json$Decode$oneOf( _List_fromArray( @@ -6563,6 +6772,7 @@ var $author$project$App$fetch_jj_status = $elm$http$Http$get( A2($elm$json$Json$Decode$field, 'status', $elm$json$Json$Decode$string)), url: 'jj/status' }); +var $elm$http$Http$filePart = _Http_pair; var $elm$url$Url$Builder$toQueryPair = function (_v0) { var key = _v0.a; var value = _v0.b; @@ -6764,10 +6974,10 @@ var $author$project$App$update = F2( case 'DropFiles': var action = msg.a; var files = msg.b; - var _v3 = $elm$core$List$head(files); - if (_v3.$ === 'Just') { - var file = _v3.a; - if (action.$ === 'Content') { + if (action.$ === 'Content') { + var _v4 = $elm$core$List$head(files); + if (_v4.$ === 'Just') { + var file = _v4.a; return _Utils_Tuple2( model, A2( @@ -6775,51 +6985,47 @@ var $author$project$App$update = F2( $author$project$App$SetFileContent, $elm$file$File$toString(file))); } else { - return _Utils_Tuple2( - model, - A2( - $elm$core$Task$perform, - $author$project$App$UploadFile(file), - $elm$file$File$toString(file))); + return _Utils_Tuple2(model, $elm$core$Platform$Cmd$none); } } else { - return _Utils_Tuple2(model, $elm$core$Platform$Cmd$none); - } - case 'UploadFile': - var file = msg.a; - var contents = msg.b; - return _Utils_Tuple2( - model, - $elm$http$Http$post( - { - body: $elm$http$Http$multipartBody( - _List_fromArray( - [ + return _Utils_Tuple2( + model, + $elm$http$Http$post( + { + body: $elm$http$Http$multipartBody( + _Utils_ap( + _List_fromArray( + [ + A2($elm$http$Http$stringPart, 'csrfmiddlewaretoken', model.csrf_token) + ]), A2( - $elm$http$Http$stringPart, - 'path', - $elm$file$File$name(file)), - A2($elm$http$Http$stringPart, 'content', contents), - A2($elm$http$Http$stringPart, 'csrfmiddlewaretoken', model.csrf_token) - ])), - expect: $elm$http$Http$expectWhatever( - function (r) { - if (r.$ === 'Ok') { - return $author$project$App$FileUploaded(file); - } else { - return $author$project$App$NoOp; - } - }), - url: 'save-file' - })); - case 'FileUploaded': - var file = msg.a; - var name = $elm$file$File$name(file); - var url = $author$project$App$file_edit_url( - {is_dir: false, name: name, path: name}); - return _Utils_Tuple2( - model, - $elm$browser$Browser$Navigation$load(url)); + $elm$core$List$map, + $elm$http$Http$filePart('files'), + files))), + expect: $elm$http$Http$expectWhatever( + function (r) { + if (r.$ === 'Ok') { + return $author$project$App$FilesUploaded(files); + } else { + return $author$project$App$NoOp; + } + }), + url: 'upload-files' + })); + } + case 'FilesUploaded': + var files = msg.a; + if (files.b) { + var file = files.a; + var name = $elm$file$File$name(file); + var url = $author$project$App$file_edit_url( + {is_dir: false, name: name, path: name}); + return _Utils_Tuple2( + model, + $elm$browser$Browser$Navigation$load(url)); + } else { + return $author$project$App$nocmd(model); + } case 'SetCommand': var cmd = msg.a; return $author$project$App$nocmd( @@ -7184,12 +7390,14 @@ var $author$project$App$DropFiles = F2( function (a, b) { return {$: 'DropFiles', a: a, b: b}; }); +var $elm$html$Html$a = _VirtualDom_node('a'); var $elm$html$Html$Attributes$action = function (uri) { return A2( $elm$html$Html$Attributes$stringProperty, 'action', _VirtualDom_noJavaScriptUri(uri)); }; +var $elm$html$Html$audio = _VirtualDom_node('audio'); var $elm$file$File$decoder = _File_decoder; var $elm$html$Html$details = _VirtualDom_node('details'); var $elm$core$List$drop = F2( @@ -7247,6 +7455,13 @@ var $elm$core$Dict$fromList = function (assocs) { $elm$core$Dict$empty, assocs); }; +var $elm$html$Html$Attributes$href = function (url) { + return A2( + $elm$html$Html$Attributes$stringProperty, + 'href', + _VirtualDom_noJavaScriptUri(url)); +}; +var $elm$html$Html$img = _VirtualDom_node('img'); var $elm$html$Html$nav = _VirtualDom_node('nav'); var $elm$virtual_dom$VirtualDom$MayPreventDefault = function (a) { return {$: 'MayPreventDefault', a: a}; @@ -7260,16 +7475,14 @@ var $elm$html$Html$Events$preventDefaultOn = F2( }); var $elm$html$Html$section = _VirtualDom_node('section'); var $elm$html$Html$span = _VirtualDom_node('span'); +var $elm$html$Html$Attributes$src = function (url) { + return A2( + $elm$html$Html$Attributes$stringProperty, + 'src', + _VirtualDom_noJavaScriptOrHtmlUri(url)); +}; var $elm$html$Html$summary = _VirtualDom_node('summary'); -var $elm$core$Maybe$withDefault = F2( - function (_default, maybe) { - if (maybe.$ === 'Just') { - var value = maybe.a; - return value; - } else { - return _default; - } - }); +var $elm$html$Html$video = _VirtualDom_node('video'); var $author$project$App$editor_pane = function (model) { var languages = $elm$core$Dict$fromList( _List_fromArray( @@ -7281,6 +7494,7 @@ var $author$project$App$editor_pane = function (model) { _Utils_Tuple2('R', 'r'), _Utils_Tuple2('elm', 'elm') ])); + var file_path = 'file/' + model.file_path; var extension = A2( $elm$core$Maybe$withDefault, '', @@ -7448,57 +7662,111 @@ var $author$project$App$editor_pane = function (model) { $elm$html$Html$Attributes$method('POST'), $elm$html$Html$Attributes$action('save-file') ]), - _List_fromArray( - [ - A3( - $elm$html$Html$node, - 'code-editor', - _List_fromArray( - [ - A2( - $elm$html$Html$Events$on, - 'change', - A2( - $elm$json$Json$Decode$map, - $author$project$App$SetFileContent, + function () { + var _v0 = model.mime_type; + switch (_v0.$) { + case 'Text': + return _List_fromArray( + [ + A3( + $elm$html$Html$node, + 'code-editor', + _List_fromArray( + [ + A2( + $elm$html$Html$Events$on, + 'change', + A2( + $elm$json$Json$Decode$map, + $author$project$App$SetFileContent, + A2( + $elm$json$Json$Decode$at, + _List_fromArray( + ['target', 'value']), + $elm$json$Json$Decode$string))), + A2($elm$html$Html$Attributes$attribute, 'content', model.file_content), + A2($elm$html$Html$Attributes$attribute, 'language', language) + ]), + _List_fromArray( + [ + $elm$html$Html$text(model.file_content) + ])), A2( - $elm$json$Json$Decode$at, - _List_fromArray( - ['target', 'value']), - $elm$json$Json$Decode$string))), - A2($elm$html$Html$Attributes$attribute, 'content', model.file_content), - A2($elm$html$Html$Attributes$attribute, 'language', language) - ]), - _List_fromArray( - [ - $elm$html$Html$text(model.file_content) - ])), - A2( - $elm$html$Html$input, - _List_fromArray( - [ - $elm$html$Html$Attributes$name('path'), - $elm$html$Html$Attributes$value(model.file_path), - $elm$html$Html$Attributes$type_('hidden') - ]), - _List_Nil), - A2( - $elm$html$Html$input, - _List_fromArray( - [ - $elm$html$Html$Attributes$name('content'), - $elm$html$Html$Attributes$value(model.file_content), - $elm$html$Html$Attributes$type_('hidden') - ]), - _List_Nil) - ])) + $elm$html$Html$input, + _List_fromArray( + [ + $elm$html$Html$Attributes$name('path'), + $elm$html$Html$Attributes$value(model.file_path), + $elm$html$Html$Attributes$type_('hidden') + ]), + _List_Nil), + A2( + $elm$html$Html$input, + _List_fromArray( + [ + $elm$html$Html$Attributes$name('content'), + $elm$html$Html$Attributes$value(model.file_content), + $elm$html$Html$Attributes$type_('hidden') + ]), + _List_Nil) + ]); + case 'Image': + return _List_fromArray( + [ + A2( + $elm$html$Html$img, + _List_fromArray( + [ + $elm$html$Html$Attributes$src(file_path) + ]), + _List_Nil) + ]); + case 'Audio': + return _List_fromArray( + [ + A2( + $elm$html$Html$audio, + _List_fromArray( + [ + $elm$html$Html$Attributes$src(file_path), + A2($elm$html$Html$Attributes$attribute, 'controls', '') + ]), + _List_Nil) + ]); + case 'Video': + return _List_fromArray( + [ + A2( + $elm$html$Html$video, + _List_fromArray( + [ + $elm$html$Html$Attributes$src(file_path), + A2($elm$html$Html$Attributes$attribute, 'controls', '') + ]), + _List_Nil) + ]); + default: + return _List_fromArray( + [ + A2( + $elm$html$Html$a, + _List_fromArray( + [ + $elm$html$Html$Attributes$href(file_path) + ]), + _List_fromArray( + [ + $elm$html$Html$text('Download') + ])) + ]); + } + }()) ])); }; var $elm$core$String$fromFloat = _String_fromNumber; var $author$project$App$SetEditorSize = function (a) { return {$: 'SetEditorSize', a: a}; }; -var $elm$html$Html$a = _VirtualDom_node('a'); var $elm$core$Basics$round = _Basics_round; var $author$project$App$as_percentage = function (amount) { return $elm$core$String$fromInt( @@ -7508,12 +7776,6 @@ var $elm$html$Html$datalist = _VirtualDom_node('datalist'); var $elm$json$Json$Decode$float = _Json_decodeFloat; var $elm$html$Html$h1 = _VirtualDom_node('h1'); var $elm$html$Html$header = _VirtualDom_node('header'); -var $elm$html$Html$Attributes$href = function (url) { - return A2( - $elm$html$Html$Attributes$stringProperty, - 'href', - _VirtualDom_noJavaScriptUri(url)); -}; var $author$project$App$link = F2( function (url, text) { return A2( @@ -7532,6 +7794,52 @@ var $elm$html$Html$Attributes$max = $elm$html$Html$Attributes$stringProperty('ma var $elm$html$Html$Attributes$min = $elm$html$Html$Attributes$stringProperty('min'); var $elm$html$Html$option = _VirtualDom_node('option'); var $elm$html$Html$output = _VirtualDom_node('output'); +var $elm$core$List$any = F2( + function (isOkay, list) { + any: + while (true) { + if (!list.b) { + return false; + } else { + var x = list.a; + var xs = list.b; + if (isOkay(x)) { + return true; + } else { + var $temp$isOkay = isOkay, + $temp$list = xs; + isOkay = $temp$isOkay; + list = $temp$list; + continue any; + } + } + } + }); +var $elm$core$List$member = F2( + function (x, xs) { + return A2( + $elm$core$List$any, + function (a) { + return _Utils_eq(a, x); + }, + xs); + }); +var $author$project$App$suffix = A2( + $elm$core$Basics$composeR, + $elm$core$String$split('.'), + A2( + $elm$core$Basics$composeR, + $elm$core$List$reverse, + A2( + $elm$core$Basics$composeR, + $elm$core$List$head, + $elm$core$Maybe$withDefault('')))); +var $author$project$App$preview_url = function (model) { + var page_suffixes = _List_fromArray( + ['html', 'php']); + var file_suffix = $author$project$App$suffix(model.file_path); + return A2($elm$core$List$member, file_suffix, page_suffixes) ? (model.preview_url + ('/' + model.file_path)) : model.preview_url; +}; var $elm$html$Html$Attributes$step = function (n) { return A2($elm$html$Html$Attributes$stringProperty, 'step', n); }; @@ -7563,7 +7871,8 @@ var $author$project$App$header = function (model) { _List_fromArray( [ $elm$html$Html$Attributes$target('preview'), - $elm$html$Html$Attributes$href(model.preview_url) + $elm$html$Html$Attributes$href( + $author$project$App$preview_url(model)) ]), _List_fromArray( [ @@ -7887,6 +8196,7 @@ var $author$project$App$SetCommand = function (a) { }; var $author$project$App$ShowCommitModal = {$: 'ShowCommitModal'}; var $author$project$App$Upload = {$: 'Upload'}; +var $elm$html$Html$Attributes$enctype = $elm$html$Html$Attributes$stringProperty('enctype'); var $elm$html$Html$li = _VirtualDom_node('li'); var $elm$core$Basics$not = _Basics_not; var $elm$html$Html$Events$onClick = function (msg) { @@ -8066,6 +8376,50 @@ var $author$project$App$main_nav = function (model) { $elm$html$Html$text('Run') ])) ])), + A3( + $author$project$App$form, + model, + _List_fromArray( + [ + $elm$html$Html$Attributes$method('POST'), + $elm$html$Html$Attributes$action('upload-files'), + $elm$html$Html$Attributes$enctype('multipart/form-data'), + A2( + $elm$html$Html$Events$stopPropagationOn, + 'drop', + $elm$json$Json$Decode$succeed( + _Utils_Tuple2($author$project$App$NoOp, true))), + A2( + $elm$html$Html$Events$stopPropagationOn, + 'dragover', + $elm$json$Json$Decode$succeed( + _Utils_Tuple2($author$project$App$NoOp, true))) + ]), + _List_fromArray( + [ + A2( + $elm$html$Html$input, + _List_fromArray( + [ + A2($elm$html$Html$Attributes$attribute, 'aria-labelledby', 'upload-files-button'), + $elm$html$Html$Attributes$type_('file'), + A2($elm$html$Html$Attributes$attribute, 'multiple', ''), + $elm$html$Html$Attributes$id('id_files'), + $elm$html$Html$Attributes$name('files') + ]), + _List_Nil), + A2( + $elm$html$Html$button, + _List_fromArray( + [ + $elm$html$Html$Attributes$id('upload-files-button'), + $elm$html$Html$Attributes$type_('submit') + ]), + _List_fromArray( + [ + $elm$html$Html$text('Upload') + ])) + ])), (A2($elm$core$String$right, 4, model.file_path) === '.elm') ? A3( $author$project$App$form, model, @@ -8167,12 +8521,6 @@ var $author$project$App$TogglePreview = function (a) { return {$: 'TogglePreview', a: a}; }; var $elm$html$Html$iframe = _VirtualDom_node('iframe'); -var $elm$html$Html$Attributes$src = function (url) { - return A2( - $elm$html$Html$Attributes$stringProperty, - 'src', - _VirtualDom_noJavaScriptOrHtmlUri(url)); -}; var $author$project$App$preview_pane = function (model) { return A2( $elm$html$Html$details, @@ -8210,7 +8558,8 @@ var $author$project$App$preview_pane = function (model) { _List_fromArray( [ $elm$html$Html$Attributes$id('preview-frame'), - $elm$html$Html$Attributes$src(model.preview_url) + $elm$html$Html$Attributes$src( + $author$project$App$preview_url(model)) ]), _List_Nil) ]) : _List_Nil)); diff --git a/thinks/static/thinks/thinks.css b/thinks/static/thinks/thinks.css index cc4a11f..4cda138 100644 --- a/thinks/static/thinks/thinks.css +++ b/thinks/static/thinks/thinks.css @@ -8,8 +8,12 @@ } @media (prefers-color-scheme: dark) { + body { --background: hsl(70,100%,8%); + --default-background: black; --color: white; + --button-bg: #333; + } } * { diff --git a/thinks/urls.py b/thinks/urls.py index 2426c6d..930fb4d 100644 --- a/thinks/urls.py +++ b/thinks/urls.py @@ -12,6 +12,7 @@ urlpatterns = [ path('think//rename-file', RenameFileView.as_view(), name='rename_file'), path('think//delete-file', DeleteFileView.as_view(), name='delete_file'), path('think//run-command', RunCommandView.as_view(), name='run_command'), + path('think//upload-files', UploadFileView.as_view(), name='upload_files'), path('think//log', LogView.as_view(), name='log'), path('think//jj/status', JJStatusView.as_view(), name='jj_status'), path('think//jj/commit', JJCommitView.as_view(), name='jj_commit'), diff --git a/thinks/views.py b/thinks/views.py index e0eb83d..bbb1dc1 100644 --- a/thinks/views.py +++ b/thinks/views.py @@ -6,6 +6,7 @@ from django.views import generic from django.urls import reverse from itertools import groupby import json +import mimetypes from pathlib import Path import shutil import shlex @@ -16,6 +17,7 @@ from .make import ThingMaker from .models import Think from .random_slug import random_slug + class LoginRequiredMixin(AccessMixin): def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated: @@ -25,10 +27,12 @@ class LoginRequiredMixin(AccessMixin): return super().dispatch(request, *args, **kwargs) + class ThinkMixin(LoginRequiredMixin): model = Think context_object_name = 'think' + class IndexView(ThinkMixin, generic.ListView): template_name = 'thinks/index.html' @@ -50,10 +54,12 @@ class IndexView(ThinkMixin, generic.ListView): return JsonResponse({'templates': [t.as_json() for t in context['templates']], 'thinks': [t.as_json() for t in context['thinks']]}) return super().get(request, *args, **kwargs) + class CreateThinkView(ThinkMixin, generic.CreateView): template_name = 'thinks/new.html' form_class = forms.CreateThinkForm + class RemixThinkView(ThinkMixin, generic.UpdateView): template_name = 'thinks/remix.html' form_class = forms.RemixThinkForm @@ -79,6 +85,7 @@ class RemixThinkView(ThinkMixin, generic.UpdateView): return redirect(think.get_absolute_url()) + class ThinkView(ThinkMixin, generic.DetailView): template_name = "thinks/think.html" @@ -124,28 +131,37 @@ class ThinkView(ThinkMixin, generic.DetailView): files = sorted(files, key=lambda x: x['name'].lower()) - if path is not None and path.is_file(): - with open(path) as f: - content = f.read() - else: - content = '' - data = context['think_editor_data'] = { 'preview_url': think.get_static_url(), 'slug': think.slug, 'files': files, 'file_path': str(relpath), - 'file_content': content, 'is_dir': path is None or path.is_dir(), 'no_preview': self.request.GET.get('no-preview') is not None, } + if path is not None and path.is_file(): + mime_types = { + '.elm': 'text/application+elm', + } + mime_type, encoding = mimetypes.guess_type(path) + if mime_type is None: + mime_type = mime_types.get(path.suffix, 'text/plain') + category, filetype = mime_type.split('/') if mime_type is not None else ('text', 'plain') + binary_categories = ['audio', 'video', 'image'] + is_binary = category in binary_categories and mime_type != 'image/svg+xml' + data['mime_type'] = mime_type + if not is_binary: + with open(path) as f: + data['file_content'] = f.read() + if path is not None and path.suffix == '.elm': with open('public/elm-packages.json') as f: data['elm_packages'] = json.load(f) return context + class RenameThinkView(ThinkMixin, generic.UpdateView): form_class = forms.RenameThinkForm template_name = 'thinks/rename.html' @@ -160,18 +176,22 @@ class RenameThinkView(ThinkMixin, generic.UpdateView): return context + class DeleteThinkView(ThinkMixin, generic.DeleteView): template_name = 'thinks/delete.html' def get_success_url(self): return reverse('index') + class ReadFileView(ThinkMixin, generic.DetailView): def get(self, request, *args, **kwargs): think = self.get_object() relpath = self.kwargs['path'] path = think.root / relpath - print(path) + if not path.is_relative_to(think.root): + raise Exception(f"Bad path: {relpath}") + return redirect(think.get_static_url() + '/' + relpath) class SaveFileView(ThinkMixin, generic.UpdateView): @@ -191,6 +211,7 @@ class SaveFileView(ThinkMixin, generic.UpdateView): def get_success_url(self): return self.object.get_absolute_url()+'?path='+str(self.path) + class RenameFileView(ThinkMixin, generic.UpdateView): form_class = forms.RenameFileForm template_name = 'thinks/rename_file.html' @@ -203,6 +224,7 @@ class RenameFileView(ThinkMixin, generic.UpdateView): def get_success_url(self): return self.object.get_absolute_url()+'?path='+str(self.path) + class DeleteFileView(ThinkMixin, generic.UpdateView): form_class = forms.DeleteFileForm template_name = 'thinks/delete_file.html' @@ -214,6 +236,7 @@ class DeleteFileView(ThinkMixin, generic.UpdateView): def get_success_url(self): return self.object.get_absolute_url()+'?path='+str(self.path.parent) + class RunCommandView(ThinkMixin, generic.UpdateView): form_class = forms.RunCommandForm @@ -228,6 +251,14 @@ class RunCommandView(ThinkMixin, generic.UpdateView): ) return JsonResponse({'stdout': res.stdout, 'stderr': res.stderr}) + +class UploadFileView(ThinkMixin, generic.UpdateView): + form_class = forms.UploadFileForm + + def get_success_url(self): + return self.object.get_absolute_url() + + class LogView(ThinkMixin, generic.DetailView): template_name = 'thinks/think.html' @@ -236,11 +267,13 @@ class LogView(ThinkMixin, generic.DetailView): return HttpResponse(think.get_log(), content_type='text/plain; charset=utf-8') + class JJStatusView(ThinkMixin, generic.detail.DetailView): def get(self, request, *args, **kwargs): status = self.get_object().jj_controller.status() return JsonResponse({'status': status}) + class JJCommitView(ThinkMixin, generic.UpdateView): form_class = forms.GitCommitForm