This commit is contained in:
death-claw
2023-12-08 14:33:13 +01:00
parent e798218a21
commit c77e3fd5c7
30 changed files with 4983 additions and 4321 deletions
@@ -3,7 +3,7 @@ package me.vripper.entities
import me.vripper.entities.domain.Status
data class Image(
var id: Long = -1,
val id: Long = -1,
val postId: Long,
val url: String,
val thumbUrl: String,
@@ -5,7 +5,7 @@ import me.vripper.entities.domain.Status
import java.time.LocalDateTime
data class Post(
var id: Long = -1,
val id: Long = -1,
val postTitle: String,
val threadTitle: String,
val forum: String,
@@ -9,12 +9,9 @@ import me.vripper.model.ErrorCount
import me.vripper.model.QueueState
import me.vripper.model.Settings
data class PostEvent(
val add: List<Post> = emptyList(),
val update: List<Post> = emptyList(),
val delete: List<Long> = emptyList()
)
data class PostCreateEvent(val posts: List<Post>)
data class PostUpdateEvent(val posts: List<Post>)
data class PostDeleteEvent(val postIds: List<Long>)
data class ImageEvent(val images: List<Image>)
data class ThreadCreateEvent(val thread: Thread)
data class ThreadDeleteEvent(val threadId: Long)
@@ -8,10 +8,10 @@ import java.util.concurrent.Executors
object EventBus {
private val Scheduler: Scheduler = Schedulers.fromExecutor(Executors.newSingleThreadExecutor())
private val scheduler: Scheduler = Schedulers.fromExecutor(Executors.newSingleThreadExecutor())
private val _events = Sinks.many().multicast().onBackpressureBuffer<Any>()
val events: Flux<Any> = _events.asFlux().publishOn(Scheduler)
val events: Flux<Any> = _events.asFlux().publishOn(scheduler)
fun publishEvent(event: Any) {
_events.tryEmitNext(event)
_events.emitNext(event) { _, _ -> true }
}
}
@@ -27,12 +27,12 @@ class DataTransaction(
fun updatePosts(posts: List<Post>) {
transaction { postDownloadStateRepository.update(posts) }
eventBus.publishEvent(PostEvent(update = posts))
eventBus.publishEvent(PostUpdateEvent(posts))
}
fun updatePost(posts: Post) {
transaction { postDownloadStateRepository.update(posts) }
eventBus.publishEvent(PostEvent(update = listOf(posts)))
fun updatePost(post: Post) {
transaction { postDownloadStateRepository.update(post) }
eventBus.publishEvent(PostUpdateEvent(listOf(post)))
}
fun save(thread: Thread) {
@@ -102,7 +102,7 @@ class DataTransaction(
}
savedPosts
}
eventBus.publishEvent(PostEvent(add = savedPosts))
eventBus.publishEvent(PostCreateEvent(savedPosts))
return savedPosts
}
@@ -150,7 +150,7 @@ class DataTransaction(
}
eventBus.publishEvent(PostEvent(delete = postIds))
eventBus.publishEvent(PostDeleteEvent(postIds = postIds))
eventBus.publishEvent(ErrorCountEvent(ErrorCount(countImagesInError())))
}
@@ -51,7 +51,7 @@ class ImagesTableView : Fragment("Photos") {
it.copy(images = it.images.filter { image: Image -> image.postId == postId })
}.filter {
it.images.isNotEmpty()
}.buffer(Duration.ofMillis(125))
}.buffer(Duration.ofMillis(175))
.subscribe { imageEvent ->
runLater {
for (image in imageEvent.map { it.images }.flatten().reversed().distinct()) {
@@ -14,13 +14,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import me.vripper.event.EventBus
import me.vripper.event.PostEvent
import me.vripper.event.PostCreateEvent
import me.vripper.event.PostDeleteEvent
import me.vripper.event.PostUpdateEvent
import me.vripper.gui.clipboard.ClipboardService
import me.vripper.gui.controller.PostController
import me.vripper.gui.model.PostModel
import me.vripper.gui.view.*
import tornadofx.*
import java.time.Duration
class PostsTableView : View() {
@@ -54,15 +55,17 @@ class PostsTableView : View() {
clipboardService.init()
}
}
eventBus.events.ofType(PostEvent::class.java).buffer(Duration.ofMillis(125)).subscribe { postEvents ->
runLater {
postEvents.map { it.delete }.flatten().forEach {
items.removeIf { p -> p.postId == it }
}
val add = postEvents.map { it.add }.flatten().map { postController.mapper(it) }
tableView.items.addAll(add)
for (post in postEvents.map { it.update }.flatten().reversed().distinct()) {
eventBus.events.ofType(PostCreateEvent::class.java).subscribe { postEvents ->
val add = postEvents.posts.map { postController.mapper(it) }
runLater {
tableView.items.addAll(add)
}
}
eventBus.events.ofType(PostUpdateEvent::class.java).subscribe { postEvents ->
runLater {
for (post in postEvents.posts) {
val postModel = items.find { it.postId == post.postId } ?: continue
postModel.status = post.status.name
@@ -77,6 +80,14 @@ class PostsTableView : View() {
}
}
}
eventBus.events.ofType(PostDeleteEvent::class.java).subscribe { postEvents ->
runLater {
postEvents.postIds.forEach {
items.removeIf { p -> p.postId == it }
}
}
}
}
override val root = vbox {
+3 -3
View File
@@ -61,10 +61,10 @@
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "vripper-web-ui:build:production"
"buildTarget": "vripper-web-ui:build:production"
},
"development": {
"browserTarget": "vripper-web-ui:build:development"
"buildTarget": "vripper-web-ui:build:development"
}
},
"options": {
@@ -75,7 +75,7 @@
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "vripper-web-ui:build"
"buildTarget": "vripper-web-ui:build"
}
},
"test": {
+4620 -4115
View File
File diff suppressed because it is too large Load Diff
+23 -23
View File
@@ -11,37 +11,37 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^16.0.0",
"@angular/cdk": "^16.0.0",
"@angular/common": "^16.0.0",
"@angular/compiler": "^16.0.0",
"@angular/core": "^16.0.0",
"@angular/forms": "^16.0.0",
"@angular/material": "^16.0.0",
"@angular/platform-browser": "^16.0.0",
"@angular/platform-browser-dynamic": "^16.0.0",
"@angular/router": "^16.0.0",
"@angular/animations": "^17.0.5",
"@angular/cdk": "^17.0.1",
"@angular/common": "^17.0.5",
"@angular/compiler": "^17.0.5",
"@angular/core": "^17.0.5",
"@angular/forms": "^17.0.5",
"@angular/material": "^17.0.1",
"@angular/platform-browser": "^17.0.5",
"@angular/platform-browser-dynamic": "^17.0.5",
"@angular/router": "^17.0.5",
"@stomp/rx-stomp": "^2.0.0",
"@stomp/stompjs": "^7.0.0",
"ag-grid-angular": "^29.3.4",
"ag-grid-community": "^29.3.4",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.13.0"
"zone.js": "~0.14.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "^16.0.0",
"@angular-eslint/builder": "16.0.1",
"@angular-eslint/eslint-plugin": "16.0.1",
"@angular-eslint/eslint-plugin-template": "16.0.1",
"@angular-eslint/schematics": "16.0.1",
"@angular-eslint/template-parser": "16.0.1",
"@angular/cli": "~16.0.0",
"@angular/compiler-cli": "^16.0.0",
"@angular-devkit/build-angular": "^17.0.5",
"@angular-eslint/builder": "17.1.1",
"@angular-eslint/eslint-plugin": "17.1.1",
"@angular-eslint/eslint-plugin-template": "17.1.1",
"@angular-eslint/schematics": "17.1.1",
"@angular-eslint/template-parser": "17.1.1",
"@angular/cli": "~17.0.5",
"@angular/compiler-cli": "^17.0.5",
"@types/jasmine": "~4.3.0",
"@typescript-eslint/eslint-plugin": "5.59.2",
"@typescript-eslint/parser": "5.59.2",
"eslint": "^8.39.0",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"eslint": "^8.53.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"jasmine-core": "~4.6.0",
@@ -52,6 +52,6 @@
"karma-jasmine-html-reporter": "~2.0.0",
"prettier": "^2.8.8",
"prettier-eslint": "^15.0.1",
"typescript": "~5.0.2"
"typescript": "~5.2.2"
}
}
+8 -1
View File
@@ -16,7 +16,10 @@ import {
} from '@angular/material/dialog';
import { SettingsComponent } from './settings/settings.component';
import { Settings } from './domain/settings.model';
import { ConfirmComponent, ConfirmDialogData } from './confirm/confirm.component';
import {
ConfirmComponent,
ConfirmDialogData,
} from './confirm/confirm.component';
import { Subject } from 'rxjs';
import { RxStompState } from '@stomp/rx-stomp';
import { StatusBarComponent } from './status-bar/status-bar.component';
@@ -96,6 +99,10 @@ export class AppComponent {
this.applicationEndpoint.settings().subscribe(s =>
this.dialog.open<SettingsComponent, Settings>(SettingsComponent, {
data: s,
maxWidth: '100vw',
maxHeight: '100vh',
width: '80vw',
height: '80vh',
})
);
};
@@ -1,10 +1,8 @@
<h2 mat-dialog-title>Confirmation</h2>
<mat-dialog-content
class="mat-typography"
style="min-width: 30vw">
<mat-dialog-content class="mat-typography" style="min-width: 30vw">
{{ data.message }}
<mat-dialog-actions align="end">
<button mat-button (click)="confirm()" cdkFocusInitial>Yes</button>
<button mat-button mat-dialog-close cdkFocusInitial>No</button>
<button mat-flat-button mat-dialog-close cdkFocusInitial>No</button>
<button mat-flat-button color="primary" (click)="confirm()">Yes</button>
</mat-dialog-actions>
</mat-dialog-content>
@@ -41,6 +41,8 @@ import { ProgressCellComponent } from '../progress-cell/progress-cell.component'
import { Image } from '../domain/image.model';
import { ImageDialogData, ImagesComponent } from '../images/images.component';
import { formatBytes } from '../utils/utils';
import { MatListModule } from '@angular/material/list';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
@Component({
selector: 'app-download-table',
@@ -52,6 +54,7 @@ import { formatBytes } from '../utils/utils';
PortalModule,
MatDialogModule,
ProgressCellComponent,
MatListModule,
],
templateUrl: './download-table.component.html',
styleUrls: ['./download-table.component.scss'],
@@ -72,19 +75,23 @@ export class DownloadTableComponent implements OnDestroy {
private applicationEndpoint: ApplicationEndpointService,
private overlayPositionBuilder: OverlayPositionBuilder,
private overlay: Overlay,
private dialog: MatDialog
private dialog: MatDialog,
breakpointObserver: BreakpointObserver
) {
this.gridOptions = <GridOptions>{
columnDefs: [
{
colId: 'title',
headerName: 'Title',
field: 'postTitle',
tooltipField: 'postTitle',
headerCheckboxSelection: true,
headerCheckboxSelectionFilteredOnly: true,
flex: 2,
minWidth: 200,
},
{
colId: 'progress',
headerName: 'Progress',
tooltipValueGetter: params => {
if (!params.data) {
@@ -103,8 +110,11 @@ export class DownloadTableComponent implements OnDestroy {
: (params.data.done / params.data.total) * 100;
},
cellRenderer: ProgressCellComponent,
flex: 1,
minWidth: 100,
},
{
colId: 'status',
headerName: 'Status',
field: 'status',
tooltipValueGetter: params => {
@@ -121,14 +131,17 @@ export class DownloadTableComponent implements OnDestroy {
value.substring(1, value.length).toLowerCase()
);
},
flex: 1,
},
{
colId: 'path',
headerName: 'Path',
field: 'downloadDirectory',
tooltipField: 'downloadDirectory',
flex: 1,
},
{
colId: 'total',
headerName: 'Total',
tooltipValueGetter: params =>
`${params.data?.done}/${params.data?.total} (${formatBytes(
@@ -138,25 +151,32 @@ export class DownloadTableComponent implements OnDestroy {
`${params.data?.done}/${params.data?.total} (${formatBytes(
params.data?.downloaded
)})`,
flex: 1,
},
{
colId: 'hosts',
headerName: 'Hosts',
field: 'hosts',
tooltipField: 'hosts',
valueGetter: (params: ValueGetterParams<Post>) => {
return params.data?.hosts.join(', ');
},
flex: 1,
},
{
colId: 'addedOn',
headerName: 'Added On',
field: 'addedOn',
tooltipField: 'addedOn',
flex: 1,
},
{
colId: 'order',
headerName: 'Order',
field: 'rank',
tooltipField: 'rank',
sort: 'asc',
flex: 0.5,
},
],
defaultColDef: {
@@ -165,7 +185,17 @@ export class DownloadTableComponent implements OnDestroy {
},
rowSelection: 'multiple',
getRowId: row => row.data['postId'],
onGridReady: () => this.connect(),
onGridReady: () => {
this.connect();
breakpointObserver
.observe(Breakpoints.HandsetPortrait)
.subscribe(result => {
this.agGrid.columnApi.setColumnsVisible(
['status', 'total', 'hosts', 'path', 'addedOn', 'order'],
!result.matches
);
});
},
onRowDataUpdated: (event: RowDataUpdatedEvent<Post>) =>
this.rowCountChange.emit(event.api.getDisplayedRowCount()),
onCellContextMenu: (event: CellContextMenuEvent<Post>) => {
@@ -252,7 +282,13 @@ export class DownloadTableComponent implements OnDestroy {
return;
}
const data: ImageDialogData = { postId: event.data.postId };
this.dialog.open(ImagesComponent, { data });
this.dialog.open(ImagesComponent, {
data,
maxWidth: '100vw',
maxHeight: '100vh',
width: '80vw',
height: '80vh',
});
},
};
}
@@ -1,14 +1,12 @@
<h2 mat-dialog-title>Images</h2>
<mat-dialog-content
class="mat-typography"
style="min-width: 50vw; min-height: 50vh">
<mat-dialog-content class="mat-typography">
<ag-grid-angular
style="width: 50vw; height: 50vh"
style="width: 100%; height: 100%"
class="ag-theme-alpine"
#agGrid
[gridOptions]="gridOptions">
</ag-grid-angular>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button [mat-dialog-close]="true" cdkFocusInitial>Close</button>
<button mat-flat-button [mat-dialog-close]="true" cdkFocusInitial>Close</button>
</mat-dialog-actions>
@@ -10,6 +10,8 @@ import { MatButtonModule } from '@angular/material/button';
import { ValueGetterParams } from 'ag-grid-community/dist/lib/entities/colDef';
import { ProgressCellComponent } from '../progress-cell/progress-cell.component';
import { ITooltipParams } from 'ag-grid-community/dist/lib/rendering/tooltipComponent';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { DialogRef } from '@angular/cdk/dialog';
export interface ImageDialogData {
postId: number;
@@ -30,23 +32,29 @@ export class ImagesComponent implements OnDestroy {
constructor(
@Inject(MAT_DIALOG_DATA) public data: ImageDialogData,
private applicationEndpoint: ApplicationEndpointService
public dialogRef: DialogRef<ImageDialogData>,
private applicationEndpoint: ApplicationEndpointService,
breakpointObserver: BreakpointObserver
) {
this.gridOptions = <GridOptions>{
columnDefs: [
{
colId: 'index',
headerName: '#',
field: 'index',
tooltipField: 'index',
sort: 'asc',
},
{
headerName: 'URL',
field: 'url',
tooltipField: 'url',
flex: 1,
},
{
colId: 'url',
headerName: 'URL',
field: 'url',
tooltipField: 'url',
flex: 2,
},
{
colId: 'progress',
headerName: 'Progress',
field: 'progress',
tooltipValueGetter: (params: ITooltipParams<Image>) => {
@@ -66,8 +74,10 @@ export class ImagesComponent implements OnDestroy {
: (params.data.downloaded / params.data.size) * 100;
},
cellRenderer: ProgressCellComponent,
flex: 1,
},
{
colId: 'status',
headerName: 'Status',
field: 'status',
valueFormatter: (params: ValueFormatterParams<Image>) => {
@@ -77,6 +87,7 @@ export class ImagesComponent implements OnDestroy {
value.substring(1, value.length).toLowerCase()
);
},
flex: 1,
},
],
defaultColDef: {
@@ -84,7 +95,23 @@ export class ImagesComponent implements OnDestroy {
resizable: true,
},
getRowId: row => row.data['url'].toString(),
onGridReady: () => this.connect(),
onGridReady: () => {
this.connect();
breakpointObserver
.observe(Breakpoints.HandsetPortrait)
.subscribe(result => {
this.agGrid.columnApi.setColumnsVisible(
['index', 'status'],
!result.matches
);
if (result.matches) {
this.dialogRef.updateSize('100vw', '80vh');
} else {
this.dialogRef.updateSize('80vw', '80vh');
}
this.dialogRef.updatePosition();
});
},
};
}
@@ -43,6 +43,7 @@ export class LogTableComponent implements OnInit, OnDestroy {
field: 'time',
tooltipField: 'time',
sort: 'desc',
flex: 1,
},
{
headerName: 'Type',
@@ -70,11 +71,13 @@ export class LogTableComponent implements OnInit, OnDestroy {
return params.data.type;
}
},
flex: 1,
},
{
headerName: 'Status',
field: 'status',
tooltipField: 'status',
flex: 1,
},
{
headerName: 'Message',
@@ -41,7 +41,13 @@ export class PostContextmenuComponent {
openImages = () => {
const data: ImageDialogData = { postId: this.post.postId };
this.dialog.open(ImagesComponent, { data });
this.dialog.open(ImagesComponent, {
data,
maxWidth: '100vw',
maxHeight: '100vh',
width: '80vw',
height: '80vh',
});
};
onPostStart!: () => void;
@@ -2,12 +2,13 @@
<mat-dialog-content
class="mat-typography"
style="min-width: 50vw; min-height: 50vh">
<mat-form-field appearance="outline" style="width: 100%; height: 100%; padding-top: 20px">
<mat-form-field
appearance="outline"
style="width: 100%; height: calc(100% - 20px); padding-top: 20px">
<mat-label>Add new links</mat-label>
<textarea
[formControl]="formControl"
cdkTextareaAutosize
cdkAutosizeMinRows="5"
matInput
name="url"
placeholder="Put each link in a new line"
@@ -15,8 +16,11 @@
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button mat-button [mat-dialog-close]="formControl.value" cdkFocusInitial>
<button mat-flat-button mat-dialog-close cdkFocusInitial>Cancel</button>
<button
mat-flat-button
color="primary"
[mat-dialog-close]="formControl.value">
Scan
</button>
</mat-dialog-actions>
@@ -5,6 +5,8 @@ import { MatButtonModule } from '@angular/material/button';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { TextFieldModule } from '@angular/cdk/text-field';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { DialogRef } from '@angular/cdk/dialog';
@Component({
selector: 'app-scan',
@@ -22,4 +24,19 @@ import { TextFieldModule } from '@angular/cdk/text-field';
})
export class ScanComponent {
formControl = new FormControl<string>('');
constructor(
public dialogRef: DialogRef<never>,
breakpointObserver: BreakpointObserver
) {
breakpointObserver
.observe(Breakpoints.HandsetPortrait)
.subscribe(result => {
if (result.matches) {
this.dialogRef.updateSize('100vw', '80vh');
} else {
this.dialogRef.updateSize('80vw', '80vh');
}
this.dialogRef.updatePosition();
});
}
}
@@ -128,22 +128,22 @@
<mat-form-field appearance="outline">
<mat-label>Global concurrent downloads</mat-label>
<input
formControlName='maxConcurrentPerHost'
formControlName="maxConcurrentPerHost"
matInput
max="12"
min="0"
name='maxConcurrentPerHost'
name="maxConcurrentPerHost"
required
type="number" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Concurrent downloads per host</mat-label>
<input
formControlName='maxGlobalConcurrent'
formControlName="maxGlobalConcurrent"
matInput
max="4"
min="1"
name='maxGlobalConcurrent'
name="maxGlobalConcurrent"
required
type="number" />
</mat-form-field>
@@ -229,9 +229,9 @@
</div>
</form>
</mat-tab>
<mat-tab label='System'>
<mat-tab label="System">
<form
[formGroup]='systemSettingsForm'
[formGroup]="systemSettingsForm"
autocomplete="off"
style="
display: flex;
@@ -242,17 +242,17 @@
">
<mat-form-field appearance="outline">
<mat-label>Temporary Path</mat-label>
<input formControlName='tempPath' matInput name='tempPath' required />
<input formControlName="tempPath" matInput name="tempPath" required />
</mat-form-field>
<mat-form-field appearance='outline'>
<mat-form-field appearance="outline">
<mat-label>Cache Path</mat-label>
<input
formControlName='cachePath'
formControlName="cachePath"
matInput
name='cachePath'
name="cachePath"
required />
</mat-form-field>
<mat-form-field appearance='outline'>
<mat-form-field appearance="outline">
<mat-label>Maximum log entries</mat-label>
<input
formControlName="maxEventLog"
@@ -268,6 +268,6 @@
</mat-tab-group>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button (click)="save()" mat-button cdkFocusInitial>Save</button>
<button mat-flat-button mat-dialog-close cdkFocusInitial>Cancel</button>
<button (click)="save()" mat-flat-button color="primary">Save</button>
</mat-dialog-actions>
@@ -25,6 +25,8 @@ import {
ViperSettings,
} from '../domain/settings.model';
import { ApplicationEndpointService } from '../services/application-endpoint.service';
import { DialogRef } from '@angular/cdk/dialog';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
@Component({
selector: 'app-settings',
@@ -87,13 +89,23 @@ export class SettingsComponent {
constructor(
@Inject(MAT_DIALOG_DATA) public data: Settings,
public dialogRef: MatDialogRef<SettingsComponent>,
private applicationEndpoint: ApplicationEndpointService
public dialogRef: DialogRef<Settings>,
private applicationEndpoint: ApplicationEndpointService,
breakpointObserver: BreakpointObserver
) {
this.viperGirlsSettingsForm.reset(data.viperSettings);
this.downloadSettingsForm.reset(data.downloadSettings);
this.connectionSettingsForm.reset(data.connectionSettings);
this.systemSettingsForm.reset(data.systemSettings);
breakpointObserver
.observe(Breakpoints.HandsetPortrait)
.subscribe(result => {
if (result.matches) {
this.dialogRef.updateSize('100vw', '80vh');
} else {
this.dialogRef.updateSize('80vw', '80vh');
}
});
}
save = () => {
@@ -1,22 +1,32 @@
<div class='status-bar' style='display: flex'>
<span style='padding-left: 10px; flex-grow: 1'>
<ng-container *ngIf='vgUsername$ | async as vgUsername'>
<span>{{
vgUsername.length > 0 ? 'Logged in as: ' + vgUsername : ''
<div class="status-bar" style="display: flex; justify-content: space-between">
<div>
<span
*ngIf="!(handsetPortrait$ | async)?.matches"
style="padding-left: 10px; flex-grow: 1">
<ng-container *ngIf="vgUsername$ | async as vgUsername">
<span>{{
vgUsername.length > 0 ? 'Logged in as: ' + vgUsername : ''
}}</span>
</ng-container>
</span>
<span *ngIf='downloadSpeed$ | async as downloadSpeed'>{{
(downloadSpeed.speed | downloadSpeed) + '/s'
</ng-container>
</span>
</div>
<div style="display: flex">
<span *ngIf="downloadSpeed$ | async as downloadSpeed">{{
(downloadSpeed.speed | downloadSpeed) + '/s'
}}</span>
<mat-divider [vertical]='true' style='height: 20px'></mat-divider>
<ng-container *ngIf='queueState$ | async as queueState'>
<span>Downloading: {{ queueState.running }}</span>
<mat-divider [vertical]="true" style="height: 20px"></mat-divider>
<span>Pending: {{ queueState.remaining }}</span>
<mat-divider [vertical]="true" style="height: 20px"></mat-divider>
</ng-container>
<span *ngIf='errorCount$ | async as errorCount' style='padding-right: 10px'
>Error: {{ errorCount.count }}</span
>
<ng-container *ngIf="queueState$ | async as queueState">
<ng-container *ngIf="!(handsetPortrait$ | async)?.matches">
<mat-divider [vertical]="true" style="height: 20px"></mat-divider>
<span>Downloading: {{ queueState.running }}</span>
<mat-divider [vertical]="true" style="height: 20px"></mat-divider>
<span>Pending: {{ queueState.remaining }}</span>
</ng-container>
</ng-container>
<ng-container *ngIf="errorCount$ | async as errorCount">
<ng-container *ngIf="!(handsetPortrait$ | async)?.matches">
<mat-divider [vertical]="true" style="height: 20px"></mat-divider>
<span style="padding-right: 10px">Error: {{ errorCount.count }}</span>
</ng-container>
</ng-container>
</div>
</div>
@@ -3,6 +3,7 @@ import { CommonModule, NgIf } from '@angular/common';
import { ApplicationEndpointService } from '../services/application-endpoint.service';
import { MatDividerModule } from '@angular/material/divider';
import { DownloadSpeedPipe } from '../pipes/download-speed.pipe';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
@Component({
selector: 'app-status-bar',
@@ -12,7 +13,13 @@ import { DownloadSpeedPipe } from '../pipes/download-speed.pipe';
styleUrls: ['./status-bar.component.scss'],
})
export class StatusBarComponent {
constructor(private applicationEndpoint: ApplicationEndpointService) {}
handsetPortrait$ = this.breakpointObserver.observe(
Breakpoints.HandsetPortrait
);
constructor(
private applicationEndpoint: ApplicationEndpointService,
private breakpointObserver: BreakpointObserver
) {}
queueState$ = this.applicationEndpoint.queueState$;
downloadSpeed$ = this.applicationEndpoint.downloadSpeed$;
@@ -1,14 +1,15 @@
<h2 mat-dialog-title>Thread</h2>
<mat-dialog-content
class="mat-typography"
style="min-width: 50vw; min-height: 50vh">
<mat-dialog-content class="mat-typography">
<ag-grid-angular
style="width: 50vw; height: 50vh"
style="width: 100%; height: 100%"
class="ag-theme-alpine"
#agGrid
[gridOptions]="gridOptions">
</ag-grid-angular>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="downloadSelected()" cdkFocusInitial>Download</button>
<button mat-flat-button [mat-dialog-close]="true" cdkFocusInitial>
Cancel
</button>
<button mat-flat-button color="primary" (click)="downloadSelected()">Download</button>
</mat-dialog-actions>
@@ -2,11 +2,7 @@ import { Component, Inject, ViewChild } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AgGridAngular, AgGridModule } from 'ag-grid-angular';
import { MatButtonModule } from '@angular/material/button';
import {
MAT_DIALOG_DATA,
MatDialogModule,
MatDialogRef,
} from '@angular/material/dialog';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import {
GridOptions,
ITooltipParams,
@@ -16,6 +12,8 @@ import { ApplicationEndpointService } from '../services/application-endpoint.ser
import { GridReadyEvent } from 'ag-grid-community/dist/lib/events';
import { PostItem } from '../domain/post-item.model';
import { ValueGetterParams } from 'ag-grid-community/dist/lib/entities/colDef';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { DialogRef } from '@angular/cdk/dialog';
export interface ThreadDialogData {
threadId: number;
@@ -35,31 +33,37 @@ export class ThreadSelectionComponent {
constructor(
@Inject(MAT_DIALOG_DATA) public data: ThreadDialogData,
private dialogRef: MatDialogRef<ThreadSelectionComponent>,
private applicationEndpoint: ApplicationEndpointService
private dialogRef: DialogRef<ThreadDialogData>,
private applicationEndpoint: ApplicationEndpointService,
breakpointObserver: BreakpointObserver
) {
this.gridOptions = <GridOptions>{
columnDefs: [
{
colId: 'number',
headerName: 'Number',
field: 'number',
tooltipField: 'number',
checkboxSelection: true,
headerCheckboxSelection: true,
flex: 1,
},
{
colId: 'title',
headerName: 'Title',
field: 'title',
tooltipField: 'title',
flex: 2,
},
{
colId: 'url',
headerName: 'URL',
field: 'url',
tooltipField: 'url',
flex: 1,
flex: 2,
},
{
colId: 'hosts',
headerName: 'Hosts',
field: 'hosts',
tooltipValueGetter: (params: ITooltipParams<PostItem>) => {
@@ -72,6 +76,7 @@ export class ThreadSelectionComponent {
.map(v => `${v.first} (${v.second})`)
.join(', ');
},
flex: 1,
},
],
defaultColDef: {
@@ -81,6 +86,17 @@ export class ThreadSelectionComponent {
rowSelection: 'multiple',
getRowId: row => row.data['postId'].toString(),
onGridReady: (event: GridReadyEvent<PostItem>) => {
breakpointObserver
.observe(Breakpoints.HandsetPortrait)
.subscribe(result => {
this.agGrid.columnApi.setColumnsVisible(['url'], !result.matches);
if (result.matches) {
this.dialogRef.updateSize('100vw', '80vh');
} else {
this.dialogRef.updateSize('80vw', '80vh');
}
this.dialogRef.updatePosition();
});
this.applicationEndpoint
.getThreadPosts(this.data.threadId)
.subscribe(result => {
@@ -75,18 +75,19 @@ export class ThreadTableComponent implements OnDestroy {
headerName: 'Title',
field: 'title',
tooltipField: 'title',
flex: 1,
flex: 2,
},
{
headerName: 'Url',
field: 'link',
tooltipField: 'link',
flex: 1,
flex: 2,
},
{
headerName: 'Count',
field: 'total',
tooltipField: 'total',
flex: 1,
},
],
defaultColDef: {
@@ -138,7 +139,13 @@ export class ThreadTableComponent implements OnDestroy {
return;
}
const data: ThreadDialogData = { threadId: event.data.threadId };
this.dialog.open(ThreadSelectionComponent, { data });
this.dialog.open(ThreadSelectionComponent, {
data,
maxWidth: '100vw',
maxHeight: '100vh',
width: '80vw',
height: '80vh',
});
};
ref.instance.onThreadDelete = () => {
@@ -177,7 +184,13 @@ export class ThreadTableComponent implements OnDestroy {
return;
}
const data: ThreadDialogData = { threadId: event.data.threadId };
this.dialog.open(ThreadSelectionComponent, { data });
this.dialog.open(ThreadSelectionComponent, {
data,
maxWidth: '100vw',
maxHeight: '100vh',
width: '80vw',
height: '80vh',
});
},
};
}
@@ -26,27 +26,31 @@
<mat-divider
[vertical]="true"
style="height: 20px; flex-grow: 0"></mat-divider>
<button
[disabled]="disableSelected()"
(click)="startSelectedClick()"
mat-icon-button
matTooltip="Start selected">
<mat-icon>play_arrow</mat-icon>
</button>
<button
[disabled]="disableSelected()"
(click)="stopSelectedClick()"
mat-icon-button
matTooltip="Stop selected">
<mat-icon>pause</mat-icon>
</button>
<button
[disabled]="disableSelected()"
(click)="removeSelectedClick()"
mat-icon-button
matTooltip="Remove selected">
<mat-icon>delete</mat-icon>
</button>
<ng-container *ngIf="handsetPortrait$ | async as handsetPortrait">
@if (!handsetPortrait.matches) {
<button
[disabled]="disableSelected()"
(click)="startSelectedClick()"
mat-icon-button
matTooltip="Start selected">
<mat-icon>play_arrow</mat-icon>
</button>
<button
[disabled]="disableSelected()"
(click)="stopSelectedClick()"
mat-icon-button
matTooltip="Stop selected">
<mat-icon>pause</mat-icon>
</button>
<button
[disabled]="disableSelected()"
(click)="removeSelectedClick()"
mat-icon-button
matTooltip="Remove selected">
<mat-icon>delete</mat-icon>
</button>
}
</ng-container>
<mat-divider
[vertical]="true"
style="height: 20px; flex-grow: 0"></mat-divider>
@@ -78,9 +82,9 @@
<button mat-icon-button matTooltip="Settings" (click)="settingsClick()">
<mat-icon>settings</mat-icon>
</button>
<button mat-icon-button matTooltip="About">
<mat-icon>info</mat-icon>
</button>
<!-- <button mat-icon-button matTooltip="About">-->
<!-- <mat-icon>info</mat-icon>-->
<!-- </button>-->
</div>
</div>
</mat-toolbar-row>
@@ -14,6 +14,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
import { ScanComponent } from '../scan/scan.component';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
@Component({
selector: 'app-toolbar',
@@ -69,11 +70,21 @@ export class ToolbarComponent {
@Input({ required: true })
disableSelected!: Signal<boolean>;
constructor(public dialog: MatDialog) {}
handsetPortrait$ = this.breakpointObserver.observe(
Breakpoints.HandsetPortrait
);
constructor(
public dialog: MatDialog,
private breakpointObserver: BreakpointObserver
) {}
openScanDialog() {
this.dialog
.open<ScanComponent, never, string>(ScanComponent)
.open<ScanComponent, never, string>(ScanComponent, {
maxWidth: '100vw',
maxHeight: '100vh',
})
.afterClosed()
.subscribe(v => {
if (v) {
+3 -9
View File
@@ -17,7 +17,7 @@ body {
}
#tab-group {
height: calc(100% - 64px);
height: calc(100% - 84px);
}
.ag-theme-alpine {
@@ -135,12 +135,6 @@ body {
}
}
.status-bar {
position: fixed;
width: 100%;
bottom: 0;
left: 0;
white-space: nowrap;
padding: 2px 0;
z-index: 9999;
.mat-mdc-dialog-content {
max-height: 100vh !important;
}
@@ -17,23 +17,14 @@ class DataBroadcast(
@PostConstruct
private fun run() {
eventBus.events.ofType(PostEvent::class.java).buffer(Duration.ofMillis(125)).subscribe { events ->
events.map { it.delete }.flatten().also {
if (it.isNotEmpty()) {
template.convertAndSend("/topic/posts/deleted", it)
}
}
events.map { it.add }.flatten().also {
if (it.isNotEmpty()) {
template.convertAndSend("/topic/posts/new", it)
}
}
events.map { it.update }.flatten().reversed().distinct().also {
template.convertAndSend("/topic/posts/updated", it)
}
eventBus.events.ofType(PostCreateEvent::class.java).subscribe { events ->
template.convertAndSend("/topic/posts/new", events.posts)
}
eventBus.events.ofType(PostUpdateEvent::class.java).subscribe { events ->
template.convertAndSend("/topic/posts/updated", events.posts)
}
eventBus.events.ofType(PostDeleteEvent::class.java).subscribe { events ->
template.convertAndSend("/topic/posts/deleted", events.postIds)
}
eventBus.events.ofType(QueueStateEvent::class.java).subscribe {
@@ -56,41 +47,35 @@ class DataBroadcast(
template.convertAndSend("/topic/loading", it.loading)
}
eventBus.events.ofType(ImageEvent::class.java).buffer(Duration.ofMillis(125)).subscribe { events ->
events.map { it.images }.flatten().reversed().distinct().groupBy { it.postId }
.forEach { template.convertAndSend("/topic/images/${it.key}", it.value) }
eventBus.events.ofType(ImageEvent::class.java).buffer(Duration.ofMillis(500)).subscribe { events ->
events.reversed().map { it.images }.flatten().distinct().groupBy { it.postId }
.forEach {
template.convertAndSend("/topic/images/${it.key}", it.value)
}
}
eventBus.events.ofType(ThreadCreateEvent::class.java).map { it.thread }.distinct()
.buffer(Duration.ofMillis(125)).subscribe {
template.convertAndSend("/topic/threads", it)
eventBus.events.ofType(ThreadCreateEvent::class.java).map { listOf(it.thread) }
.subscribe { events ->
template.convertAndSend("/topic/threads", events)
}
eventBus.events.ofType(ThreadDeleteEvent::class.java).map { it.threadId }.distinct()
.buffer(Duration.ofMillis(125)).subscribe {
eventBus.events.ofType(ThreadDeleteEvent::class.java).map { listOf(it.threadId) }
.subscribe {
template.convertAndSend("/topic/threads/deleted", it)
}
eventBus.events.ofType(ThreadClearEvent::class.java).subscribe {
template.convertAndSend("/topic/threads/deletedAll", listOf(true))
eventBus.events.ofType(ThreadClearEvent::class.java).map { listOf(true) }.subscribe {
template.convertAndSend("/topic/threads/deletedAll", it)
}
eventBus.events.ofType(LogCreateEvent::class.java).map { it.logEntry }.buffer(Duration.ofMillis(125))
eventBus.events.ofType(LogCreateEvent::class.java).map { it.logEntry }
.subscribe { logCreateEvent ->
logCreateEvent.distinct().also {
if (it.isNotEmpty()) {
template.convertAndSend("/topic/logs/new", it)
}
}
template.convertAndSend("/topic/logs/new", logCreateEvent)
}
eventBus.events.ofType(LogUpdateEvent::class.java).map { it.logEntry }.buffer(Duration.ofMillis(125))
eventBus.events.ofType(LogUpdateEvent::class.java).map { it.logEntry }
.subscribe { logUpdateEvent ->
logUpdateEvent.distinct().also {
if (it.isNotEmpty()) {
template.convertAndSend("/topic/logs/updated", it)
}
}
template.convertAndSend("/topic/logs/updated", logUpdateEvent)
}
eventBus.events.ofType(LogDeleteEvent::class.java).subscribe {