!7 // Note: It's ok again to use db_mainConcepts() !include early #1033884 // Compact Module Include Gazelle V [dev version] module GazelleScreenCam { !include early #1025212 // +Enabled without visualization int pixelRows = 128, colors = 8; S script = "64p 8c gradientImage"; S testScreenScript; S newScript; bool animate = true; bool horizontalLayout; // flat layout int fpsTarget = 20; WatchTarget watchTarget; transient NewScriptCompileResult newScriptCompileResult; transient ImageSurface isPosterized, isRegions, isTestScreen; transient new ScreenCamStream imageStream; transient new BWIntegralImageStream integralImages; transient new SourceTriggeredStream<BWImage> scaledAndPosterizedImages; transient new DoubleFPSCounter fpsCounter; transient int fps; //transient ScreenSelectorRadioButtons screenSelector; transient WatchTargetSelector watchTargetSelector; transient new RollingAverage remainingMSPerFrame; transient int remainingMS; transient new FunctionTimings<S> functionTimings; transient ReliableSingleThread rstRunScript = dm_rst(me(), r _runScript); transient JGazelleVScriptRunner scriptRunner; transient JGazelleVScriptRunner testScreenScriptRunner; transient Animation animation; transient BWImage_FastRegions mainScreenRegions; transient UIURLSystem uiURLs; // set by host transient Concepts concepts; S uiURL = "Main Tabs"; transient FileWatchService fileWatcher; transient SimpleCRUD_v2<Label> labelCRUD; transient SimpleCRUD_v2<GalleryImage> imageCRUD; transient JGallery gallery; transient JComponent galleryComponent; transient FlexibleRateTimer screenCamTimer; transient SingleComponentPanel scpMain; transient JTabbedPane tabs; transient bool showRegionsAsOutline = true; transient JComponent watchScreenPane; transient S screenCamRecognitionOutput; start { dm_onFieldChange horizontalLayout( //r dm_revisualize // deh buggy r dm_reload ); // non-standalone mode (doesn't work yet) if (concepts == null) { printWithMS("Starting DB"); db(); concepts = db_mainConcepts(); } else assertSame(concepts, db_mainConcepts()); // make indexes indexConceptField(concepts, GalleryImage, "path"); indexConceptField(concepts, Example, "item"); // update count in tab when tab isn't selected onConceptChanges(concepts, -> { labelCRUD?.updateEnclosingTabTitle(); //imageCRUD?.updateEnclosingTabTitle(); updateEnclosingTabTitleWithCount(galleryComponent, countConcepts(GalleryImage)); }); uiURLs = new UIURLSystem(me(), dm_fieldLiveValue uiURL()); scriptRunner = new JGazelleVScriptRunner(dm_fieldLiveValue script(me())); testScreenScriptRunner = new JGazelleVScriptRunner(dm_fieldLiveValue testScreenScript(me())); printWithMS("Making image streams"); imageStream.directlyFeedInto(integralImages); integralImages.onNewElement(ii -> scaledAndPosterizedImages.newElement( scaleAndPosterize(ii, new SnPSettings(pixelRows, colors)))); integralImages.onNewElement(r { if (shouldRunScript()) rstRunScript.go(); }); printWithMS("Almost done"); scaledAndPosterizedImages.onNewElement(img -> { fpsCounter.inc(); setField(fps := iround(fpsCounter!)); // display before analysis (old) // isPosterized?.setImage_thisThread(img); // find regions floodFill(img); // display after analysis so we can highlight a region if (isPosterized != null) { var img2 = highlightRegion(img, isPosterized, mainScreenRegions); isPosterized.setImage_thisThread(img2); } }); printWithMS("Starting screen cam"); ownResource(screenCamTimer = new FlexibleRateTimer(fpsTarget, rEnter { if (!enabled) ret; watchTargetSelector?.updateScreenCount(); Timestamp deadline = tsNowPlusMS(1000/fpsTarget); if (watchTarget cast WatchScreen) imageStream.useScreen(watchTarget.screenNr-1); else if (watchTarget cast WatchMouse) imageStream.area(mouseArea(watchTarget.width, watchTarget.height)); imageStream.step(); long remaining = deadline.minus(tsNow()); remainingMSPerFrame.add(remaining); setField(remainingMS := iround(remainingMSPerFrame!)); })); screenCamTimer.start(); dm_onFieldChange fpsTarget(-> screenCamTimer.setFrequencyImmediately(fpsTarget)); printWithMS("Starting dir watcher"); startDirWatcher(); printWithMS("Gathering images from disk"); for (f : allImageFiles(galleryDir())) addToGallery(f); printWithMS("Got dem images"); } // convert to color & highlight region BufferedImage highlightRegion(BWImage image, ImageSurface is, BWImage_FastRegions regions) { var pixels = image.getRGBPixels(); highlightRegion(pixels, is, regions); ret bufferedImage(pixels, image.getWidth(), image.getHeight()); } // convert to color & highlight region void highlightRegion(int[] pixels, ImageSurface is, BWImage_FastRegions regions) { var mouse = is.mousePosition; int color = 0xFF008000, holeColor = 0xFFFFB266; if (mouse != null && regions != null) { int iHighlightedRegion = regions.regionAt(mouse); if (showRegionsAsOutline) { RegionBorder_innerPoints x = new(regions, iHighlightedRegion); new BoolVar hole; x.onNewTrace(isHole -> hole.set(isHole)); x.onFoundPoint(p -> pixels[p.y*regions.w+p.x] = hole! ? holeColor : color); x.run(); } else regions.markRegionInPixelArray(pixels, iHighlightedRegion, color); } } void startDirWatcher { fileWatcher = new FileWatchService; fileWatcher.addRecursiveListener(picturesDir(), file -> { if (!isImageFile(file)) ret; addToGallery(file); }); } ImageSurface stdImageSurface() { ret pixelatedImageSurface().setAutoZoomToDisplay(true).repaintInThread(false); } visualize { gallery = new JGallery; galleryComponent = gallery.visualize(); new AWTOnConceptChanges(concepts, galleryComponent, -> { gallery.setImageFiles(map(list(GalleryImage), i -> i.path)); }).install(); /*screenSelector = new ScreenSelectorRadioButtons(dm_fieldLiveValue screenNr()); screenSelector.compactLayout(true); screenSelector.hideIfOnlyOne(true); screenSelector.screenLabel("");*/ isPosterized = stdImageSurface(); //isRegions = stdImageSurface(); isTestScreen = stdImageSurface(); // when test screen is visible, do the animation awtEvery(isTestScreen, 1000/20, r stepAnimation); var jSpeedInfo = dm_transientCalculatedToolTip speedInfo_long(rightAlignLabel(dm_transientCalculatedLabel speedInfo())); //print("Labels: " + list(concepts, Label)); labelCRUD = new SimpleCRUD_v2(concepts, Label); labelCRUD.hideFields("globalID"); labelCRUD.addCountToEnclosingTab(true); labelCRUD.itemToMap_inner2 = l -> litorderedmap( "Name" := l.name, "# Examples" := n2(countConcepts(concepts, Example, label := l))); imageCRUD = new SimpleCRUD_v2(concepts, GalleryImage); imageCRUD.addCountToEnclosingTab(true); imageCRUD.itemToMap_inner2 = img -> litorderedmap( "File" := fileName(img.path), "Folder" := dirPath(img.path)); // main visual var studyPanel = new StudyPanel; watchTargetSelector = new WatchTargetSelector; watchScreenPane = borderlessScrollPane(jHigherScrollPane( jfullcenter(vstack( withLeftAndRightMargin(hstack( dm_rcheckBox enabled("Watch"), watchTargetSelector.visualize(), jlabel(" in "), withLabelToTheRight("colors @ ", dm_spinner colors(2, 256)), withLabelToTheRight("p", dm_powersOfTwoSpinner pixelRows(512)), , /* speed info can also go here */ )), verticalStrut(2), withSideMargins(centerAndEastWithMargin( dm_fieldLabel screenCamRecognitionOutput(), jSpeedInfo)), )))); tabs = scrollingTabs(jTopOrLeftTabs(horizontalLayout, "Screen Cam" := screenCamPanel(), /*WithToolTip("Regions", "Visualize regions detected on screen") := jscroll_centered_borderless(isRegions),*/ WithToolTip("Study", "Here you can analyze gallery images") := withTopAndBottomMargin(studyPanel.visualize()), WithToolTip("Simple Script", "Run a simple (linear) Gazelle V script") := withBottomMargin(scriptRunner.scriptAndResultPanel()), WithToolTip("Full Script", "Run a \"left-arrow script\"") := withBottomMargin(newScriptPanel()), WithToolTip("Gallery", "Gallery view with preview images") := withBottomMargin(galleryComponent), WithToolTip("Gallery 2", "Gallery view with more functions (delete etc)") := wrapCRUD(imageCRUD), WithToolTip("Labels", "Manage labels (image markers)") := wrapCRUD(labelCRUD), )); var cbEnabled = toolTip("Switch screen cam on or off", dm_checkBox enabled("")); var lblScreenCam = setToolTip("Show scaled down and color-reduced screen image", jlabel("Screen Cam")); tabComponentClickFixer(lblScreenCam); var screenCamTab = hstackWithSpacing(cbEnabled, lblScreenCam); replaceTabTitleComponent(tabs, "Screen Cam", screenCamTab); // for tab titles labelCRUD.update(); imageCRUD.update(); initUIURLs(); var urlBar = uiURLs.urlBar(); setToolTip(uiURLs.comboBox, "UI navigation system (partially populated)"); scpMain = singleComponentPanel(); uiURLs.scp(scpMain); uiURLs.showUIURL(uiURL); var vis = northAndCenter( withSideAndTopMargin(urlBar), //centerAndSouthOrEast(horizontalLayout, /*withBottomMargin*/(scpMain), /* watchScreenPane )*/ ); setHorizontalMarginForAllButtons(vis, 4); ret vis; } JComponent screenCamPanel() { ret centerAndSouthOrEast(horizontalLayout, withTools(isPosterized), watchScreenPane); } S speedInfo() { ret "FPS " + fps + " idle " + remainingMS + " ms"; } S speedInfo_long() { ret "Screen cam running at " + nFrames(fps) + "/second. " + n2(remainingMS) + " ms remaining per frame in first core" + " (of targeted " + fpsTarget + " FPS)"; } void floodFill(BWImage img) { BWImage_FastRegions ff = new(img); //ff.tolerance(0); ff.collectBounds(); functionTimings.time("Regions", ff); mainScreenRegions = ff; if (isRegions != null && isRegions.isShowing_quick()) { //print("Showing regions image"); isRegions.setImage_thisThread(ff.regionsImage()); } S text = nRegions(ff.regionCount()); setField(screenCamRecognitionOutput := text); setEnclosingTabTitle(isRegions, text); } bool useErrorHandling() { false; } S renderFunctionTimings() { ret lines(ciSorted(map(functionTimings!, (f, avg) -> firstToUpper(f) + ": " + n2(iround(nsToMicroseconds(avg!))) + " " + microSymbol() + "s (" + n2(iround(avg.n())) + ")"))); } transient long _runScript_idx; void _runScript() { long n = integralImages.elementCount(); if (n > _runScript_idx) { _runScript_idx = n; scriptRunner.parseAndRunOn(integralImages!); } } bool shouldRunScript() { ret isShowing(scriptRunner.scpScriptResult); } void stepAnimation { if (!animate) ret; if (animation == null) { animation = new AnimatedLine; animation.start(); } animation.nextFrame(); var img = whiteImage(animation.w, animation.h); animation.setGraphics(createGraphics(img)); animation.paint(); isTestScreen?.setImage(img); var ii = bwIntegralImage_withMeta(img); testScreenScriptRunner.parseAndRunOn(ii); } JComponent testScreenPanel() { ret centerAndSouthWithMargin( hsplit( northAndCenterWithMargin(centerAndEastWithMargin( jlabel("Input"), dm_fieldCheckBox animate()), jscroll_centered_borderless(isTestScreen)), northAndCenterWithMargin(centerAndEastWithMargin( jlabel("Output"), testScreenScriptRunner.lblScore), testScreenScriptRunner.scpScriptResult) ), testScreenScriptRunner.scriptInputField() ); } L popDownItems() { ret ll(jCheckBoxMenuItem_dyn("Horizontal Layout", -> horizontalLayout, b -> setField(horizontalLayout := b))); } void unvisualize {} // don't zero transient component fields void resetTimings { functionTimings.reset(); } // add tool side bar to image surface JComponent withTools(ImageSurface is) { new ImageSurface_PositionToolTip(is); ret centerAndEastWithMargin(jscroll_centered_borderless(is), vstackWithSpacing(3, jimageButtonScaledToWidth(16, #1103054, "Save screenshot in gallery", rThread saveScreenshotToGallery), )); } class WatchTargetSelector { JComboBox<WatchTarget> cb = jComboBox(); int screenCount; visualize { updateList(); print("Selecting watchTarget: " + watchTarget); selectItem(cb, watchTarget); main onChange(cb, watchTarget -> { setField(+watchTarget); print("Chose watchTarget: " + GazelleScreenCam.this.watchTarget); }); ret cb; } void updateScreenCount() { if (screenCount != screenCount()) updateList(); } void updateList() swing { setComboBoxItems(cb, makeWatchTargets()); } L<WatchTarget> makeWatchTargets() { ret flattenToList( countIteratorAsList_incl(1, screenCount = screenCount(), i -> WatchScreen(i)), new WatchMouse ); } } class SnPSelector { settable new SnPSettings settings; event change; visualize { var colors = jspinner(settings.colors, 2, 256); main onChange(colors, -> { settings.colors = intFromSpinner(colors); change(); }); var pixelRows = jPowersOfTwoSpinner(512, settings.pixelRows); main onChange(pixelRows, -> { settings.pixelRows = intFromSpinner(pixelRows); change(); }); ret hstack( colors, jlabel(" colors @ "), pixelRows, jlabel(" p")); } } JComponent wrapCRUD(SimpleCRUD_v2 crud) { ret crud == null ?: withTopAndBottomMargin(jRaisedSection(withMargin(crud.make_dontStartBots()))); } File galleryDir() { ret picturesDir(gazelle22_imagesSubDirName()); } void saveScreenshotToGallery enter { var img = imageStream!; saveImageWithCounter(galleryDir(), "Screenshot", img); } GalleryImage addToGallery(File imgFile) { if (!isImageFile(imgFile)) null; var img = uniq(concepts, GalleryImage, path := imgFile); //printVars("addToGallery", +imgFile, +img); ret img; } BWImage scaleAndPosterize(IBWIntegralImage ii, SnPSettings settings) { ret posterizeBWImage_withMeta(settings.colors, scaledBWImageFromBWIntegralImage_withMeta_height(settings.pixelRows, ii)); } class StudyPanel { new SnPSelector snpSelector; GalleryImage image; BufferedImage originalImage; ImageSurface isOriginal = stdImageSurface(); ImageSurface isOriginalWithRegions = stdImageSurface(); ImageSurface isPosterized = stdImageSurface(); SingleComponentPanel scp = singleComponentPanel(); SingleComponentPanel analysisPanel = singleComponentPanel(); SingleComponentPanel scpExampleCRUD = singleComponentPanel(); ConceptsComboBox<GalleryImage> cbImage = new(concepts, GalleryImage); ImageToRegions itr; int iSelectedRegion; SimpleCRUD_v2<SavedRegion> regionCRUD; SimpleCRUD_v2<Example> exampleCRUD; *() { //cbImage.sortTheList = l -> sortConceptsByIDDesc(l); cbImage.sortTheList = l -> sortedByComparator(l, (a, b) -> cmpAlphanumIC(fileName(a.path), fileName(b.path))); main onChangeAndNow(cbImage, img -> dm_q(me(), r { image = img; scp.setComponent(studyImagePanel()); })); isPosterized.removeAllTools(); isPosterized.onMousePositionChanged(r_dm_q(me(), l0 regionUpdate)); imageSurfaceOnLeftMouseDown(isPosterized, pt -> dm_q(me(), r { chooseRegionAt(pt) })); snpSelector.onChange(r_dm_q(me(), l0 runSnP)); } void chooseRegionAt(Pt p) { if (itr == null) ret; iSelectedRegion = itr.regions.regionAt(p); if (iSelectedRegion > 0) { var savedRegion = uniq_returnIfNew(concepts, SavedRegion, +image, snpSettings := snpSelector.settings.cloneMe(), regionIndex := iSelectedRegion); // it's new, add values if (savedRegion != null) { print("Saved new region!"); var bitMatrix = toScanlineBitMatrix(itr.regions.regionBitMatrix(iSelectedRegion)); cset(savedRegion, +bitMatrix); } } regionUpdate(); } // load new image JComponent studyImagePanel() { if (image == null) null; originalImage = loadImage2(image.path); if (originalImage == null) ret jcenteredlabel("Image not found"); isOriginal.setImage(originalImage); isOriginalWithRegions.setImage(originalImage); iSelectedRegion = 0; itr = new ImageToRegions(originalImage, snpSelector.settings); runSnP(); regionCRUD = new SimpleCRUD_v2<SavedRegion>(concepts, SavedRegion); regionCRUD.addFilter(+image); regionCRUD .showSearchBar(false) .showAddButton(false) .showEditButton(false); regionCRUD.itemToMap_inner2 = region -> { var examples = conceptsWhere(concepts, Example, item := region); ret litorderedmap( "Pixels" := region.bitMatrix == null ? "-" : n2(region.bitMatrix.pixelCount()), //"Shape" := "TODO", "Labels" := joinWithComma(map(examples, e -> e.label)), // TODO: scale using snpsettings "Position" := region.bitMatrix == null ? "-" : region.bitMatrix.boundingBoxOfTrueBits()); }; var regionCRUDComponent = regionCRUD.make_dontStartBots(); regionCRUD.onSelectionChanged(l0 updateExampleCRUD); ret hsplit( jtabs( "Image" := jscroll_centered_borderless(isOriginal), "Image + regions" := jscroll_centered_borderless(isOriginalWithRegions), "Posterized" := jscroll_centered_borderless(isPosterized), ), vsplit( analysisPanel, hsplit( jCenteredSection("Saved regions", regionCRUDComponent), scpExampleCRUD) )); } void runSnP { if (itr == null) ret; itr.run(); regionUpdate(); } void regionUpdate { if (itr == null) ret; var pixels = itr.posterized.getRGBPixels(); // hovering region marked green highlightRegion(pixels, isPosterized, itr.regions); // selected region marked blue itr.regions.markRegionInPixelArray(pixels, iSelectedRegion, 0xFFADD8E6); var highlighted = bufferedImage(pixels, itr.posterized.getWidth(), itr.posterized.getHeight()); isPosterized.setImage(highlighted); isPosterized.performAutoZoom(); // seems to be necessary for some reason updateAnalysis(); } void updateAnalysis { new LS lines; lines.add(nRegions(itr.regions.regionCount())); lines.add("Selected region: " + iSelectedRegion); S text = lines_rtrim(lines); analysisPanel.setComponent(jscroll(jMultiLineLabel(text))); } void updateExampleCRUD { var region = regionCRUD.selected(); if (region == null) ret with scpExampleCRUD.set(null); exampleCRUD = new SimpleCRUD_v2<Example>(concepts, Example); exampleCRUD.entityName = -> "label for region"; exampleCRUD.addFilter(item := region); exampleCRUD.showSearchBar(false); scpExampleCRUD.set(jCenteredSection("Labels for region", exampleCRUD.make_dontStartBots())); } void importImage { new JFileChooser fc; if (fc.showOpenDialog(cbImage) == JFileChooser.APPROVE_OPTION) { File file = fc.getSelectedFile().getAbsoluteFile(); if (!isImageFile(file)) ret with infoMessage("Not an image file"); var img = addToGallery(file); waitUntil(250, 5.0, -> comboBoxContainsItem(cbImage, img)); setSelectedItem(cbImage, img); } } visual jRaisedSection(northAndCenterWithMargins( centerAndEast(withLabel("Study", cbImage), hstack( jlabel(" in "), snpSelector.visualize(), horizontalStrut(10), jPopDownButton_noText( "Import image...", rThreadEnter importImage, ), )), scp)); } // end of StudyPanel // S&P, then regions class ImageToRegions { BufferedImage inputImage; BWIntegralImage ii; SnPSettings snpSettings; BWImage posterized; BWImage_FastRegions regions; *(BufferedImage *inputImage, SnPSettings *snpSettings) {} run { ii = bwIntegralImage_withMeta(inputImage); posterized = scaleAndPosterize(ii, snpSettings); regions = new BWImage_FastRegions(posterized); regions.collectBounds(); functionTimings.time("Regions", regions); } } void initUIURLs { uiURLs.put("Main Tabs", -> horizontalLayout ? withMargin(tabs) : withSideMargin(tabs)); uiURLs.put("Screen Cam FPS", -> jFullCenter( vstackWithSpacing( jCenteredLabel("FPS target (frames per second) for screen cam:"), jFullCenter(dm_spinner fpsTarget(1, 60)), jCenteredLabel("(You can lower this value if you have a slower computer)")))); uiURLs.put("Timings", -> { JTextArea taTimings = jTextArea_noUndo(); awtEveryAndNow(taTimings, .5, r { setText(taTimings, renderFunctionTimings()) }); ret withRightAlignedButtons(taTimings, Reset := r resetTimings); }); uiURLs.put("Test Screen" := -> testScreenPanel()); //uiURLs.put("New Script" := -> newScriptPanel()); // add more ui urls here } JComponent newScriptPanel() { //var ta = dm_textArea newScript(); var ta = dm_syntaxTextArea newScript(); awtCalcEvery(ta.textArea(), 1.0, r_dm_q(r compileNewScript)); ret jCenteredSection("Left-arrow style script", centerAndSouthWithMargin( ta.visualize(), centerAndEastWithMargin( dm_label newScriptCompileResult(), jbutton("Run" := rThreadEnter runNewScript)))); } void compileNewScript { var newScript = this.newScript; var result = newScriptCompileResult; if (result == null || !eq(result.script, newScript)) { try { result = new NewScriptCompileResult; result.script = newScript; result.parser = new GazelleV_LeftArrowScriptParser; result.parser.allowTheWorld(mc(), utils.class); result.parsedScript = result.parser.parse(result.script); print(result.parsedScript); } catch print e { result.compileError = e; } setField(newScriptCompileResult := result); } } void runNewScript { //showText_fast_noWrap("Script Error", renderStackTrace(e)); dm_runInQAndWait(r compileNewScript); var result = newScriptCompileResult; if (result.parsedScript != null) result.result = okOrError(-> result.parsedScript!); } class NewScriptCompileResult { S script; GazelleV_LeftArrowScriptParser parser; Throwable compileError; GazelleV_LeftArrowScript.Script parsedScript; OKOrError result; toString { ret compileError != null ? exceptionToStringShorter(compileError) : "Compiled OK"; } } } // end of module concept Label > ConceptWithGlobalID { S name; //new RefL examples; toString { ret /*"Label " +*/ name; } } concept GalleryImage { File path; toString { ret /*"[" + id + "] " +*/ fileName(path); } } concept SavedRegion { new Ref image; // e.g. a GalleryImage SnPSettings snpSettings; int regionIndex; // region number relative to snpSettings //Rect bounds; // get it from bitMatrix instead ScanlineBitMatrix bitMatrix; //new RefL<Label> labels; } concept Example { new Ref<Label> label; new Ref item; // e.g. a SavedRegion double confidence = 1; } concept IfThenTheory { new Ref if_; new Ref then; } sclass WatchTarget {} // screenNr: 1 = screen 1 etc srecord WatchScreen(int screenNr) > WatchTarget { toString { ret "Screen " + screenNr; } } srecord WatchMouse(int width, int height) > WatchTarget { WatchMouse() { height = 256; width = iround(height*16.0/9); } toString { ret "Mouse"; } } // SnP = Scale and Posterize srecord SnPSettings(int pixelRows, int colors) { SnPSettings() { pixelRows = 128; colors = 8; } SnPSettings cloneMe() { ret shallowClone(this); } } /*asclass RegionPaintMode { } sclass RegionPaintMode_Normal > RegionPaintMode { } sclass RegionPaintMode_Outline > RegionPaintMode { }*/ // include functions that scripts may want to use please include function leftScreenBounds. please include function rightScreenBounds. please include class ScreenOverlay. please include function mergeRects. please include class TranslucentWindowTest.
Began life as a copy of #1033862
download show line numbers debug dex old transpilations
Travelled to 2 computer(s): bhatertpkbcr, mqqgnosmbjvj
No comments. add comment
Snippet ID: | #1034012 |
Snippet name: | Gazelle Screen Cam / Gazelle 22 Module [backup] |
Eternal ID of this version: | #1034012/1 |
Text MD5: | b8a0771ca5647560ebb13c07c14b303f |
Author: | stefan |
Category: | javax / gazelle v |
Type: | JavaX source code (Dynamic Module) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2022-01-17 00:16:47 |
Source code size: | 26413 bytes / 830 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 137 / 149 |
Referenced in: | -