Compare commits

...

4 commits

7 changed files with 600 additions and 149 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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;

View file

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

View file

@ -8,8 +8,12 @@
}
@media (prefers-color-scheme: dark) {
body {
--background: hsl(70,100%,8%);
--default-background: black;
--color: white;
--button-bg: #333;
}
}
* {

View file

@ -12,6 +12,7 @@ urlpatterns = [
path('think/<slug:slug>/rename-file', RenameFileView.as_view(), name='rename_file'),
path('think/<slug:slug>/delete-file', DeleteFileView.as_view(), name='delete_file'),
path('think/<slug:slug>/run-command', RunCommandView.as_view(), name='run_command'),
path('think/<slug:slug>/upload-files', UploadFileView.as_view(), name='upload_files'),
path('think/<slug:slug>/log', LogView.as_view(), name='log'),
path('think/<slug:slug>/jj/status', JJStatusView.as_view(), name='jj_status'),
path('think/<slug:slug>/jj/commit', JJCommitView.as_view(), name='jj_commit'),

View file

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