1+ <!doctype html>
2+ < html lang ="en ">
3+
4+ < head >
5+ < meta charset ="utf-8 " />
6+ < meta name ="viewport " content ="width=device-width,initial-scale=1 " />
7+ < title > Movie Diff</ title >
8+
9+ < link rel ="stylesheet " href ="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css " />
10+ < style > @import url (http://fonts.googleapis.com/css?family=Inconsolata);@import url (http://fonts.googleapis.com/css?family=PT+Sans);@import url (http://fonts.googleapis.com/css?family=PT+Sans+Narrow:400, 700 );article , aside , details , figcaption , figure , footer , header , hgroup , nav , section , summary {display : block}audio , canvas , video {display : inline-block}audio : not ([controls ]){display : none;height : 0 }[hidden ]{display : none}html {font-family : sans-serif;-webkit-text-size-adjust : 100% ;-ms-text-size-adjust : 100% }body {margin : 0 }a : focus {outline : thin dotted}a : active , a : hover {outline : 0 }h1 {font-size : 2em }abbr [title ]{border-bottom : 1px dotted}b , strong {font-weight : bold}dfn {font-style : italic}mark {background : # ff0 ;color : # 000 }code , kbd , pre , samp {font-family : monospace, serif;font-size : 1em }pre {white-space : pre-wrap;word-wrap : break-word}q {quotes : "\201C" "\201D" "\2018" "\2019" }small {font-size : 80% }sub , sup {font-size : 75% ;line-height : 0 ;position : relative;vertical-align : baseline}sup {top : -0.5em }sub {bottom : -0.25em }img {border : 0 }svg : not (: root ){overflow : hidden}figure {margin : 0 }fieldset {border : 1px solid # c0c0c0 ;margin : 0 2px ;padding : .35em .625em .75em }legend {border : 0 ;padding : 0 }button , input , select , textarea {font-family : inherit;font-size : 100% ;margin : 0 }button , input {line-height : normal}button , html input [type = "button" ], input [type = "reset" ], input [type = "submit" ]{-webkit-appearance : button;cursor : pointer}button [disabled ], input [disabled ]{cursor : default}input [type = "checkbox" ], input [type = "radio" ]{box-sizing : border-box;padding : 0 }input [type = "search" ]{-webkit-appearance : textfield;-moz-box-sizing : content-box;-webkit-box-sizing : content-box;box-sizing : content-box}input [type = "search" ]::-webkit-search-cancel-button , input [type = "search" ]::-webkit-search-decoration {-webkit-appearance : none}button ::-moz-focus-inner , input ::-moz-focus-inner {border : 0 ;padding : 0 }textarea {overflow : auto;vertical-align : top}table {border-collapse : collapse;border-spacing : 0 }html {font-family : 'PT Sans' , sans-serif}pre , code {font-family : 'Inconsolata' , sans-serif}h1 , h2 , h3 , h4 , h5 , h6 {font-family : 'PT Sans Narrow' , sans-serif;font-weight : 700 }html {background-color : # 073642 ;color : # 839496 ;margin : 1em }body {background-color : # 002b36 ;margin : 0 auto;max-width : 23cm ;border : 1pt solid # 586e75 ;padding : 1em }code {background-color : # 073642 ;padding : 2px }a {color : # b58900 }a : visited {color : # cb4b16 }a : hover {color : # cb4b16 }h1 {color : # d33682 }h2 , h3 , h4 , h5 , h6 {color : # 859900 }pre {background-color : # 002b36 ;color : # 839496 ;border : 1pt solid # 586e75 ;padding : 1em ;box-shadow : 5pt 5pt 8pt # 073642 }pre code {background-color : # 002b36 }h1 {font-size : 2.8em }h2 {font-size : 2.4em }h3 {font-size : 1.8em }h4 {font-size : 1.4em }h5 {font-size : 1.3em }h6 {font-size : 1.15em }.tag {background-color : # 073642 ;color : # d33682 ;padding : 0 .2em }.todo , .next , .done {color : # 002b36 ;background-color : # dc322f ;padding : 0 .2em }.tag {-webkit-border-radius : .35em ;-moz-border-radius : .35em ;border-radius : .35em }.TODO {-webkit-border-radius : .2em ;-moz-border-radius : .2em ;border-radius : .2em ;background-color : # 2aa198 }.NEXT {-webkit-border-radius : .2em ;-moz-border-radius : .2em ;border-radius : .2em ;background-color : # 268bd2 }.ACTIVE {-webkit-border-radius : .2em ;-moz-border-radius : .2em ;border-radius : .2em ;background-color : # 268bd2 }.DONE {-webkit-border-radius : .2em ;-moz-border-radius : .2em ;border-radius : .2em ;background-color : # 859900 }.WAITING {-webkit-border-radius : .2em ;-moz-border-radius : .2em ;border-radius : .2em ;background-color : # cb4b16 }.HOLD {-webkit-border-radius : .2em ;-moz-border-radius : .2em ;border-radius : .2em ;background-color : # d33682 }.NOTE {-webkit-border-radius : .2em ;-moz-border-radius : .2em ;border-radius : .2em ;background-color : # d33682 }.CANCELLED {-webkit-border-radius : .2em ;-moz-border-radius : .2em ;border-radius : .2em ;background-color : # 859900 }</ style >
11+ < style >
12+ body {
13+ max-width : unset;
14+ display : flex;
15+ flex-direction : column;
16+ border : none;
17+ }
18+
19+ h1 {
20+ font-size : 20px ;
21+ margin : 0 0 12px
22+ }
23+
24+ .panel {
25+ flex : 1 ;
26+ border : 1px solid # 6a6a6a ;
27+ border-radius : 8px ;
28+ padding : 12px
29+ }
30+
31+ label {
32+ display : block;
33+ font-size : 15px ;
34+ }
35+
36+ .meta {
37+ font-size : 13px ;
38+ margin-top : 8px
39+ }
40+
41+ .controls {
42+ display : flex;
43+ gap : 8px ;
44+ align-items : center;
45+ margin : 12px 0
46+ }
47+
48+ .btn {
49+ padding : 8px 12px ;
50+ border-radius : 6px ;
51+ border : 1px solid # 6a6a6a ;
52+ cursor : pointer
53+ }
54+
55+ .btn .primary {
56+ background : # 0366d6 ;
57+ color : white;
58+ border-color : rgba (0 , 0 , 0 , 0.05 )
59+ }
60+
61+ .status {
62+ font-size : 13px
63+ }
64+
65+ .fileLabel {
66+ margin-bottom : 4px ;
67+ }
68+
69+ .files {
70+ display : flex;
71+ gap : 12px ;
72+ width : fit-content
73+ }
74+ </ style >
75+ </ head >
76+
77+ < body >
78+ < h1 > Movie Diff</ h1 >
79+ < div class ="files ">
80+ < div class ="panel " id ="leftPanel ">
81+ < label for ="leftZip " class ="fileLabel "> Old Movie</ label >
82+ < input id ="leftZip " type ="file " accept =".bk2 " />
83+ </ div >
84+
85+ < div class ="panel " id ="rightPanel ">
86+ < label for ="rightZip " class ="fileLabel "> New Movie</ label >
87+ < input id ="rightZip " type ="file " accept =".bk2 " />
88+ </ div >
89+ </ div >
90+
91+ < div class ="controls ">
92+ < button id ="renderBtn " class ="btn primary "> Render diff</ button >
93+ < label class ="btn " style ="display:flex;gap:8px;align-items:center "> < input id ="viewToggle " type ="checkbox "
94+ checked ="true " /> Split view</ label >
95+ </ div >
96+ < div class ="status " id ="status "> Waiting for files...</ div >
97+
98+ < div id ="diffContainer " class ="panel " style ="min-height:220px "> </ div >
99+
100+ < script src ="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js "> </ script >
101+ < script src ="https://cdn.jsdelivr.net/npm/diff@5.1.0/dist/diff.min.js "> </ script >
102+ < script src ="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html.min.js "> </ script >
103+
104+ < script >
105+ async function extractInputLogFromZip ( file ) {
106+ if ( ! file ) return { error : 'No file' } ;
107+ try {
108+ const arrayBuffer = await file . arrayBuffer ( ) ;
109+ const zip = await JSZip . loadAsync ( arrayBuffer ) ;
110+ const candidates = [ ] ;
111+ zip . forEach ( ( relativePath , zipEntry ) => {
112+ const name = relativePath . replace ( / ^ \/ + | ^ \. \/ / , '' ) ;
113+ if ( name . toLowerCase ( ) === 'input log.txt' . toLowerCase ( ) ) candidates . push ( zipEntry ) ;
114+ } ) ;
115+ if ( candidates . length === 0 ) {
116+ zip . forEach ( ( relativePath , zipEntry ) => {
117+ const parts = relativePath . split ( '/' ) ;
118+ const base = parts [ parts . length - 1 ] ;
119+ if ( base . toLowerCase ( ) === 'input log.txt' . toLowerCase ( ) ) candidates . push ( zipEntry ) ;
120+ } ) ;
121+ }
122+ if ( candidates . length === 0 ) return { error : 'Input Log.txt not found in the movie' } ;
123+ const entry = candidates [ 0 ] ;
124+ const text = await entry . async ( 'text' ) ;
125+ return { text, name : entry . name } ;
126+ } catch ( e ) {
127+ return { error : 'Failed to read movie: ' + ( e && e . message ? e . message : e ) } ;
128+ }
129+ }
130+
131+ const leftInput = document . getElementById ( 'leftZip' ) ;
132+ const rightInput = document . getElementById ( 'rightZip' ) ;
133+ const leftMeta = document . getElementById ( 'leftMeta' ) ;
134+ const rightMeta = document . getElementById ( 'rightMeta' ) ;
135+ const status = document . getElementById ( 'status' ) ;
136+ const renderBtn = document . getElementById ( 'renderBtn' ) ;
137+ const diffContainer = document . getElementById ( 'diffContainer' ) ;
138+ const viewToggle = document . getElementById ( 'viewToggle' ) ;
139+
140+ let leftFile , rightFile , leftText , rightText ;
141+
142+ leftInput . addEventListener ( 'change' , async ( e ) => {
143+ leftFile = e . target . files [ 0 ] ;
144+ if ( leftFile && rightFile ) {
145+ status . textContent = 'Ready' ;
146+ }
147+ } ) ;
148+ rightInput . addEventListener ( 'change' , async ( e ) => {
149+ rightFile = e . target . files [ 0 ] ;
150+ if ( leftFile && rightFile ) {
151+ status . textContent = 'Ready' ;
152+ }
153+ } ) ;
154+
155+ async function prepareAndRender ( ) {
156+ status . textContent = 'Reading movies...' ;
157+ diffContainer . innerHTML = '' ;
158+
159+ const leftRes = await extractInputLogFromZip ( leftFile ) ;
160+ if ( leftRes . error ) {
161+ status . textContent = 'Old Movie: ' + leftRes . error ;
162+ return ;
163+ }
164+ leftText = leftRes . text ;
165+
166+ const rightRes = await extractInputLogFromZip ( rightFile ) ;
167+ if ( rightRes . error ) {
168+ status . textContent = 'New Movie: ' + rightRes . error ;
169+ return ;
170+ }
171+ rightText = rightRes . text ;
172+
173+ status . textContent = 'Computing diff...' ;
174+
175+ const leftName = leftFile ? leftFile . name : 'left' ;
176+ const rightName = rightFile ? rightFile . name : 'right' ;
177+ const patch = Diff . createTwoFilesPatch ( leftName + '/Input Log.txt' , rightName + '/Input Log.txt' , leftText , rightText , '' , '' , { context : 3 } ) ;
178+
179+ const outputFormat = viewToggle . checked ? 'side-by-side' : 'line-by-line' ;
180+ const diffHtml = Diff2Html . html ( patch , { drawFileList : false , matching : 'lines' , colorScheme : 'dark' , outputFormat } ) ;
181+ diffContainer . innerHTML = diffHtml ;
182+
183+ status . textContent = 'Rendered' ;
184+ }
185+
186+ renderBtn . addEventListener ( 'click' , async ( ) => {
187+ if ( ! leftFile || ! rightFile ) {
188+ status . textContent = 'Select both movie files first.' ;
189+ return ;
190+ }
191+ await prepareAndRender ( ) ;
192+ } ) ;
193+
194+ viewToggle . addEventListener ( 'change' , ( ) => {
195+ if ( ! leftText || ! rightText ) return ;
196+ const leftName = leftFile ? leftFile . name : 'left' ;
197+ const rightName = rightFile ? rightFile . name : 'right' ;
198+ const patch = Diff . createTwoFilesPatch ( leftName + '/Input Log.txt' , rightName + '/Input Log.txt' , leftText , rightText , '' , '' , { context : 3 } ) ;
199+ const outputFormat = viewToggle . checked ? 'side-by-side' : 'line-by-line' ;
200+ const diffHtml = Diff2Html . html ( patch , { drawFileList : false , matching : 'lines' , colorScheme : 'dark' , outputFormat } ) ;
201+ diffContainer . innerHTML = diffHtml ;
202+
203+ status . textContent = 'Rendered' ;
204+ } ) ;
205+
206+ function setupDrop ( panelEl , inputEl , metaEl ) {
207+ panelEl . addEventListener ( 'dragover' , ( e ) => { e . preventDefault ( ) ; panelEl . style . borderColor = '#bbb' ; } ) ;
208+ panelEl . addEventListener ( 'dragleave' , ( ) => { panelEl . style . borderColor = '' ; } ) ;
209+ panelEl . addEventListener ( 'drop' , ( e ) => {
210+ e . preventDefault ( ) ; panelEl . style . borderColor = '' ;
211+ const f = e . dataTransfer . files [ 0 ] ;
212+ if ( f ) { inputEl . files = e . dataTransfer . files ; inputEl . dispatchEvent ( new Event ( 'change' ) ) ; }
213+ } ) ;
214+ }
215+ setupDrop ( document . getElementById ( 'leftPanel' ) , leftInput , leftMeta ) ;
216+ setupDrop ( document . getElementById ( 'rightPanel' ) , rightInput , rightMeta ) ;
217+
218+ status . textContent = 'Select two movie files.' ;
219+ </ script >
220+ </ body >
221+
222+ </ html >
0 commit comments