feat: custom the file size and duration limit

This commit is contained in:
acgnhik 2022-07-31 13:03:05 +08:00
parent 1b6ac56099
commit 7282a1b429
32 changed files with 521 additions and 145 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -10,6 +10,6 @@
<body>
<app-root></app-root>
<noscript>Please enable JavaScript to continue using this application.</noscript>
<script src="runtime.d0b2d3624e101c6d.js" type="module"></script><script src="polyfills.4b08448aee19bb22.js" type="module"></script><script src="main.888c50197ddf8040.js" type="module"></script>
<script src="runtime.68e08c4d681726f6.js" type="module"></script><script src="polyfills.4b08448aee19bb22.js" type="module"></script><script src="main.16a8fc7b1f8a870d.js" type="module"></script>
</body></html>

View File

@ -1,6 +1,6 @@
{
"configVersion": 1,
"timestamp": 1659242444285,
"timestamp": 1659243718041,
"index": "/index.html",
"assetGroups": [
{
@ -12,17 +12,17 @@
},
"urls": [
"/103.5b5d2a6e5a8a7479.js",
"/146.92e3b29c4c754544.js",
"/183.90c399afcab1b014.js",
"/202.f24f6ef0c4c16342.js",
"/146.5a8902910bda9e87.js",
"/183.2c7c85597ba82f9e.js",
"/45.c90c3cea2bf1a66e.js",
"/66.d61b8b935d3ed1ff.js",
"/500.5d39ab52fb714a12.js",
"/91.be3cbd4101dc7500.js",
"/common.858f777e9296e6f2.js",
"/index.html",
"/main.888c50197ddf8040.js",
"/main.16a8fc7b1f8a870d.js",
"/manifest.webmanifest",
"/polyfills.4b08448aee19bb22.js",
"/runtime.d0b2d3624e101c6d.js",
"/runtime.68e08c4d681726f6.js",
"/styles.2e152d608221c2ee.css"
],
"patterns": []
@ -1635,11 +1635,11 @@
"dataGroups": [],
"hashTable": {
"/103.5b5d2a6e5a8a7479.js": "cc0240f217015b6d4ddcc14f31fcc42e1c1c282a",
"/146.92e3b29c4c754544.js": "3824de681dd1f982ea69a065cdf54d7a1e781f4d",
"/183.90c399afcab1b014.js": "467a8b4c21dace3ae358507932287ca3596051e6",
"/202.f24f6ef0c4c16342.js": "0e583339251cf7346f46b19f13342dd30ab9d6ad",
"/146.5a8902910bda9e87.js": "d9c33c7073662699f00f46f3a384ae5b749fdef9",
"/183.2c7c85597ba82f9e.js": "22a1524d6399d9bde85334a2eba15670f68ccd96",
"/45.c90c3cea2bf1a66e.js": "e5bfb8cf3803593e6b8ea14c90b3d3cb6a066764",
"/66.d61b8b935d3ed1ff.js": "6b81e8268d5a2d2596b0a7926985dd80fb06532a",
"/500.5d39ab52fb714a12.js": "646fbfd3af1124519171f1cd9fac4c214b5af60f",
"/91.be3cbd4101dc7500.js": "f0fec71455c96f9a60c4fa671d2ccdba07e9a00a",
"/assets/animal/panda.js": "fec2868bb3053dd2da45f96bbcb86d5116ed72b1",
"/assets/animal/panda.svg": "bebd302cdc601e0ead3a6d2710acf8753f3d83b1",
"/assets/fill/.gitkeep": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
@ -3234,11 +3234,11 @@
"/assets/twotone/warning.js": "fb2d7ea232f3a99bf8f080dbc94c65699232ac01",
"/assets/twotone/warning.svg": "8c7a2d3e765a2e7dd58ac674870c6655cecb0068",
"/common.858f777e9296e6f2.js": "b68ca68e1e214a2537d96935c23410126cc564dd",
"/index.html": "eb71622159c7cfc8674746c857e97f0825058b53",
"/main.888c50197ddf8040.js": "f506b85641a4598b002c21bc49c9a36e0c058326",
"/index.html": "3f28dbdfc92c1a0930448a8ff6d5d2ac49648987",
"/main.16a8fc7b1f8a870d.js": "9c680888ae14907d6c20e60c026b49a2331768e9",
"/manifest.webmanifest": "62c1cb8c5ad2af551a956b97013ab55ce77dd586",
"/polyfills.4b08448aee19bb22.js": "8e73f2d42cc13ca353cea5c886d930bd6da08d0d",
"/runtime.d0b2d3624e101c6d.js": "3d52e48c0441e75e5d16b979724068f2cd8e2914",
"/runtime.68e08c4d681726f6.js": "04815a3dd35466f647f3707a295bc2c76c9f0375",
"/styles.2e152d608221c2ee.css": "9830389a46daa5b4511e0dd343aad23ca9f9690f"
},
"navigationUrls": [

View File

@ -0,0 +1 @@
(()=>{"use strict";var e,v={},m={};function r(e){var i=m[e];if(void 0!==i)return i.exports;var t=m[e]={exports:{}};return v[e].call(t.exports,t,t.exports,r),t.exports}r.m=v,e=[],r.O=(i,t,o,f)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,o,f]=e[n],c=!0,l=0;l<t.length;l++)(!1&f||a>=f)&&Object.keys(r.O).every(b=>r.O[b](t[l]))?t.splice(l--,1):(c=!1,f<a&&(a=f));if(c){e.splice(n--,1);var d=o();void 0!==d&&(i=d)}}return i}f=f||0;for(var n=e.length;n>0&&e[n-1][2]>f;n--)e[n]=e[n-1];e[n]=[t,o,f]},r.n=e=>{var i=e&&e.__esModule?()=>e.default:()=>e;return r.d(i,{a:i}),i},r.d=(e,i)=>{for(var t in i)r.o(i,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:i[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((i,t)=>(r.f[t](e,i),i),[])),r.u=e=>(592===e?"common":e)+"."+{45:"c90c3cea2bf1a66e",91:"be3cbd4101dc7500",103:"5b5d2a6e5a8a7479",146:"5a8902910bda9e87",183:"2c7c85597ba82f9e",500:"5d39ab52fb714a12",592:"858f777e9296e6f2"}[e]+".js",r.miniCssF=e=>{},r.o=(e,i)=>Object.prototype.hasOwnProperty.call(e,i),(()=>{var e={},i="blrec:";r.l=(t,o,f,n)=>{if(e[t])e[t].push(o);else{var a,c;if(void 0!==f)for(var l=document.getElementsByTagName("script"),d=0;d<l.length;d++){var u=l[d];if(u.getAttribute("src")==t||u.getAttribute("data-webpack")==i+f){a=u;break}}a||(c=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",i+f),a.src=r.tu(t)),e[t]=[o];var s=(g,b)=>{a.onerror=a.onload=null,clearTimeout(p);var _=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),_&&_.forEach(h=>h(b)),g)return g(b)},p=setTimeout(s.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=s.bind(null,a.onerror),a.onload=s.bind(null,a.onload),c&&document.head.appendChild(a)}}})(),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;r.tu=i=>(void 0===e&&(e={createScriptURL:t=>t},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e.createScriptURL(i))})(),r.p="",(()=>{var e={666:0};r.f.j=(o,f)=>{var n=r.o(e,o)?e[o]:void 0;if(0!==n)if(n)f.push(n[2]);else if(666!=o){var a=new Promise((u,s)=>n=e[o]=[u,s]);f.push(n[2]=a);var c=r.p+r.u(o),l=new Error;r.l(c,u=>{if(r.o(e,o)&&(0!==(n=e[o])&&(e[o]=void 0),n)){var s=u&&("load"===u.type?"missing":u.type),p=u&&u.target&&u.target.src;l.message="Loading chunk "+o+" failed.\n("+s+": "+p+")",l.name="ChunkLoadError",l.type=s,l.request=p,n[1](l)}},"chunk-"+o,o)}else e[o]=0},r.O.j=o=>0===e[o];var i=(o,f)=>{var l,d,[n,a,c]=f,u=0;if(n.some(p=>0!==e[p])){for(l in a)r.o(a,l)&&(r.m[l]=a[l]);if(c)var s=c(r)}for(o&&o(f);u<n.length;u++)r.o(e,d=n[u])&&e[d]&&e[d][0](),e[n[u]]=0;return r.O(s)},t=self.webpackChunkblrec=self.webpackChunkblrec||[];t.forEach(i.bind(null,0)),t.push=i.bind(null,t.push.bind(t))})()})();

View File

@ -1 +0,0 @@
(()=>{"use strict";var e,v={},m={};function r(e){var f=m[e];if(void 0!==f)return f.exports;var t=m[e]={exports:{}};return v[e].call(t.exports,t,t.exports,r),t.exports}r.m=v,e=[],r.O=(f,t,i,o)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,i,o]=e[n],c=!0,l=0;l<t.length;l++)(!1&o||a>=o)&&Object.keys(r.O).every(b=>r.O[b](t[l]))?t.splice(l--,1):(c=!1,o<a&&(a=o));if(c){e.splice(n--,1);var d=i();void 0!==d&&(f=d)}}return f}o=o||0;for(var n=e.length;n>0&&e[n-1][2]>o;n--)e[n]=e[n-1];e[n]=[t,i,o]},r.n=e=>{var f=e&&e.__esModule?()=>e.default:()=>e;return r.d(f,{a:f}),f},r.d=(e,f)=>{for(var t in f)r.o(f,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:f[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((f,t)=>(r.f[t](e,f),f),[])),r.u=e=>(592===e?"common":e)+"."+{45:"c90c3cea2bf1a66e",66:"d61b8b935d3ed1ff",103:"5b5d2a6e5a8a7479",146:"92e3b29c4c754544",183:"90c399afcab1b014",202:"f24f6ef0c4c16342",592:"858f777e9296e6f2"}[e]+".js",r.miniCssF=e=>{},r.o=(e,f)=>Object.prototype.hasOwnProperty.call(e,f),(()=>{var e={},f="blrec:";r.l=(t,i,o,n)=>{if(e[t])e[t].push(i);else{var a,c;if(void 0!==o)for(var l=document.getElementsByTagName("script"),d=0;d<l.length;d++){var u=l[d];if(u.getAttribute("src")==t||u.getAttribute("data-webpack")==f+o){a=u;break}}a||(c=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",f+o),a.src=r.tu(t)),e[t]=[i];var s=(g,b)=>{a.onerror=a.onload=null,clearTimeout(p);var _=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),_&&_.forEach(h=>h(b)),g)return g(b)},p=setTimeout(s.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=s.bind(null,a.onerror),a.onload=s.bind(null,a.onload),c&&document.head.appendChild(a)}}})(),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;r.tu=f=>(void 0===e&&(e={createScriptURL:t=>t},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e.createScriptURL(f))})(),r.p="",(()=>{var e={666:0};r.f.j=(i,o)=>{var n=r.o(e,i)?e[i]:void 0;if(0!==n)if(n)o.push(n[2]);else if(666!=i){var a=new Promise((u,s)=>n=e[i]=[u,s]);o.push(n[2]=a);var c=r.p+r.u(i),l=new Error;r.l(c,u=>{if(r.o(e,i)&&(0!==(n=e[i])&&(e[i]=void 0),n)){var s=u&&("load"===u.type?"missing":u.type),p=u&&u.target&&u.target.src;l.message="Loading chunk "+i+" failed.\n("+s+": "+p+")",l.name="ChunkLoadError",l.type=s,l.request=p,n[1](l)}},"chunk-"+i,i)}else e[i]=0},r.O.j=i=>0===e[i];var f=(i,o)=>{var l,d,[n,a,c]=o,u=0;if(n.some(p=>0!==e[p])){for(l in a)r.o(a,l)&&(r.m[l]=a[l]);if(c)var s=c(r)}for(i&&i(o);u<n.length;u++)r.o(e,d=n[u])&&e[d]&&e[d][0](),e[n[u]]=0;return r.O(s)},t=self.webpackChunkblrec=self.webpackChunkblrec||[];t.forEach(f.bind(null,0)),t.push=f.bind(null,t.push.bind(t))})()})();

View File

@ -242,18 +242,22 @@ class OutputOptions(BaseModel):
@validator('filesize_limit')
def _validate_filesize_limit(cls, value: Optional[int]) -> Optional[int]:
# allowed 1 ~ 20 GB, 0 indicates not limit.
# file size in bytes, 0 indicates not limit。
if value is not None:
allowed_values = frozenset(1024**3 * i for i in range(0, 21))
cls._validate_with_collection(value, allowed_values)
if not (0 <= value <= 1073731086581): # 1073731086581(999.99 GB)
raise ValueError(
'The filesize limit must be in the range of 0 to 1073731086581'
)
return value
@validator('duration_limit')
def _validate_duration_limit(cls, value: Optional[int]) -> Optional[int]:
# allowed 1 ~ 24 hours, 0 indicates not limit.
# duration in seconds, 0 indicates not limit。
if value is not None:
allowed_values = frozenset(3600 * i for i in range(0, 25))
cls._validate_with_collection(value, allowed_values)
if not (0 <= value <= 359999): # 359999(99:59:59)
raise ValueError(
'The duration limit must be in the range of 0 to 359999'
)
return value

View File

@ -44,9 +44,19 @@
<nz-form-label
class="setting-label"
nzNoColon
[nzTooltipTitle]="splitFileTip"
[nzTooltipTitle]="filesizeLimitTip"
>大小限制</nz-form-label
>
<ng-template #filesizeLimitTip>
<p>
自动分割文件以限制录播文件大小
<br />
格式:数字 + 单位(GB, MB, KB, B)
<br />
不自动分割文件设置为 <strong>0 B</strong>
<br />
</p>
</ng-template>
<nz-form-control
class="setting-control select"
[nzWarningTip]="syncFailedWarningTip"
@ -54,20 +64,26 @@
syncStatus.filesizeLimit ? filesizeLimitControl : 'warning'
"
>
<nz-select
formControlName="filesizeLimit"
[nzOptions]="filesizeLimitOptions"
>
</nz-select>
<app-input-filesize formControlName="filesizeLimit"></app-input-filesize>
</nz-form-control>
</nz-form-item>
<nz-form-item class="setting-item">
<nz-form-label
class="setting-label"
nzNoColon
[nzTooltipTitle]="splitFileTip"
[nzTooltipTitle]="durationLimitTip"
>时长限制</nz-form-label
>
<ng-template #durationLimitTip>
<p>
自动分割文件以限制录播文件时长
<br />
格式HH:MM:SS
<br />
不自动分割文件设置为 <strong>00:00:00</strong>
<br />
</p>
</ng-template>
<nz-form-control
class="setting-control select"
[nzWarningTip]="syncFailedWarningTip"
@ -75,11 +91,7 @@
syncStatus.durationLimit ? durationLimitControl : 'warning'
"
>
<nz-select
formControlName="durationLimit"
[nzOptions]="durationLimitOptions"
>
</nz-select>
<app-input-duration formControlName="durationLimit"></app-input-duration>
</nz-form-control>
</nz-form-item>
</form>

View File

@ -6,25 +6,23 @@ import {
OnChanges,
ChangeDetectorRef,
} from '@angular/core';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
import {
FormBuilder,
FormControl,
FormGroup,
Validators,
} from '@angular/forms';
import { Observable } from 'rxjs';
import cloneDeep from 'lodash-es/cloneDeep';
import mapValues from 'lodash-es/mapValues';
import type { Mutable } from '../../shared/utility-types';
import { OutputSettings } from '../shared/setting.model';
import { filterValueChanges } from '../shared/rx-operators';
import {
SettingsSyncService,
SyncStatus,
calcSyncStatus,
} from '../shared/services/settings-sync.service';
import {
DURATION_LIMIT_OPTIONS,
FILESIZE_LIMIT_OPTIONS,
SPLIT_FILE_TIP,
SYNC_FAILED_WARNING_TIP,
} from '../shared/constants/form';
import { SYNC_FAILED_WARNING_TIP } from '../shared/constants/form';
@Component({
selector: 'app-output-settings',
@ -37,14 +35,7 @@ export class OutputSettingsComponent implements OnInit, OnChanges {
syncStatus!: SyncStatus<OutputSettings>;
readonly settingsForm: FormGroup;
readonly splitFileTip = SPLIT_FILE_TIP;
readonly syncFailedWarningTip = SYNC_FAILED_WARNING_TIP;
readonly filesizeLimitOptions = cloneDeep(FILESIZE_LIMIT_OPTIONS) as Mutable<
typeof FILESIZE_LIMIT_OPTIONS
>;
readonly durationLimitOptions = cloneDeep(DURATION_LIMIT_OPTIONS) as Mutable<
typeof DURATION_LIMIT_OPTIONS
>;
constructor(
formBuilder: FormBuilder,
@ -54,8 +45,22 @@ export class OutputSettingsComponent implements OnInit, OnChanges {
this.settingsForm = formBuilder.group({
outDir: [''],
pathTemplate: [''],
filesizeLimit: [''],
durationLimit: [''],
filesizeLimit: [
'',
[
Validators.required,
Validators.min(0),
Validators.max(1073731086581), // 1073731086581(999.99 GB)
],
],
durationLimit: [
'',
[
Validators.required,
Validators.min(0),
Validators.max(359999), // 359999(99:59:59)
],
],
});
}
@ -70,6 +75,7 @@ export class OutputSettingsComponent implements OnInit, OnChanges {
get filesizeLimitControl() {
return this.settingsForm.get('filesizeLimit') as FormControl;
}
get durationLimitControl() {
return this.settingsForm.get('durationLimit') as FormControl;
}
@ -84,7 +90,9 @@ export class OutputSettingsComponent implements OnInit, OnChanges {
.syncSettings(
'output',
this.settings,
this.settingsForm.valueChanges as Observable<OutputSettings>
this.settingsForm.valueChanges.pipe(
filterValueChanges<OutputSettings>(this.settingsForm)
)
)
.subscribe((detail) => {
this.syncStatus = { ...this.syncStatus, ...calcSyncStatus(detail) };

View File

@ -1,8 +1,5 @@
import { CoverSaveStrategy, DeleteStrategy } from '../setting.model';
import range from 'lodash-es/range';
export const SPLIT_FILE_TIP = '会按照此限制自动分割文件';
export const SYNC_FAILED_WARNING_TIP = '设置同步失败!';
export const PATH_TEMPLATE_PATTERN =
@ -30,22 +27,6 @@ export const PATH_TEMPLATE_VARIABLES: Readonly<PathTemplateVariable[]> = [
{ name: 'second', desc: '文件创建日期时间之秒数' },
] as const;
export const FILESIZE_LIMIT_OPTIONS = [
{ label: '不限', value: 0 },
...range(1, 21).map((i) => ({
label: `${i} GB`,
value: 1024 ** 3 * i,
})),
] as const;
export const DURATION_LIMIT_OPTIONS = [
{ label: '不限', value: 0 },
...range(1, 25).map((i) => ({
label: `${i} 小时`,
value: 3600 * i,
})),
] as const;
export const DELETE_STRATEGIES = [
{ label: '自动', value: DeleteStrategy.AUTO },
{ label: '谨慎', value: DeleteStrategy.SAFE },

View File

@ -23,16 +23,18 @@ export function filterValueChanges<T extends object>(control: AbstractControl) {
export function trimString<T extends object>() {
return pipe(
map(
(object: T) =>
transform(
object,
(result, value: any, prop) => {
result[prop] = isString(value) ? value.trim() : value;
},
{} as T
) as T
)
map((object: T) => {
if (isString(object)) {
return object.trim() as unknown as T;
}
return transform(
object,
(result, value: any, prop) => {
result[prop] = isString(value) ? value.trim() : value;
},
{} as T
) as T;
})
);
}

View File

@ -0,0 +1,15 @@
<form nz-form [formGroup]="formGroup">
<nz-form-item>
<nz-form-control [nzErrorTip]="errorTip">
<input nz-input type="text" formControlName="duration" />
<ng-template #errorTip let-control>
<ng-container *ngIf="control.hasError('required')">
请输入文件大小
</ng-container>
<ng-container *ngIf="control.hasError('pattern')">
输入有错误
</ng-container>
</ng-template>
</nz-form-control>
</nz-form-item>
</form>

View File

@ -0,0 +1,3 @@
nz-form-item {
margin: 0;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { InputDurationComponent } from './input-duration.component';
describe('InputDurationComponent', () => {
let component: InputDurationComponent;
let fixture: ComponentFixture<InputDurationComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ InputDurationComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(InputDurationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,95 @@
import {
Component,
ChangeDetectionStrategy,
forwardRef,
OnInit,
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
NG_VALUE_ACCESSOR,
Validators,
} from '@angular/forms';
import { OnChangeType, OnTouchedType } from 'ng-zorro-antd/core/types';
import { formatDuration, parseDuration } from '../../../shared/utils';
import { filterValueChanges } from 'src/app/settings/shared/rx-operators';
@Component({
selector: 'app-input-duration',
templateUrl: './input-duration.component.html',
styleUrls: ['./input-duration.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputDurationComponent),
multi: true,
},
],
})
export class InputDurationComponent implements OnInit, ControlValueAccessor {
readonly formGroup: FormGroup;
private value: number = 0;
onChange: OnChangeType = () => {};
onTouched: OnTouchedType = () => {};
constructor(formBuilder: FormBuilder) {
this.formGroup = formBuilder.group({
duration: [
'',
[Validators.required, Validators.pattern(/^\d{2}:[0~5]\d:[0~5]\d$/)],
],
});
}
get durationControl() {
return this.formGroup.get('duration') as FormControl;
}
ngOnInit(): void {
this.durationControl.valueChanges
.pipe(filterValueChanges(this.durationControl))
.subscribe((displayValue) => {
this.onDisplayValueChange(displayValue);
});
}
writeValue(value: number): void {
this.value = value;
this.updateDisplayValue(value);
}
registerOnChange(fn: OnChangeType): void {
this.onChange = fn;
}
registerOnTouched(fn: OnTouchedType): void {
this.onTouched = fn;
}
setDisabledState(disabled: boolean): void {
if (disabled) {
this.durationControl.disable();
} else {
this.durationControl.enable();
}
}
private onDisplayValueChange(displayValue: string): void {
const value = parseDuration(displayValue);
if (typeof value === 'number' && this.value !== value) {
this.value = value;
this.onChange(value);
}
}
private updateDisplayValue(value: number): void {
const displayValue = formatDuration(value);
this.durationControl.setValue(displayValue);
}
}

View File

@ -0,0 +1,15 @@
<form nz-form [formGroup]="formGroup">
<nz-form-item>
<nz-form-control [nzErrorTip]="errorTip">
<input nz-input type="text" formControlName="filesize" />
<ng-template #errorTip let-control>
<ng-container *ngIf="control.hasError('required')">
请输入文件大小
</ng-container>
<ng-container *ngIf="control.hasError('pattern')">
输入有错误
</ng-container>
</ng-template>
</nz-form-control>
</nz-form-item>
</form>

View File

@ -0,0 +1,3 @@
nz-form-item {
margin: 0;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { InputFilesizeComponent } from './input-filesize.component';
describe('InputFilesizeComponent', () => {
let component: InputFilesizeComponent;
let fixture: ComponentFixture<InputFilesizeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ InputFilesizeComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(InputFilesizeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,98 @@
import {
Component,
ChangeDetectionStrategy,
forwardRef,
OnInit,
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
NG_VALUE_ACCESSOR,
Validators,
} from '@angular/forms';
import { OnChangeType, OnTouchedType } from 'ng-zorro-antd/core/types';
import { formatFilesize, parseFilesize } from '../../../shared/utils';
import { filterValueChanges } from 'src/app/settings/shared/rx-operators';
@Component({
selector: 'app-input-filesize',
templateUrl: './input-filesize.component.html',
styleUrls: ['./input-filesize.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputFilesizeComponent),
multi: true,
},
],
})
export class InputFilesizeComponent implements OnInit, ControlValueAccessor {
readonly formGroup: FormGroup;
private value: number = 0;
onChange: OnChangeType = () => {};
onTouched: OnTouchedType = () => {};
constructor(formBuilder: FormBuilder) {
this.formGroup = formBuilder.group({
filesize: [
'',
[
Validators.required,
Validators.pattern(/^\d{1,3}(?:\.\d{1,2})?\s?[GMK]?B$/),
],
],
});
}
get filesizeControl() {
return this.formGroup.get('filesize') as FormControl;
}
ngOnInit(): void {
this.filesizeControl.valueChanges
.pipe(filterValueChanges(this.filesizeControl))
.subscribe((displayValue) => {
this.onDisplayValueChange(displayValue);
});
}
writeValue(value: number): void {
this.value = value;
this.updateDisplayValue(value);
}
registerOnChange(fn: OnChangeType): void {
this.onChange = fn;
}
registerOnTouched(fn: OnTouchedType): void {
this.onTouched = fn;
}
setDisabledState(disabled: boolean): void {
if (disabled) {
this.filesizeControl.disable();
} else {
this.filesizeControl.enable();
}
}
private onDisplayValueChange(displayValue: string): void {
const value = parseFilesize(displayValue);
if (typeof value === 'number' && this.value !== value) {
this.value = value;
this.onChange(value);
}
}
private updateDisplayValue(value: number): void {
const displayValue = formatFilesize(value);
this.filesizeControl.setValue(displayValue);
}
}

View File

@ -1,29 +1,11 @@
import { Pipe, PipeTransform } from '@angular/core';
import { formatDuration } from '../utils';
@Pipe({
name: 'duration',
})
export class DurationPipe implements PipeTransform {
transform(totalSeconds: number): string {
if (totalSeconds < 0) {
throw RangeError(
'the argument totalSeconds must be greater than or equal to 0'
);
}
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds / 60) % 60);
const seconds = Math.floor(totalSeconds % 60);
let result = '';
if (hours > 0) {
result += hours + ':';
}
result += minutes < 10 ? '0' + minutes : minutes;
result += ':';
result += seconds < 10 ? '0' + seconds : seconds;
return result;
return formatDuration(totalSeconds, true);
}
}

View File

@ -1,8 +1,12 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { NzSpinModule } from 'ng-zorro-antd/spin';
import { NzPageHeaderModule } from 'ng-zorro-antd/page-header';
import { NzFormModule } from 'ng-zorro-antd/form';
import { NzInputModule } from 'ng-zorro-antd/input';
import { DataurlPipe } from './pipes/dataurl.pipe';
import { DurationPipe } from './pipes/duration.pipe';
@ -11,10 +15,12 @@ import { FilesizePipe } from './pipes/filesize.pipe';
import { QualityPipe } from './pipes/quality.pipe';
import { ProgressPipe } from './pipes/progress.pipe';
import { FilenamePipe } from './pipes/filename.pipe';
import { FilestatusPipe } from './pipes/filestatus.pipe';
import { SubPageContentDirective } from './directives/sub-page-content.directive';
import { PageSectionComponent } from './components/page-section/page-section.component';
import { SubPageComponent } from './components/sub-page/sub-page.component';
import { SubPageContentDirective } from './directives/sub-page-content.directive';
import { FilestatusPipe } from './pipes/filestatus.pipe';
import { InputFilesizeComponent } from './components/input-filesize/input-filesize.component';
import { InputDurationComponent } from './components/input-duration/input-duration.component';
@NgModule({
declarations: [
@ -23,14 +29,24 @@ import { FilestatusPipe } from './pipes/filestatus.pipe';
DataratePipe,
FilesizePipe,
QualityPipe,
SubPageComponent,
SubPageContentDirective,
PageSectionComponent,
ProgressPipe,
FilenamePipe,
FilestatusPipe,
SubPageContentDirective,
SubPageComponent,
PageSectionComponent,
InputFilesizeComponent,
InputDurationComponent,
],
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
NzSpinModule,
NzFormModule,
NzPageHeaderModule,
NzInputModule,
],
imports: [CommonModule, NzSpinModule, NzPageHeaderModule],
exports: [
DataurlPipe,
DurationPipe,
@ -39,11 +55,12 @@ import { FilestatusPipe } from './pipes/filestatus.pipe';
QualityPipe,
ProgressPipe,
FilenamePipe,
SubPageComponent,
SubPageContentDirective,
PageSectionComponent,
FilestatusPipe,
SubPageContentDirective,
SubPageComponent,
PageSectionComponent,
InputFilesizeComponent,
InputDurationComponent,
],
})
export class SharedModule {}

View File

@ -1,4 +1,5 @@
import { transform, isEqual, isObject } from 'lodash-es';
import * as filesize from 'filesize';
// ref: https://gist.github.com/Yimiprod/7ee176597fef230d1451
export function difference(object: object, base: object): object {
@ -85,3 +86,70 @@ export function toByteRateString(
const digits = precision - Math.floor(Math.abs(Math.log10(num))) - 1;
return num.toFixed(digits < 0 ? 0 : digits) + spacer + unit;
}
export function formatDuration(
totalSeconds: number,
concise: boolean = false
): string {
if (!(totalSeconds > 0)) {
totalSeconds = 0;
}
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds / 60) % 60);
const seconds = Math.floor(totalSeconds % 60);
let result = '';
if (concise) {
if (hours > 0) {
result += hours + ':';
}
} else {
result += hours < 10 ? '0' + hours : hours;
result += ':';
}
result += minutes < 10 ? '0' + minutes : minutes;
result += ':';
result += seconds < 10 ? '0' + seconds : seconds;
return result;
}
export function parseDuration(str: string): number | null {
try {
const [_, hours, minutes, seconds] = /(\d{1,2}):(\d{2}):(\d{2})/.exec(str)!;
return parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseInt(seconds);
} catch (error) {
console.error(`Failed to parse duration: ${str}`, error);
return null;
}
}
export function formatFilesize(size: number): string {
return filesize(size);
}
export function parseFilesize(str: string): number | null {
try {
const [_, num, unit] = /^(\d+(?:\.\d+)?)\s*([TGMK]?B)$/.exec(str)!;
switch (unit) {
case 'B':
return parseFloat(num);
case 'KB':
return 1024 ** 1 * parseFloat(num);
case 'MB':
return 1024 ** 2 * parseFloat(num);
case 'GB':
return 1024 ** 3 * parseFloat(num);
case 'TB':
return 1024 ** 4 * parseFloat(num);
default:
console.warn(`Unexpected unit: ${unit}`, str);
return null;
}
} catch (error) {
console.error(`Failed to parse filesize: ${str}`, error);
return null;
}
}

View File

@ -53,7 +53,7 @@
>
</nz-form-item>
<nz-form-item
class="setting-item"
class="setting-item filesize-limit"
*ngIf="
(options.recorder.streamFormat || model.recorder.streamFormat) ===
'flv' ||
@ -66,17 +66,25 @@
<nz-form-label
class="setting-label"
nzNoColon
[nzTooltipTitle]="splitFileTip"
[nzTooltipTitle]="filesizeLimitTip"
>大小限制</nz-form-label
>
<nz-form-control class="setting-control select">
<nz-select
<ng-template #filesizeLimitTip>
<p>
自动分割文件以限制录播文件大小
<br />
格式:数字 + 单位(GB, MB, KB, B)
<br />
不自动分割文件设置为 <strong>0 B</strong>
<br />
</p>
</ng-template>
<nz-form-control class="setting-control input">
<app-input-filesize
name="filesizeLimit"
[(ngModel)]="model.output.filesizeLimit"
[disabled]="options.output.filesizeLimit === null"
[nzOptions]="filesizeLimitOptions"
>
</nz-select>
></app-input-filesize>
</nz-form-control>
<label
nz-checkbox
@ -90,7 +98,7 @@
>
</nz-form-item>
<nz-form-item
class="setting-item"
class="setting-item duration-limit"
*ngIf="
(options.recorder.streamFormat || model.recorder.streamFormat) ===
'flv' ||
@ -103,17 +111,25 @@
<nz-form-label
class="setting-label"
nzNoColon
[nzTooltipTitle]="splitFileTip"
[nzTooltipTitle]="durationLimitTip"
>时长限制</nz-form-label
>
<nz-form-control class="setting-control select">
<nz-select
<ng-template #durationLimitTip>
<p>
自动分割文件以限制录播文件时长
<br />
格式HH:MM:SS
<br />
不自动分割文件设置为 <strong>00:00:00</strong>
<br />
</p>
</ng-template>
<nz-form-control class="setting-control input">
<app-input-duration
name="durationLimit"
[(ngModel)]="model.output.durationLimit"
[disabled]="options.output.durationLimit === null"
[nzOptions]="durationLimitOptions"
>
</nz-select>
></app-input-duration>
</nz-form-control>
<label
nz-checkbox

View File

@ -1,4 +1,4 @@
@use '../../settings/shared/styles/setting';
@use "../../settings/shared/styles/setting";
nz-divider {
margin: 0 !important;
@ -44,7 +44,8 @@ nz-divider {
}
}
&.input, &.textarea {
&.input,
&.textarea {
grid-template-columns: repeat(2, 1fr);
.setting-label {
@ -80,3 +81,19 @@ nz-divider {
}
}
}
.filesize-limit,
.duration-limit {
.setting-control {
&.input {
max-width: 8em !important;
width: 8em !important;
}
@media screen and (max-width: 319px) {
&.input {
margin-left: 0 !important;
}
}
}
}

View File

@ -21,15 +21,12 @@ import {
} from '../../settings/shared/setting.model';
import {
PATH_TEMPLATE_PATTERN,
FILESIZE_LIMIT_OPTIONS,
DURATION_LIMIT_OPTIONS,
STREAM_FORMAT_OPTIONS,
QUALITY_OPTIONS,
TIMEOUT_OPTIONS,
DISCONNECTION_TIMEOUT_OPTIONS,
BUFFER_OPTIONS,
DELETE_STRATEGIES,
SPLIT_FILE_TIP,
COVER_SAVE_STRATEGIES,
RECORDING_MODE_OPTIONS,
} from '../../settings/shared/constants/form';
@ -58,14 +55,7 @@ export class TaskSettingsDialogComponent implements OnChanges {
readonly warningTip =
'需要重启弹幕客户端才能生效,如果任务正在录制可能会丢失弹幕!';
readonly splitFileTip = SPLIT_FILE_TIP;
readonly pathTemplatePattern = PATH_TEMPLATE_PATTERN;
readonly filesizeLimitOptions = cloneDeep(FILESIZE_LIMIT_OPTIONS) as Mutable<
typeof FILESIZE_LIMIT_OPTIONS
>;
readonly durationLimitOptions = cloneDeep(DURATION_LIMIT_OPTIONS) as Mutable<
typeof DURATION_LIMIT_OPTIONS
>;
readonly streamFormatOptions = cloneDeep(STREAM_FORMAT_OPTIONS) as Mutable<
typeof STREAM_FORMAT_OPTIONS
>;
@ -121,6 +111,7 @@ export class TaskSettingsDialogComponent implements OnChanges {
}
handleConfirm(): void {
debugger;
this.confirm.emit(difference(this.options, this.taskOptions!));
this.close();
}
@ -132,7 +123,6 @@ export class TaskSettingsDialogComponent implements OnChanges {
const prop = key as keyof TaskOptions;
const options = this.options[prop];
const globalSettings = this.globalSettings[prop];
Reflect.set(
model,
prop,